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
This commit is contained in:
SnapOtter
2026-05-16 23:08:19 +08:00
parent e316dff294
commit 148118d969
23 changed files with 171 additions and 11 deletions
@@ -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<UserEntry[]>([]);
@@ -764,6 +783,8 @@ function PeopleSection() {
const [newTeam, setNewTeam] = useState("Default");
const [addError, setAddError] = useState<string | null>(null);
const [adding, setAdding] = useState(false);
const [showGeneratedPw, setShowGeneratedPw] = useState(false);
const [pwCopied, setPwCopied] = useState(false);
const [openMenuId, setOpenMenuId] = useState<string | null>(null);
const [editingUser, setEditingUser] = useState<UserEntry | null>(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"
/>
<input
type="password"
value={newPassword}
onChange={(e) => 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"
/>
<div className="flex items-center gap-1.5">
<input
type={showGeneratedPw ? "text" : "password"}
value={newPassword}
onChange={(e) => {
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 && (
<button
type="button"
onClick={async () => {
const ok = await copyToClipboard(newPassword);
if (ok) {
setPwCopied(true);
setTimeout(() => setPwCopied(false), 2000);
}
}}
className={cn(
"shrink-0 p-2 rounded-lg border border-border transition-colors",
pwCopied
? "text-green-500 bg-green-500/10"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
title={
pwCopied
? t.settings.people.passwordCopied
: t.settings.people.copyPasswordButton
}
>
{pwCopied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</button>
)}
</div>
<select
value={newRole}
onChange={(e) => setNewRole(e.target.value)}
@@ -1062,12 +1119,36 @@ function PeopleSection() {
</button>
<button
type="button"
onClick={() => setShowAddForm(false)}
onClick={() => {
const pw = generatePassword();
setNewPassword(pw);
setShowGeneratedPw(true);
setPwCopied(false);
}}
className="flex items-center gap-1.5 px-3 py-2 rounded-lg border border-primary/30 bg-primary/10 text-xs text-primary hover:bg-primary/20 font-medium transition-colors"
>
<Sparkles className="h-3 w-3" />
{t.changePassword.generateButton}
</button>
<div className="flex-1" />
<button
type="button"
onClick={() => {
setShowAddForm(false);
setShowGeneratedPw(false);
setPwCopied(false);
}}
className="px-4 py-2 rounded-lg border border-border text-sm text-muted-foreground hover:bg-muted transition-colors"
>
{t.common.cancel}
</button>
</div>
{showGeneratedPw && !pwCopied && (
<p className="text-xs text-amber-500 flex items-center gap-1.5">
<Key className="h-3.5 w-3.5 shrink-0" />
{t.settings.people.copyPasswordWarning}
</p>
)}
{addError && <p className="text-sm text-destructive">{addError}</p>}
</form>
)}
+3 -1
View File
@@ -1,3 +1,4 @@
import { Sparkles } from "lucide-react";
import { type FormEvent, useState } from "react";
import { useTranslation } from "@/contexts/i18n-context";
import { formatHeaders } from "@/lib/api";
@@ -164,8 +165,9 @@ export function ChangePasswordPage() {
<button
type="button"
onClick={handleGenerate}
className="text-xs text-primary hover:text-primary/80 font-medium"
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg border border-primary/30 bg-primary/10 text-xs text-primary hover:bg-primary/20 font-medium transition-colors"
>
<Sparkles className="h-3 w-3" />
{t.changePassword.generateButton}
</button>
</div>
+3
View File
@@ -1499,6 +1499,9 @@ export const ar: TranslationKeys = {
resetPasswordAction: "إعادة تعيين كلمة المرور",
deleteUserAction: "حذف المستخدم",
saveButton: "حفظ",
copyPasswordButton: "نسخ كلمة المرور",
passwordCopied: "تم النسخ!",
copyPasswordWarning: "انسخ كلمة المرور الآن. لن تتمكن من رؤيتها بعد إنشاء المستخدم.",
},
teams: {
heading: "الفرق",
+4
View File
@@ -1516,6 +1516,10 @@ export const de: TranslationKeys = {
resetPasswordAction: "Passwort zuruecksetzen",
deleteUserAction: "Benutzer loeschen",
saveButton: "Speichern",
copyPasswordButton: "Passwort kopieren",
passwordCopied: "Kopiert!",
copyPasswordWarning:
"Kopieren Sie dieses Passwort jetzt. Nach dem Erstellen des Benutzers ist es nicht mehr sichtbar.",
},
teams: {
heading: "Teams",
+4
View File
@@ -1457,6 +1457,10 @@ export const en = {
resetPasswordAction: "Reset Password",
deleteUserAction: "Delete User",
saveButton: "Save",
copyPasswordButton: "Copy password",
passwordCopied: "Copied!",
copyPasswordWarning:
"Copy this password now. You won't be able to see it after creating the user.",
},
teams: {
heading: "Teams",
+4
View File
@@ -1497,6 +1497,10 @@ export const es: TranslationKeys = {
resetPasswordAction: "Restablecer contrasena",
deleteUserAction: "Eliminar usuario",
saveButton: "Guardar",
copyPasswordButton: "Copiar contrasena",
passwordCopied: "Copiado!",
copyPasswordWarning:
"Copia esta contrasena ahora. No podras verla despues de crear el usuario.",
},
teams: {
heading: "Equipos",
+4
View File
@@ -1516,6 +1516,10 @@ export const fr: TranslationKeys = {
resetPasswordAction: "Reinitialiser le mot de passe",
deleteUserAction: "Supprimer l'utilisateur",
saveButton: "Enregistrer",
copyPasswordButton: "Copier le mot de passe",
passwordCopied: "Copie !",
copyPasswordWarning:
"Copiez ce mot de passe maintenant. Vous ne pourrez plus le voir apres la creation de l'utilisateur.",
},
teams: {
heading: "Equipes",
+3
View File
@@ -1495,6 +1495,9 @@ export const hi: TranslationKeys = {
resetPasswordAction: "पासवर्ड रीसेट करें",
deleteUserAction: "उपयोगकर्ता हटाएं",
saveButton: "सहेजें",
copyPasswordButton: "पासवर्ड कॉपी करें",
passwordCopied: "कॉपी हो गया!",
copyPasswordWarning: "इस पासवर्ड को अभी कॉपी करें। उपयोगकर्ता बनाने के बाद आप इसे नहीं देख पाएंगे।",
},
teams: {
heading: "टीमें",
+4
View File
@@ -1507,6 +1507,10 @@ export const id: TranslationKeys = {
resetPasswordAction: "Reset Kata Sandi",
deleteUserAction: "Hapus Pengguna",
saveButton: "Simpan",
copyPasswordButton: "Salin kata sandi",
passwordCopied: "Disalin!",
copyPasswordWarning:
"Salin kata sandi ini sekarang. Anda tidak akan bisa melihatnya setelah membuat pengguna.",
},
teams: {
heading: "Tim",
+4
View File
@@ -1510,6 +1510,10 @@ export const it: TranslationKeys = {
resetPasswordAction: "Reimposta password",
deleteUserAction: "Elimina utente",
saveButton: "Salva",
copyPasswordButton: "Copia password",
passwordCopied: "Copiato!",
copyPasswordWarning:
"Copia questa password adesso. Non potrai vederla dopo aver creato l'utente.",
},
teams: {
heading: "Team",
+4
View File
@@ -1465,6 +1465,10 @@ export const ja: TranslationKeys = {
resetPasswordAction: "パスワードリセット",
deleteUserAction: "ユーザー削除",
saveButton: "保存",
copyPasswordButton: "パスワードをコピー",
passwordCopied: "コピーしました!",
copyPasswordWarning:
"今すぐこのパスワードをコピーしてください。ユーザー作成後は表示できません。",
},
teams: {
heading: "チーム",
+3
View File
@@ -1450,6 +1450,9 @@ export const ko: TranslationKeys = {
resetPasswordAction: "비밀번호 재설정",
deleteUserAction: "사용자 삭제",
saveButton: "저장",
copyPasswordButton: "비밀번호 복사",
passwordCopied: "복사됨!",
copyPasswordWarning: "지금 이 비밀번호를 복사하세요. 사용자 생성 후에는 확인할 수 없습니다.",
},
teams: {
heading: "팀",
+4
View File
@@ -1510,6 +1510,10 @@ export const nl: TranslationKeys = {
resetPasswordAction: "Wachtwoord resetten",
deleteUserAction: "Gebruiker verwijderen",
saveButton: "Opslaan",
copyPasswordButton: "Wachtwoord kopieren",
passwordCopied: "Gekopieerd!",
copyPasswordWarning:
"Kopieer dit wachtwoord nu. U kunt het niet meer zien na het aanmaken van de gebruiker.",
},
teams: {
heading: "Teams",
+4
View File
@@ -1514,6 +1514,10 @@ export const pl: TranslationKeys = {
resetPasswordAction: "Resetuj hasło",
deleteUserAction: "Usuń użytkownika",
saveButton: "Zapisz",
copyPasswordButton: "Kopiuj haslo",
passwordCopied: "Skopiowano!",
copyPasswordWarning:
"Skopiuj to haslo teraz. Nie bedziesz mogl go zobaczyc po utworzeniu uzytkownika.",
},
teams: {
heading: "Zespoły",
+3
View File
@@ -1509,6 +1509,9 @@ export const ptBR: TranslationKeys = {
resetPasswordAction: "Redefinir senha",
deleteUserAction: "Excluir usuario",
saveButton: "Salvar",
copyPasswordButton: "Copiar senha",
passwordCopied: "Copiado!",
copyPasswordWarning: "Copie esta senha agora. Voce nao podera ve-la apos criar o usuario.",
},
teams: {
heading: "Equipes",
+4
View File
@@ -1508,6 +1508,10 @@ export const ru: TranslationKeys = {
resetPasswordAction: "Сбросить пароль",
deleteUserAction: "Удалить пользователя",
saveButton: "Сохранить",
copyPasswordButton: "Скопировать пароль",
passwordCopied: "Скопировано!",
copyPasswordWarning:
"Скопируйте этот пароль сейчас. После создания пользователя вы не сможете его увидеть.",
},
teams: {
heading: "Команды",
+4
View File
@@ -1506,6 +1506,10 @@ export const sv: TranslationKeys = {
resetPasswordAction: "Aterstall losenord",
deleteUserAction: "Radera anvandare",
saveButton: "Spara",
copyPasswordButton: "Kopiera losenord",
passwordCopied: "Kopierat!",
copyPasswordWarning:
"Kopiera detta losenord nu. Du kan inte se det efter att anvandaren har skapats.",
},
teams: {
heading: "Team",
+3
View File
@@ -1487,6 +1487,9 @@ export const th: TranslationKeys = {
resetPasswordAction: "รีเซ็ตรหัสผ่าน",
deleteUserAction: "ลบผู้ใช้",
saveButton: "บันทึก",
copyPasswordButton: "คัดลอกรหัสผ่าน",
passwordCopied: "คัดลอกแล้ว!",
copyPasswordWarning: "คัดลอกรหัสผ่านนี้ตอนนี้ คุณจะไม่สามารถดูได้หลังจากสร้างผู้ใช้",
},
teams: {
heading: "ทีม",
+4
View File
@@ -1512,6 +1512,10 @@ export const tr: TranslationKeys = {
resetPasswordAction: "Parolayı Sıfırla",
deleteUserAction: "Kullanıcıyı Sil",
saveButton: "Kaydet",
copyPasswordButton: "Sifreyi kopyala",
passwordCopied: "Kopyalandi!",
copyPasswordWarning:
"Bu sifreyi simdi kopyalayin. Kullaniciyi olusturduktan sonra goeremezsiniz.",
},
teams: {
heading: "Takımlar",
+4
View File
@@ -1508,6 +1508,10 @@ export const uk: TranslationKeys = {
resetPasswordAction: "Скинути пароль",
deleteUserAction: "Видалити користувача",
saveButton: "Зберегти",
copyPasswordButton: "Скопіювати пароль",
passwordCopied: "Скопійовано!",
copyPasswordWarning:
"Скопіюйте цей пароль зараз. Після створення користувача ви не зможете його побачити.",
},
teams: {
heading: "Команди",
+4
View File
@@ -1508,6 +1508,10 @@ export const vi: TranslationKeys = {
resetPasswordAction: "Đặt lại mật khẩu",
deleteUserAction: "Xóa người dùng",
saveButton: "Lưu",
copyPasswordButton: "Sao chep mat khau",
passwordCopied: "Da sao chep!",
copyPasswordWarning:
"Sao chep mat khau nay ngay bay gio. Ban se khong the xem lai sau khi tao nguoi dung.",
},
teams: {
heading: "Nhóm",
+3
View File
@@ -1439,6 +1439,9 @@ export const zhCN: TranslationKeys = {
resetPasswordAction: "重置密码",
deleteUserAction: "删除用户",
saveButton: "保存",
copyPasswordButton: "复制密码",
passwordCopied: "已复制!",
copyPasswordWarning: "请立即复制此密码。创建用户后将无法再次查看。",
},
teams: {
heading: "团队",
+3
View File
@@ -1437,6 +1437,9 @@ export const zhTW: TranslationKeys = {
resetPasswordAction: "重設密碼",
deleteUserAction: "刪除使用者",
saveButton: "儲存",
copyPasswordButton: "複製密碼",
passwordCopied: "已複製!",
copyPasswordWarning: "請立即複製此密碼。建立使用者後將無法再次查看。",
},
teams: {
heading: "團隊",