feat(auth) : add admin account management
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
LayoutDashboardIcon,
|
||||
LogOutIcon,
|
||||
PlusIcon,
|
||||
UserPlusIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -28,6 +29,15 @@ export function AdminHeader() {
|
||||
<span className="hidden sm:inline">ไกด์ช่องเกนชินไม่ใช่เกมมือถือ</span>
|
||||
</Link>
|
||||
<nav aria-label="Admin navigation" className="flex items-center gap-1">
|
||||
<Button
|
||||
variant={pathname === "/admin/register" ? "secondary" : "ghost"}
|
||||
size="sm"
|
||||
nativeButton={false}
|
||||
render={<Link href="/admin/register" />}
|
||||
>
|
||||
<UserPlusIcon data-icon="inline-start" />
|
||||
<span className="hidden sm:inline">Accounts</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={pathname === "/admin" ? "secondary" : "ghost"}
|
||||
size="sm"
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { CircleAlertIcon, UserPlusIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { CircleAlertIcon, Trash2Icon, UserPlusIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { type FormEvent, useState } from "react";
|
||||
import { useActionState, useEffect, useRef, useTransition } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
createAccount,
|
||||
initialCreateAccountState,
|
||||
removeAccount,
|
||||
} from "@/app/admin/register/actions";
|
||||
import { DeleteConfirmationDialog } from "@/components/admin/delete-confirmation-dialog";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/components/ui/alert";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -27,93 +33,57 @@ import {
|
||||
} from "@/components/ui/field";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { authClient } from "@/lib/auth/client";
|
||||
|
||||
function registrationError(code: string | undefined): string {
|
||||
switch (code) {
|
||||
case "ADMIN_REGISTRATION_FORBIDDEN":
|
||||
return "อีเมลนี้ไม่ได้รับอนุญาตให้สร้างบัญชีผู้ดูแล";
|
||||
case "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL":
|
||||
return "บัญชีนี้มีอยู่แล้ว โปรดกลับไปเข้าสู่ระบบ";
|
||||
case "PASSWORD_TOO_SHORT":
|
||||
return "รหัสผ่านต้องมีอย่างน้อย 8 ตัวอักษร";
|
||||
case "PASSWORD_TOO_LONG":
|
||||
return "รหัสผ่านต้องไม่เกิน 128 ตัวอักษร";
|
||||
default:
|
||||
return "สร้างบัญชีไม่สำเร็จ โปรดตรวจสอบข้อมูลแล้วลองอีกครั้ง";
|
||||
}
|
||||
}
|
||||
|
||||
export function RegisterCard() {
|
||||
export function RegisterCard({
|
||||
accounts,
|
||||
currentUserId,
|
||||
}: {
|
||||
accounts: Array<{ id: string; name: string; email: string; createdAt: Date }>;
|
||||
currentUserId: string;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const [state, formAction, pending] = useActionState(
|
||||
createAccount,
|
||||
initialCreateAccountState,
|
||||
);
|
||||
const [removing, startRemoval] = useTransition();
|
||||
|
||||
async function register(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
setError(null);
|
||||
useEffect(() => {
|
||||
if (state.status !== "success") return;
|
||||
formRef.current?.reset();
|
||||
router.refresh();
|
||||
}, [router, state]);
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
const name = String(formData.get("name") ?? "").trim();
|
||||
const email = String(formData.get("email") ?? "").trim();
|
||||
const password = String(formData.get("password") ?? "");
|
||||
const confirmation = String(formData.get("passwordConfirmation") ?? "");
|
||||
|
||||
if (!name) {
|
||||
setError("กรุณากรอกชื่อผู้ดูแล");
|
||||
return;
|
||||
}
|
||||
if (password !== confirmation) {
|
||||
setError("รหัสผ่านทั้งสองช่องไม่ตรงกัน");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await authClient.signUp.email({
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
});
|
||||
if (result.error) {
|
||||
setError(registrationError(result.error.code));
|
||||
return;
|
||||
function remove(userId: string) {
|
||||
startRemoval(async () => {
|
||||
const result = await removeAccount(userId);
|
||||
if (result.status === "error") toast.error(result.message);
|
||||
else {
|
||||
toast.success("ลบบัญชีแล้ว");
|
||||
router.refresh();
|
||||
}
|
||||
router.push("/admin");
|
||||
router.refresh();
|
||||
} catch {
|
||||
setError("เชื่อมต่อระบบลงทะเบียนไม่ได้ โปรดลองอีกครั้ง");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">สร้างบัญชีผู้ดูแล</CardTitle>
|
||||
<CardDescription>
|
||||
ลงทะเบียนด้วยอีเมลและรหัสผ่านสำหรับ Better Auth
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={register}>
|
||||
<div className="grid gap-6 lg:grid-cols-[minmax(0,28rem)_minmax(0,1fr)]">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>สร้างบัญชี</CardTitle>
|
||||
<CardDescription>
|
||||
เพิ่มบัญชีผู้ดูแลที่สามารถเข้าสู่ระบบและจัดการเนื้อหาได้
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form ref={formRef} action={formAction}>
|
||||
<FieldGroup>
|
||||
<Alert>
|
||||
<CircleAlertIcon aria-hidden="true" />
|
||||
<AlertTitle>หน้าลงทะเบียนชั่วคราว</AlertTitle>
|
||||
<AlertDescription>
|
||||
ระบบรับเฉพาะอีเมลที่ระบุไว้ใน ADMIN_EMAIL เท่านั้น
|
||||
ควรนำหน้านี้ออกหลังสร้างบัญชีสำเร็จ
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="register-name">ชื่อผู้ดูแล</FieldLabel>
|
||||
<FieldLabel htmlFor="register-name">ชื่อ</FieldLabel>
|
||||
<Input
|
||||
id="register-name"
|
||||
name="name"
|
||||
autoComplete="name"
|
||||
disabled={loading}
|
||||
disabled={pending}
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
@@ -126,7 +96,7 @@ export function RegisterCard() {
|
||||
autoComplete="username"
|
||||
autoCapitalize="none"
|
||||
spellCheck={false}
|
||||
disabled={loading}
|
||||
disabled={pending}
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
@@ -139,7 +109,7 @@ export function RegisterCard() {
|
||||
autoComplete="new-password"
|
||||
minLength={8}
|
||||
maxLength={128}
|
||||
disabled={loading}
|
||||
disabled={pending}
|
||||
required
|
||||
/>
|
||||
<FieldDescription>อย่างน้อย 8 ตัวอักษร</FieldDescription>
|
||||
@@ -155,36 +125,70 @@ export function RegisterCard() {
|
||||
autoComplete="new-password"
|
||||
minLength={8}
|
||||
maxLength={128}
|
||||
disabled={loading}
|
||||
disabled={pending}
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
{error ? (
|
||||
<Alert variant="destructive">
|
||||
{state.message ? (
|
||||
<Alert variant={state.status === "error" ? "destructive" : "default"}>
|
||||
<CircleAlertIcon aria-hidden="true" />
|
||||
<AlertTitle>สร้างบัญชีไม่สำเร็จ</AlertTitle>
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
<AlertTitle>
|
||||
{state.status === "error" ? "สร้างบัญชีไม่สำเร็จ" : "สร้างบัญชีแล้ว"}
|
||||
</AlertTitle>
|
||||
<AlertDescription>{state.message}</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
<Button className="w-full" size="lg" type="submit" disabled={loading}>
|
||||
{loading ? (
|
||||
<Button className="w-full" size="lg" type="submit" disabled={pending}>
|
||||
{pending ? (
|
||||
<Spinner data-icon="inline-start" />
|
||||
) : (
|
||||
<UserPlusIcon data-icon="inline-start" aria-hidden="true" />
|
||||
)}
|
||||
{loading ? "กำลังสร้างบัญชี…" : "สร้างบัญชีผู้ดูแล"}
|
||||
{pending ? "กำลังสร้างบัญชี…" : "สร้างบัญชี"}
|
||||
</Button>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-center">
|
||||
<Link
|
||||
href="/admin/login"
|
||||
className={buttonVariants({ variant: "link", size: "sm" })}
|
||||
>
|
||||
กลับไปเข้าสู่ระบบ
|
||||
</Link>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>บัญชีในระบบ</CardTitle>
|
||||
<CardDescription>{accounts.length} บัญชี</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
{accounts.map((account) => {
|
||||
const isCurrent = account.id === currentUserId;
|
||||
return (
|
||||
<div key={account.id} className="flex items-center gap-3 rounded-lg border p-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate font-medium">{account.name}</p>
|
||||
<p className="truncate text-sm text-muted-foreground">{account.email}</p>
|
||||
</div>
|
||||
{isCurrent ? (
|
||||
<span className="text-sm text-muted-foreground">บัญชีปัจจุบัน</span>
|
||||
) : (
|
||||
<DeleteConfirmationDialog
|
||||
trigger={<Button variant="ghost" size="icon-sm" disabled={removing} />}
|
||||
title={`ลบบัญชี ${account.name}?`}
|
||||
description="ผู้ใช้นี้จะออกจากระบบและไม่สามารถเข้าสู่ระบบได้อีก การดำเนินการนี้ย้อนกลับไม่ได้"
|
||||
confirmLabel="ลบบัญชี"
|
||||
onConfirm={() => remove(account.id)}
|
||||
>
|
||||
<Trash2Icon />
|
||||
<span className="sr-only">ลบบัญชี {account.name}</span>
|
||||
</DeleteConfirmationDialog>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
บัญชีทุกบัญชีมีสิทธิ์ผู้ดูแลระบบ
|
||||
</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user