import { useEffect, useMemo, useState } from 'preact/hooks'; import { Check, Copy, Download, LoaderCircle, Minus, Plus, RefreshCw, ShieldCheck } from 'lucide-preact'; import { copyTextToClipboard } from '@/lib/clipboard'; import { t } from '@/lib/i18n'; import { clampInteger, defaultGeneratorSettings, estimateStrength, generateValue, normalizeGeneratorSettings, type EmailMode, type EmailOptions, type GeneratorMode, type GeneratorSettings, type PassphraseOptions, type PasswordOptions, type PinOptions, type SshKeyOptions, type UsernameOptions, } from '@/lib/password-generator'; import { generateSshKey, type GeneratedSshKey } from '@/lib/ssh-key-generator'; const SETTINGS_KEY = 'nodewarden.passwordGenerator.settings.v2'; function readSettings(): GeneratorSettings { try { const current = localStorage.getItem(SETTINGS_KEY); if (current) return normalizeGeneratorSettings(JSON.parse(current)); // Preserve compatible options for users upgrading from the original generator. const legacy = JSON.parse(localStorage.getItem('nodewarden.passwordGenerator.settings.v1') || '{}'); return normalizeGeneratorSettings(legacy); } catch { return defaultGeneratorSettings; } } export default function PasswordGeneratorPage() { const initial = useMemo(readSettings, []); const [settings, setSettings] = useState(initial); const [seed, setSeed] = useState(0); const [copied, setCopied] = useState(false); const [sshKey, setSshKey] = useState(null); const [sshKeyError, setSshKeyError] = useState(''); const [sshKeyLoading, setSshKeyLoading] = useState(false); const generated = useMemo(() => settings.mode === 'sshKey' ? sshKey?.fingerprint || '' : generateValue(settings), [settings, seed, sshKey]); const strength = useMemo( () => estimateStrength(settings.mode, generated, settings.mode === 'passphrase' ? settings.passphrase.words : undefined), [generated, settings.mode, settings.passphrase.words], ); const strengthLabel = strength ? t(['txt_password_strength_weak', 'txt_password_strength_fair', 'txt_password_strength_good', 'txt_password_strength_strong'][strength - 1]) : ''; useEffect(() => { try { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); } catch { // The generator remains fully usable when browser storage is unavailable. } }, [settings]); useEffect(() => { if (settings.mode !== 'sshKey') return; let cancelled = false; setSshKeyLoading(true); setSshKeyError(''); void generateSshKey({ ...settings.sshKey, comment: '' }) .then((value) => { if (!cancelled) setSshKey(value); }) .catch(() => { if (!cancelled) { setSshKey(null); setSshKeyError(t('txt_generator_ssh_error')); } }) .finally(() => { if (!cancelled) setSshKeyLoading(false); }); return () => { cancelled = true; }; }, [settings.mode, settings.sshKey.type, settings.sshKey.rsaLength, seed]); const regenerate = () => { setCopied(false); setSeed((value) => value + 1); }; const copy = async () => { const value = settings.mode === 'sshKey' && sshKey ? publicKeyWithComment(sshKey.publicKey, settings.sshKey.comment) : generated; await copyTextToClipboard(value, { onSuccess: () => setCopied(true), onError: () => setCopied(false) }); window.setTimeout(() => setCopied(false), 1600); }; const changeMode = (mode: GeneratorMode) => { setSettings((current) => ({ ...current, mode })); setCopied(false); }; const changePasswordOption = (key: K, value: PasswordOptions[K]) => { setSettings((current) => ({ ...current, password: { ...current.password, [key]: value } })); setCopied(false); }; const changeCharacterType = (key: 'uppercase' | 'lowercase' | 'numbers' | 'special', checked: boolean) => { const enabled = ['uppercase', 'lowercase', 'numbers', 'special'].filter((item) => settings.password[item as 'uppercase']); if (!checked && enabled.length === 1 && enabled[0] === key) return; changePasswordOption(key, checked); }; const changePassphraseOption = (key: K, value: PassphraseOptions[K]) => { setSettings((current) => ({ ...current, passphrase: { ...current.passphrase, [key]: value } })); setCopied(false); }; const changePinOption = (key: K, value: PinOptions[K]) => { setSettings((current) => ({ ...current, pin: { ...current.pin, [key]: value } })); setCopied(false); }; const changeUsernameOption = (key: K, value: UsernameOptions[K]) => { setSettings((current) => ({ ...current, username: { ...current.username, [key]: value } })); setCopied(false); }; const changeEmailOption = (key: K, value: EmailOptions[K]) => { setSettings((current) => ({ ...current, email: { ...current.email, [key]: value } })); setCopied(false); }; const changeSshKeyOption = (key: K, value: SshKeyOptions[K]) => { setSettings((current) => ({ ...current, sshKey: { ...current.sshKey, [key]: value } })); setCopied(false); }; return (
{([ ['password', 'txt_password'], ['passphrase', 'txt_passphrase'], ['pin', 'txt_generator_pin'], ['username', 'txt_generator_username'], ['email', 'txt_generator_email_alias'], ['sshKey', 'txt_generator_ssh_key'], ] as const).map(([mode, label]) => ( ))}
{settings.mode === 'sshKey' ? ( ) : {generated || t('txt_generator_email_required_hint')}} {settings.mode !== 'sshKey' &&
{strength > 0 ? ( <>
{[1, 2, 3, 4].map((level) => )}
{strengthLabel} ) : } {t('txt_generator_character_count', { count: generated.length })}
}

{t(settings.mode === 'sshKey' ? 'txt_generator_ssh_security_note' : 'txt_generator_security_note')}

{t('txt_options')}

{settings.mode === 'password' && ( )} {settings.mode === 'passphrase' && ( <> changePassphraseOption('words', value)} /> {settings.passphrase.wordList === 'custom' &&