feat: refactor AdminCreateUserForm to named export and improve error message handling; add LoginForm component with validation

This commit is contained in:
ngc2207 2024-12-03 18:03:14 +08:00
parent 6b5f0cd8bf
commit d6202c5c57
7 changed files with 181 additions and 6 deletions

View File

@ -63,7 +63,7 @@ const defaultValues: z.infer<typeof formSchema> = {
must_change_password: true,
};
export default function AdminCreateUserForm() {
export function AdminCreateUserForm() {
const { toast } = useToast();
const { user, error, loading, adminCreateUser } = adminCreateUserStore();
@ -101,7 +101,7 @@ export default function AdminCreateUserForm() {
toast({
variant: "destructive",
title: "Failed to create user",
description: `${error.message}`,
description: error.message,
action: <ToastAction altText="Try again">Try again</ToastAction>,
});
}

View File

@ -1,4 +1,4 @@
import AdminCreateUserForm from "./components/admin-create-user-form";
import { AdminCreateUserForm } from "./components/admin-create-user-form";
export default function AdminCreateUserPage() {
return (

View File

@ -0,0 +1,157 @@
"use client";
import { z } from "zod";
import Link from "next/link";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import logger from "@/lib/logger";
import { useEffect } from "react";
import { Loader2 } from "lucide-react";
import { useForm } from "react-hook-form";
import { useToast } from "@/hooks/use-toast";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { ToastAction } from "@/components/ui/toast";
import { zodResolver } from "@hookform/resolvers/zod";
import userGetCurrentStore from "@/app/actions/(gitea)/user/store";
import { Form, FormField, FormItem, FormLabel } from "@/components/ui/form";
const formSchema = z.object({
username: z.string().refine(
(value) => {
const isEmail = value.includes("@");
if (isEmail) {
return z
.string()
.email("Invalid email address")
.max(254, "Email is too long (max 254 characters)")
.safeParse(value).success;
} else {
return z
.string()
.min(1, "Username cannot be empty")
.max(40, "Username is too long (max 40 characters)")
.safeParse(value).success;
}
},
{
message: "Invalid email address or username",
}
),
password: z
.string()
.min(8, "Password must be at least 8 characters")
.max(255, "Password must be at most 255 characters"),
});
const defaultValues: z.infer<typeof formSchema> = {
username: "",
password: "",
};
export function LoginForm() {
const { toast } = useToast();
const { user, error, loading, userGetCurrent } = userGetCurrentStore();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues,
});
function onSubmit(values: z.infer<typeof formSchema>) {
logger.info({ values }, "submitting form");
userGetCurrent(values.username, values.password).then(() => {
form.reset(defaultValues);
});
}
function onReset() {
form.reset(defaultValues);
}
useEffect(() => {
form.reset(defaultValues);
}, [form]);
useEffect(() => {
if (user) {
toast({
variant: "default",
title: "Successfully Logged In",
description: `Welcome back, ${user.login}`,
action: <ToastAction altText="Continue">Continue</ToastAction>,
});
} else if (error) {
toast({
variant: "destructive",
title: "Error Logging In",
description: error.message,
action: <ToastAction altText="Retry">Retry</ToastAction>,
});
}
}, [user, error, toast]);
return (
<Card className="mx-auto max-w-sm">
<CardHeader>
<CardTitle className="text-2xl">Login</CardTitle>
<CardDescription>
Enter your email below to login to your account
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-4">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem className="grid gap-2">
<FormLabel>Username or Email Address</FormLabel>
<Input {...field} />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem className="grid gap-2">
<div className="flex items-center">
<FormLabel>Password</FormLabel>
<Link
href="#"
className="ml-auto inline-block text-sm underline"
>
Forgot your password?
</Link>
</div>
<Input {...field} type="password" />
</FormItem>
)}
/>
<div className="flex items-center justify-between">
<Button variant="outline" onClick={onReset}>
Reset
</Button>
<Button type="submit" disabled={loading}>
{loading ? (
<>
<Loader2 className="animate-spin" /> Login...
</>
) : (
"Login"
)}
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,9 @@
import { LoginForm } from "@/app/(main)/login/components/login-form";
export default function LoginPage() {
return (
<div className="h-full w-full flex items-center justify-center px-4">
<LoginForm />
</div>
);
}

View File

@ -1,6 +1,8 @@
"use server";
import base64 from "base-64";
import { APIError, RequestParams, User } from "gitea-js";
import { callGiteaApi, gitea, GiteaApiResponse } from "@/lib/gitea";
import { api, callGiteaApi, GiteaApiResponse } from "@/lib/gitea";
export async function userGetCurrent(
username: string,
@ -14,7 +16,7 @@ export async function userGetCurrent(
};
const context = { username, password };
const result = await callGiteaApi(
() => gitea.user.userGetCurrent(params),
() => api.user.userGetCurrent(params),
"Successfully retrieved current user",
"Failed to retrieve current user",
context

View File

@ -33,3 +33,5 @@ const userGetCurrentStore = create<UserGetCurrentState>((set) => ({
}
},
}));
export default userGetCurrentStore;

View File

@ -2,6 +2,11 @@ import logger from "./logger";
import fetch from "cross-fetch";
import { APIError, giteaApi } from "gitea-js";
const api = giteaApi(process.env.GITEA_URL as string, {
token: "",
customFetch: fetch,
});
const gitea = giteaApi(process.env.GITEA_URL as string, {
token: process.env.GITEA_TOKEN,
customFetch: fetch,
@ -31,5 +36,5 @@ async function callGiteaApi<D>(
}
}
export { gitea, callGiteaApi };
export { api, gitea, callGiteaApi };
export type { GiteaApiResponse };