feat: add passkey-first login and management flow

This commit is contained in:
Shuai
2026-03-31 00:59:50 +08:00
parent 1184cb8d9a
commit 0f6da7d147
16 changed files with 799 additions and 6 deletions
+32
View File
@@ -14,6 +14,10 @@ interface SettingsPageProps {
onOpenDisableTotp: () => void;
onGetRecoveryCode: (masterPassword: string) => Promise<string>;
onNotify?: (type: 'success' | 'error', text: string) => void;
passkeys: Array<{ id: string; name: string; creationDate: string; lastUsedDate: string | null }>;
onCreatePasskey: (name: string) => Promise<void>;
onRenamePasskey: (id: string, name: string) => Promise<void>;
onDeletePasskey: (id: string) => Promise<void>;
}
function randomBase32Secret(length: number): string {
@@ -47,6 +51,7 @@ export default function SettingsPage(props: SettingsPageProps) {
const [totpLocked, setTotpLocked] = useState(props.totpEnabled);
const [recoveryMasterPassword, setRecoveryMasterPassword] = useState('');
const [recoveryCode, setRecoveryCode] = useState('');
const [passkeyName, setPasskeyName] = useState('');
useEffect(() => {
if (!props.totpEnabled) {
@@ -140,6 +145,33 @@ export default function SettingsPage(props: SettingsPageProps) {
</button>
</section>
<section className="card">
<h3>Passkey</h3>
<div className="field-grid">
<label className="field">
<span></span>
<input className="input" value={passkeyName} onInput={(e) => setPasskeyName((e.currentTarget as HTMLInputElement).value)} placeholder="例如:MacBook Touch ID" />
</label>
<div className="field" style={{ alignSelf: 'end' }}>
<button type="button" className="btn btn-primary" disabled={!passkeyName.trim()} onClick={() => void props.onCreatePasskey(passkeyName.trim()).then(() => setPasskeyName(''))}>
Passkey
</button>
</div>
</div>
<p className="muted-inline" style={{ marginBottom: 8 }}> 5 </p>
<div className="stack">
{props.passkeys.map((item) => (
<div key={item.id} className="card" style={{ marginBottom: 0 }}>
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
<input className="input" style={{ flex: 1, minWidth: 180 }} value={item.name} onInput={(e) => void props.onRenamePasskey(item.id, (e.currentTarget as HTMLInputElement).value)} />
<button type="button" className="btn btn-danger" onClick={() => void props.onDeletePasskey(item.id)}></button>
</div>
</div>
))}
{!props.passkeys.length && <div className="empty"> Passkey</div>}
</div>
</section>
<section className="card">
<div className="settings-twofactor-grid">
<div className="settings-subcard">