Unverified Commit 67b4e190 authored by Lemon-miaow's avatar Lemon-miaow
Browse files

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

parent 3347cc05
Loading
Loading
Loading
Loading
+2 −0
Changes for .gitignore: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -29,6 +29,8 @@ plugins/*/bin/
.vscode/
.idea/
*.iml
.DS_Store
**/.DS_Store

# ---- Secrets / keys (NEVER commit) ----
*.pem
+81 −29
Changes for panel/src/components/CreateUserDialog.tsx: 81 added lines, 29 removed lines.
Original line number Diff line number Diff line
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) {
      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,13 +123,42 @@ 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>

        {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">
@@ -139,20 +204,6 @@ export function CreateUserDialog({ onCreated }: Props) {
              </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
@@ -183,6 +234,7 @@ export function CreateUserDialog({ onCreated }: Props) {
              </Button>
            </DialogFooter>
          </form>
        )}
      </DialogContent>
    </Dialog>
  );
+0 −1
Changes for panel/src/components/RequireOwner.tsx: 0 added lines, 1 removed line.
Original line number Diff line number Diff line
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";
+3 −0
Changes for panel/src/i18n/resources/en-US/admin.json: 3 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -108,8 +108,11 @@
  "users_create_btn": "Create User",
  "users_create_title": "Create New User",
  "users_create_desc": "Create a new platform account. The user will receive the initial password and will be prompted to change it on first login if the toggle is enabled.",
  "users_create_success_title": "User Created Successfully",
  "users_create_success_desc": "Please copy and save the initial credentials. Once you close this dialog, the initial password cannot be viewed again!",
  "users_create_username_placeholder": "e.g. alice",
  "users_create_validation_username": "Username is required.",
  "users_create_validation_username_taken": "This username is already taken.",
  "users_create_validation_password": "Password must be at least 8 characters.",
  "users_create_must_change": "Require password change on first login",
  "users_search_placeholder": "Search username or email...",
+3 −0
Changes for panel/src/i18n/resources/zh-CN/admin.json: 3 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -108,8 +108,11 @@
  "users_create_btn": "创建用户",
  "users_create_title": "创建新用户",
  "users_create_desc": "创建一个新的平台账号。用户将收到初始密码,如果开启「首次登录修改密码」,用户将在首次登录时被要求修改密码。",
  "users_create_success_title": "用户创建成功",
  "users_create_success_desc": "请复制并妥善保管该用户的初始凭据。关闭此对话框后,此初始密码将无法再次查看!",
  "users_create_username_placeholder": "例如: alice",
  "users_create_validation_username": "用户名为必填项。",
  "users_create_validation_username_taken": "该用户名已被使用。",
  "users_create_validation_password": "密码至少需要 8 个字符。",
  "users_create_must_change": "要求首次登录修改密码",
  "users_search_placeholder": "搜索用户名或邮箱...",
Loading