From 148118d969a8e1c3df818eea43644c9ec3b56945 Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Sat, 16 May 2026 23:08:19 +0800 Subject: [PATCH] feat: add generate strong password button to Add Members form Add a "Generate strong password" button with Sparkles icon to the Add Members form in Settings > People. Generated passwords are shown in plain text with a copy button and an amber warning to copy before creating the user. Also upgraded the button style on the change password page to match. Translated copy/warning strings for all 21 locales. Closes #139 --- .../components/settings/settings-dialog.tsx | 101 ++++++++++++++++-- apps/web/src/pages/change-password-page.tsx | 4 +- packages/shared/src/i18n/ar.ts | 3 + packages/shared/src/i18n/de.ts | 4 + packages/shared/src/i18n/en.ts | 4 + packages/shared/src/i18n/es.ts | 4 + packages/shared/src/i18n/fr.ts | 4 + packages/shared/src/i18n/hi.ts | 3 + packages/shared/src/i18n/id.ts | 4 + packages/shared/src/i18n/it.ts | 4 + packages/shared/src/i18n/ja.ts | 4 + packages/shared/src/i18n/ko.ts | 3 + packages/shared/src/i18n/nl.ts | 4 + packages/shared/src/i18n/pl.ts | 4 + packages/shared/src/i18n/pt-BR.ts | 3 + packages/shared/src/i18n/ru.ts | 4 + packages/shared/src/i18n/sv.ts | 4 + packages/shared/src/i18n/th.ts | 3 + packages/shared/src/i18n/tr.ts | 4 + packages/shared/src/i18n/uk.ts | 4 + packages/shared/src/i18n/vi.ts | 4 + packages/shared/src/i18n/zh-CN.ts | 3 + packages/shared/src/i18n/zh-TW.ts | 3 + 23 files changed, 171 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/settings/settings-dialog.tsx b/apps/web/src/components/settings/settings-dialog.tsx index 4e919c76..d7843d21 100644 --- a/apps/web/src/components/settings/settings-dialog.tsx +++ b/apps/web/src/components/settings/settings-dialog.tsx @@ -751,6 +751,25 @@ function SecuritySection() { /* ────────────────────── People ────────────────────── */ +function generatePassword(): string { + const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const lower = "abcdefghijklmnopqrstuvwxyz"; + const digits = "0123456789"; + const all = upper + lower + digits; + const required = [ + upper[Math.floor(Math.random() * upper.length)], + lower[Math.floor(Math.random() * lower.length)], + digits[Math.floor(Math.random() * digits.length)], + ]; + const rest = Array.from({ length: 13 }, () => all[Math.floor(Math.random() * all.length)]); + const chars = [...required, ...rest]; + for (let i = chars.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [chars[i], chars[j]] = [chars[j], chars[i]]; + } + return chars.join(""); +} + function PeopleSection() { const { t } = useTranslation(); const [users, setUsers] = useState([]); @@ -764,6 +783,8 @@ function PeopleSection() { const [newTeam, setNewTeam] = useState("Default"); const [addError, setAddError] = useState(null); const [adding, setAdding] = useState(false); + const [showGeneratedPw, setShowGeneratedPw] = useState(false); + const [pwCopied, setPwCopied] = useState(false); const [openMenuId, setOpenMenuId] = useState(null); const [editingUser, setEditingUser] = useState(null); const [editRole, setEditRole] = useState(""); @@ -836,6 +857,8 @@ function PeopleSection() { setNewRole("user"); setNewTeam("Default"); setShowAddForm(false); + setShowGeneratedPw(false); + setPwCopied(false); setActionMsg({ type: "success", text: t.settings.people.createSuccess }); await loadUsers(); } catch (err) { @@ -1009,15 +1032,49 @@ function PeopleSection() { required className="px-3 py-2 rounded-lg border border-border bg-background text-sm text-foreground" /> - setNewPassword(e.target.value)} - placeholder={t.auth.password} - required - minLength={8} - className="px-3 py-2 rounded-lg border border-border bg-background text-sm text-foreground" - /> +
+ { + setNewPassword(e.target.value); + setShowGeneratedPw(false); + setPwCopied(false); + }} + placeholder={t.auth.password} + required + minLength={8} + className={cn( + "flex-1 min-w-0 px-3 py-2 rounded-lg border border-border bg-background text-sm text-foreground", + showGeneratedPw && "font-mono", + )} + /> + {showGeneratedPw && ( + + )} +