import { useEffect, useMemo, useState } from 'preact/hooks'; import { Clipboard, KeyRound, RefreshCw, ShieldCheck, ShieldOff } from 'lucide-preact'; import qrcode from 'qrcode-generator'; import type { Profile } from '@/lib/types'; import { t } from '@/lib/i18n'; interface SettingsPageProps { profile: Profile; totpEnabled: boolean; onChangePassword: (currentPassword: string, nextPassword: string, nextPassword2: string) => Promise; onEnableTotp: (secret: string, token: string) => Promise; onOpenDisableTotp: () => void; onGetRecoveryCode: (masterPassword: string) => Promise; onNotify?: (type: 'success' | 'error', text: string) => void; } function randomBase32Secret(length: number): string { const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; const random = crypto.getRandomValues(new Uint8Array(length)); let out = ''; for (const x of random) out += alphabet[x % alphabet.length]; return out; } function buildOtpUri(email: string, secret: string): string { const issuer = 'NodeWarden'; return `otpauth://totp/${encodeURIComponent(`${issuer}:${email}`)}?secret=${encodeURIComponent(secret)}&issuer=${encodeURIComponent(issuer)}&algorithm=SHA1&digits=6&period=30`; } export default function SettingsPage(props: SettingsPageProps) { const totpSecretStorageKey = `nodewarden.totp.secret.${props.profile.id}`; const [currentPassword, setCurrentPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); const [newPassword2, setNewPassword2] = useState(''); const [secret, setSecret] = useState(() => localStorage.getItem(totpSecretStorageKey) || randomBase32Secret(32)); const [token, setToken] = useState(''); const [totpLocked, setTotpLocked] = useState(props.totpEnabled); const [recoveryMasterPassword, setRecoveryMasterPassword] = useState(''); const [recoveryCode, setRecoveryCode] = useState(''); useEffect(() => { if (!props.totpEnabled) { setTotpLocked(false); return; } setTotpLocked(true); }, [props.totpEnabled]); const qrDataUrl = useMemo(() => { const qr = qrcode(0, 'M'); qr.addData(buildOtpUri(props.profile.email, secret)); qr.make(); const svg = qr.createSvgTag({ scalable: true, margin: 0 }); return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; }, [props.profile.email, secret]); async function enableTotp(): Promise { await props.onEnableTotp(secret, token); // Secret is now stored on the server; remove plaintext copy from localStorage. localStorage.removeItem(totpSecretStorageKey); setTotpLocked(true); } async function loadRecoveryCode(): Promise { const code = await props.onGetRecoveryCode(recoveryMasterPassword); setRecoveryCode(code); props.onNotify?.('success', t('txt_recovery_code_loaded')); } return (

{t('txt_change_master_password')}

{t('txt_totp')}

{totpLocked &&
{t('txt_totp_is_enabled_for_this_account')}
}
TOTP QR

{t('txt_recovery_code')}

{t('txt_this_is_a_one_time_code_after_it_is_used_a_new_code_is_generated_automatically')}

{recoveryCode && (
{recoveryCode}
)}
); }