fix(panel): override generic already_exists error message during user creation and profile editing
This commit is contained in:
10 files changed
+234
-188
No files matched your search
@@ -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,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";
|
||||
|
||||
Reference in new issue
Block a user