From a2ce1c6b7b008fe484fff8fb0b21750c8f972100 Mon Sep 17 00:00:00 2001 From: m4 Date: Wed, 12 Aug 2026 16:50:44 +0800 Subject: [PATCH] feat(webui): add create-user dialog --- src/app/components/UserCreateDialog.tsx | 165 ++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 src/app/components/UserCreateDialog.tsx diff --git a/src/app/components/UserCreateDialog.tsx b/src/app/components/UserCreateDialog.tsx new file mode 100644 index 0000000..363ee8f --- /dev/null +++ b/src/app/components/UserCreateDialog.tsx @@ -0,0 +1,165 @@ +"use client"; + +import { FormEvent, useEffect, useState } from "react"; +import { useTranslations } from "next-intl"; +import { toast } from "sonner"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + isValidPassword, + isValidUsername, + type ManagedUserRole, +} from "@/lib/userManagement"; + +interface UserCreateDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onChanged: () => Promise; +} + +export function UserCreateDialog({ + open, + onOpenChange, + onChanged, +}: UserCreateDialogProps) { + const t = useTranslations("userManagement"); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [role, setRole] = useState("user"); + const [error, setError] = useState(null); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (!open) { + setUsername(""); + setPassword(""); + setRole("user"); + setError(null); + setSaving(false); + } + }, [open]); + + const canSubmit = + isValidUsername(username.trim()) && isValidPassword(password) && !saving; + + const submit = async (event: FormEvent) => { + event.preventDefault(); + if (!canSubmit) return; + setSaving(true); + setError(null); + try { + const response = await fetch("/api/auth/users", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "same-origin", + body: JSON.stringify({ username: username.trim(), password, role }), + }); + const body = (await response.json().catch(() => ({}))) as { + message?: string; + error?: string; + }; + if (!response.ok) { + throw new Error(body.message || body.error || "Request failed."); + } + toast.success(t("create.created", { username: username.trim() })); + onOpenChange(false); + await onChanged(); + } catch (reason) { + setError(reason instanceof Error ? reason.message : "Request failed."); + setSaving(false); + } + }; + + return ( + + + + {t("create.title")} + {t("create.description")} + +
+
+ + setUsername(event.target.value)} + disabled={saving} + autoFocus + /> +

+ {t("create.usernameHint")} +

+
+
+ + setPassword(event.target.value)} + disabled={saving} + /> +

+ {t("create.passwordHint")} +

+
+
+ + +
+ {error && ( +

+ {error} +

+ )} + + + + +
+
+
+ ); +}