feat: enhance authentication and settings UI

This commit is contained in:
shuaiplus
2026-02-28 03:07:39 +08:00
committed by Shuai
parent 0cf8028087
commit 651eb69bd6
15 changed files with 674 additions and 166 deletions
+45 -20
View File
@@ -1,9 +1,11 @@
import { useMemo, useState } from 'preact/hooks';
import { useEffect, useMemo, useState } from 'preact/hooks';
import { Clipboard, RefreshCw, ShieldCheck, ShieldOff } from 'lucide-preact';
import qrcode from 'qrcode-generator';
import type { Profile } from '@/lib/types';
interface SettingsPageProps {
profile: Profile;
totpEnabled: boolean;
onSaveProfile: (name: string, email: string) => Promise<void>;
onChangePassword: (currentPassword: string, nextPassword: string, nextPassword2: string) => Promise<void>;
onEnableTotp: (secret: string, token: string) => Promise<void>;
@@ -24,13 +26,23 @@ function buildOtpUri(email: string, secret: string): string {
}
export default function SettingsPage(props: SettingsPageProps) {
const totpSecretStorageKey = `nodewarden.totp.secret.${props.profile.id}`;
const [name, setName] = useState(props.profile.name || '');
const [email, setEmail] = useState(props.profile.email || '');
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [newPassword2, setNewPassword2] = useState('');
const [secret, setSecret] = useState(randomBase32Secret(32));
const [secret, setSecret] = useState(() => localStorage.getItem(totpSecretStorageKey) || randomBase32Secret(32));
const [token, setToken] = useState('');
const [totpLocked, setTotpLocked] = useState(props.totpEnabled);
useEffect(() => {
if (!props.totpEnabled) {
setTotpLocked(false);
return;
}
setTotpLocked(true);
}, [props.totpEnabled]);
const qrSvg = useMemo(() => {
const qr = qrcode(0, 'M');
@@ -39,6 +51,12 @@ export default function SettingsPage(props: SettingsPageProps) {
return qr.createSvgTag({ scalable: true, margin: 0 });
}, [email, props.profile.email, secret]);
async function enableTotp(): Promise<void> {
await props.onEnableTotp(secret, token);
localStorage.setItem(totpSecretStorageKey, secret);
setTotpLocked(true);
}
return (
<div className="stack">
<section className="card">
@@ -95,31 +113,38 @@ export default function SettingsPage(props: SettingsPageProps) {
<section className="card">
<h3>TOTP</h3>
{totpLocked && <div className="status-ok">TOTP is enabled for this account.</div>}
<div className="totp-grid">
<div className="totp-qr" dangerouslySetInnerHTML={{ __html: qrSvg }} />
<div>
<label className="field">
<span>Authenticator Key</span>
<input className="input" value={secret} onInput={(e) => setSecret((e.currentTarget as HTMLInputElement).value.toUpperCase())} />
</label>
<label className="field">
<span>Verification Code</span>
<input className="input" value={token} onInput={(e) => setToken((e.currentTarget as HTMLInputElement).value)} />
</label>
<div className="actions">
<button type="button" className="btn btn-primary" onClick={() => void props.onEnableTotp(secret, token)}>
Enable TOTP
</button>
<button type="button" className="btn btn-secondary" onClick={() => setSecret(randomBase32Secret(32))}>
Regenerate
</button>
<button type="button" className="btn btn-secondary" onClick={() => navigator.clipboard.writeText(secret)}>
Copy Secret
</button>
<div>
<label className="field">
<span>Authenticator Key</span>
<input className="input" value={secret} disabled={totpLocked} onInput={(e) => setSecret((e.currentTarget as HTMLInputElement).value.toUpperCase())} />
</label>
<label className="field">
<span>Verification Code</span>
<input className="input" value={token} disabled={totpLocked} onInput={(e) => setToken((e.currentTarget as HTMLInputElement).value)} />
</label>
<div className="actions">
<button type="button" className="btn btn-primary" disabled={totpLocked} onClick={() => void enableTotp()}>
<ShieldCheck size={14} className="btn-icon" />
{totpLocked ? 'Enabled' : 'Enable TOTP'}
</button>
<button type="button" className="btn btn-secondary" disabled={totpLocked} onClick={() => setSecret(randomBase32Secret(32))}>
<RefreshCw size={14} className="btn-icon" />
Regenerate
</button>
<button type="button" className="btn btn-secondary" disabled={totpLocked} onClick={() => navigator.clipboard.writeText(secret)}>
<Clipboard size={14} className="btn-icon" />
Copy Secret
</button>
</div>
</div>
</div>
</div>
<button type="button" className="btn btn-danger" onClick={props.onOpenDisableTotp}>
<ShieldOff size={14} className="btn-icon" />
Disable TOTP
</button>
</section>