mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1499,6 +1499,9 @@ export const ar: TranslationKeys = {
|
||||
resetPasswordAction: "إعادة تعيين كلمة المرور",
|
||||
deleteUserAction: "حذف المستخدم",
|
||||
saveButton: "حفظ",
|
||||
copyPasswordButton: "نسخ كلمة المرور",
|
||||
passwordCopied: "تم النسخ!",
|
||||
copyPasswordWarning: "انسخ كلمة المرور الآن. لن تتمكن من رؤيتها بعد إنشاء المستخدم.",
|
||||
},
|
||||
teams: {
|
||||
heading: "الفرق",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1495,6 +1495,9 @@ export const hi: TranslationKeys = {
|
||||
resetPasswordAction: "पासवर्ड रीसेट करें",
|
||||
deleteUserAction: "उपयोगकर्ता हटाएं",
|
||||
saveButton: "सहेजें",
|
||||
copyPasswordButton: "पासवर्ड कॉपी करें",
|
||||
passwordCopied: "कॉपी हो गया!",
|
||||
copyPasswordWarning: "इस पासवर्ड को अभी कॉपी करें। उपयोगकर्ता बनाने के बाद आप इसे नहीं देख पाएंगे।",
|
||||
},
|
||||
teams: {
|
||||
heading: "टीमें",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1465,6 +1465,10 @@ export const ja: TranslationKeys = {
|
||||
resetPasswordAction: "パスワードリセット",
|
||||
deleteUserAction: "ユーザー削除",
|
||||
saveButton: "保存",
|
||||
copyPasswordButton: "パスワードをコピー",
|
||||
passwordCopied: "コピーしました!",
|
||||
copyPasswordWarning:
|
||||
"今すぐこのパスワードをコピーしてください。ユーザー作成後は表示できません。",
|
||||
},
|
||||
teams: {
|
||||
heading: "チーム",
|
||||
|
||||
@@ -1450,6 +1450,9 @@ export const ko: TranslationKeys = {
|
||||
resetPasswordAction: "비밀번호 재설정",
|
||||
deleteUserAction: "사용자 삭제",
|
||||
saveButton: "저장",
|
||||
copyPasswordButton: "비밀번호 복사",
|
||||
passwordCopied: "복사됨!",
|
||||
copyPasswordWarning: "지금 이 비밀번호를 복사하세요. 사용자 생성 후에는 확인할 수 없습니다.",
|
||||
},
|
||||
teams: {
|
||||
heading: "팀",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1508,6 +1508,10 @@ export const ru: TranslationKeys = {
|
||||
resetPasswordAction: "Сбросить пароль",
|
||||
deleteUserAction: "Удалить пользователя",
|
||||
saveButton: "Сохранить",
|
||||
copyPasswordButton: "Скопировать пароль",
|
||||
passwordCopied: "Скопировано!",
|
||||
copyPasswordWarning:
|
||||
"Скопируйте этот пароль сейчас. После создания пользователя вы не сможете его увидеть.",
|
||||
},
|
||||
teams: {
|
||||
heading: "Команды",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1487,6 +1487,9 @@ export const th: TranslationKeys = {
|
||||
resetPasswordAction: "รีเซ็ตรหัสผ่าน",
|
||||
deleteUserAction: "ลบผู้ใช้",
|
||||
saveButton: "บันทึก",
|
||||
copyPasswordButton: "คัดลอกรหัสผ่าน",
|
||||
passwordCopied: "คัดลอกแล้ว!",
|
||||
copyPasswordWarning: "คัดลอกรหัสผ่านนี้ตอนนี้ คุณจะไม่สามารถดูได้หลังจากสร้างผู้ใช้",
|
||||
},
|
||||
teams: {
|
||||
heading: "ทีม",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1508,6 +1508,10 @@ export const uk: TranslationKeys = {
|
||||
resetPasswordAction: "Скинути пароль",
|
||||
deleteUserAction: "Видалити користувача",
|
||||
saveButton: "Зберегти",
|
||||
copyPasswordButton: "Скопіювати пароль",
|
||||
passwordCopied: "Скопійовано!",
|
||||
copyPasswordWarning:
|
||||
"Скопіюйте цей пароль зараз. Після створення користувача ви не зможете його побачити.",
|
||||
},
|
||||
teams: {
|
||||
heading: "Команди",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1439,6 +1439,9 @@ export const zhCN: TranslationKeys = {
|
||||
resetPasswordAction: "重置密码",
|
||||
deleteUserAction: "删除用户",
|
||||
saveButton: "保存",
|
||||
copyPasswordButton: "复制密码",
|
||||
passwordCopied: "已复制!",
|
||||
copyPasswordWarning: "请立即复制此密码。创建用户后将无法再次查看。",
|
||||
},
|
||||
teams: {
|
||||
heading: "团队",
|
||||
|
||||
@@ -1437,6 +1437,9 @@ export const zhTW: TranslationKeys = {
|
||||
resetPasswordAction: "重設密碼",
|
||||
deleteUserAction: "刪除使用者",
|
||||
saveButton: "儲存",
|
||||
copyPasswordButton: "複製密碼",
|
||||
passwordCopied: "已複製!",
|
||||
copyPasswordWarning: "請立即複製此密碼。建立使用者後將無法再次查看。",
|
||||
},
|
||||
teams: {
|
||||
heading: "團隊",
|
||||
|
||||
Reference in New Issue
Block a user