fix(panel): override generic already_exists error message during user creation and profile editing

This commit is contained in:
Lemon-miaow committed 2026-07-04 09:33:41 +08:00
1 parent 3347cc05d5
commit 67b4e1904e
10 files changed
+234 -188

No files matched your search

+152 -100
View File
@@ -1,5 +1,5 @@
import { useState } from "react";
import { Plus, Loader2 } from "lucide-react";
import { Plus, Loader2, Copy, Check } from "lucide-react";
import { useTranslation } from "react-i18next";
import {
Dialog,
@@ -26,24 +26,39 @@ interface Props {
onCreated: (id: string) => void;
}
function generateRandomPassword(length = 16): string {
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$";
let password = "";
for (let i = 0; i < length; i++) {
password += chars.charAt(Math.floor(Math.random() * chars.length));
}
return password;
}
export function CreateUserDialog({ onCreated }: Props) {
const { t } = useTranslation("admin");
const [open, setOpen] = useState(false);
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [role, setRole] = useState<"user" | "admin">("user");
const [password, setPassword] = useState("");
const [mustChange, setMustChange] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [err, setErr] = useState<string | null>(null);
// Success state fields
const [createdUser, setCreatedUser] = useState<any | null>(null);
const [generatedPassword, setGeneratedPassword] = useState("");
const [copied, setCopied] = useState(false);
function reset() {
setUsername("");
setEmail("");
setRole("user");
setPassword("");
setMustChange(true);
setErr(null);
setCreatedUser(null);
setGeneratedPassword("");
setCopied(false);
}
async function handleSubmit(e: React.FormEvent) {
@@ -55,30 +70,51 @@ export function CreateUserDialog({ onCreated }: Props) {
setErr(t("users_create_validation_username"));
return;
}
if (password.length < 8) {
setErr(t("users_create_validation_password"));
return;
}
const genPassword = generateRandomPassword();
setSubmitting(true);
try {
const u = await api.createUser({
username: username.trim(),
email: email.trim() || undefined,
role,
password,
password: genPassword,
must_change_password: mustChange,
});
setOpen(false);
reset();
onCreated(u.id);
} catch (e) {
setErr(humanizeError(e));
setGeneratedPassword(genPassword);
setCreatedUser(u);
} catch (e: any) {
if (e && e.code === "already_exists") {
setErr(t("users_create_validation_username_taken") || "该用户名已被使用。");
} else {
setErr(humanizeError(e));
}
} finally {
setSubmitting(false);
}
}
const handleCopy = async () => {
if (!createdUser) return;
const text = `Username: ${createdUser.username}\nPassword: ${generatedPassword}`;
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (e) {
// ignore
}
};
const handleDone = () => {
const id = createdUser?.id;
setOpen(false);
reset();
if (id) {
onCreated(id);
}
};
return (
<Dialog open={open} onOpenChange={(v) => { setOpen(v); if (!v) reset(); }}>
<DialogTrigger asChild>
@@ -87,103 +123,119 @@ export function CreateUserDialog({ onCreated }: Props) {
{t("users_create_btn")}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogContent className="sm:max-w-md" hideClose={submitting}>
<DialogHeader>
<DialogTitle>{t("users_create_title")}</DialogTitle>
<DialogTitle>{createdUser ? t("users_create_success_title") || "创建成功" : t("users_create_title")}</DialogTitle>
<DialogDescription>
{t("users_create_desc")}
{createdUser
? t("users_create_success_desc") || "请务必复制并妥善保管该用户的初始凭据,关闭后密码将不再显示。"
: t("users_create_desc")}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Username */}
<div className="space-y-1.5">
<Label className="text-xs font-semibold text-muted-foreground">
{t("users_field_username")} *
</Label>
<Input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder={t("users_create_username_placeholder")}
className="h-9 text-sm"
autoFocus
/>
{createdUser ? (
<div className="space-y-4">
<div className="rounded-md border border-border/50 bg-muted/20 p-4 space-y-3">
<div className="space-y-1">
<Label className="text-xs font-semibold text-muted-foreground">{t("users_field_username")}</Label>
<div className="font-mono text-sm font-semibold select-all">{createdUser.username}</div>
</div>
<div className="space-y-1">
<Label className="text-xs font-semibold text-muted-foreground">{t("users_field_password")}</Label>
<div className="font-mono text-sm font-semibold text-emerald-600 dark:text-emerald-400 select-all">
{generatedPassword}
</div>
</div>
</div>
<DialogFooter className="flex flex-row justify-end gap-2">
<Button type="button" variant="outline" onClick={handleCopy} className="gap-1.5">
{copied ? <Check className="h-4 w-4 text-emerald-500" /> : <Copy className="h-4 w-4" />}
{copied ? t("common:copied") || "已复制" : t("common:copy") || "复制凭据"}
</Button>
<Button type="button" onClick={handleDone}>
{t("common:done") || "完成"}
</Button>
</DialogFooter>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
{/* Username */}
<div className="space-y-1.5">
<Label className="text-xs font-semibold text-muted-foreground">
{t("users_field_username")} *
</Label>
<Input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder={t("users_create_username_placeholder")}
className="h-9 text-sm"
autoFocus
/>
</div>
{/* Email */}
<div className="space-y-1.5">
<Label className="text-xs font-semibold text-muted-foreground">
{t("users_field_email")}
</Label>
<Input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="[email protected]"
className="h-9 text-sm"
/>
</div>
{/* Email */}
<div className="space-y-1.5">
<Label className="text-xs font-semibold text-muted-foreground">
{t("users_field_email")}
</Label>
<Input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="[email protected]"
className="h-9 text-sm"
/>
</div>
{/* Role */}
<div className="space-y-1.5">
<Label className="text-xs font-semibold text-muted-foreground">
{t("users_field_role")}
</Label>
<Select value={role} onValueChange={(v: "user" | "admin") => setRole(v)}>
<SelectTrigger className="h-9 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="user">{t("users_role_user")}</SelectItem>
<SelectItem value="admin">{t("users_role_admin")}</SelectItem>
</SelectContent>
</Select>
</div>
{/* Role */}
<div className="space-y-1.5">
<Label className="text-xs font-semibold text-muted-foreground">
{t("users_field_role")}
</Label>
<Select value={role} onValueChange={(v: "user" | "admin") => setRole(v)}>
<SelectTrigger className="h-9 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="user">{t("users_role_user")}</SelectItem>
<SelectItem value="admin">{t("users_role_admin")}</SelectItem>
</SelectContent>
</Select>
</div>
{/* Password */}
<div className="space-y-1.5">
<Label className="text-xs font-semibold text-muted-foreground">
{t("users_field_password")} *
</Label>
<Input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="min. 8 characters"
className="h-9 text-sm"
/>
</div>
{/* Must change password toggle */}
<label className="flex items-center gap-2 cursor-pointer select-none">
<input
type="checkbox"
checked={mustChange}
onChange={(e) => setMustChange(e.target.checked)}
className="h-4 w-4 rounded border-border"
/>
<span className="text-sm text-foreground">
{t("users_create_must_change")}
</span>
</label>
{/* Must change password toggle */}
<label className="flex items-center gap-2 cursor-pointer select-none">
<input
type="checkbox"
checked={mustChange}
onChange={(e) => setMustChange(e.target.checked)}
className="h-4 w-4 rounded border-border"
/>
<span className="text-sm text-foreground">
{t("users_create_must_change")}
</span>
</label>
{err && (
<p className="rounded-md border border-destructive/20 bg-destructive/10 p-3 text-sm text-destructive">
{err}
</p>
)}
{err && (
<p className="rounded-md border border-destructive/20 bg-destructive/10 p-3 text-sm text-destructive">
{err}
</p>
)}
<DialogFooter>
<Button type="submit" disabled={submitting} className="gap-1.5">
{submitting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Plus className="h-4 w-4" />
)}
{t("users_create_btn")}
</Button>
</DialogFooter>
</form>
<DialogFooter>
<Button type="submit" disabled={submitting} className="gap-1.5">
{submitting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Plus className="h-4 w-4" />
)}
{t("users_create_btn")}
</Button>
</DialogFooter>
</form>
)}
</DialogContent>
</Dialog>
);
}
}
-1
View File
@@ -1,5 +1,4 @@
import { Outlet } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useTier } from "@/lib/tier";
import { NotAuthorized } from "@/components/States";
import { Loading } from "@/components/States";