feat(desktop): UserKeySetup screen (PIN + optional recovery code)
This commit is contained in:
@@ -0,0 +1,93 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { setupNewUserIdentity } from '../lib/userIdentity';
|
||||||
|
import { PinInput } from './PinInput';
|
||||||
|
import { ShieldIcon, SpinnerIcon } from './icons';
|
||||||
|
|
||||||
|
interface Props { userId: string; onComplete: () => void }
|
||||||
|
type Step = 'enter' | 'recovery';
|
||||||
|
|
||||||
|
export function UserKeySetup({ userId, onComplete }: Props) {
|
||||||
|
const [pin, setPin] = useState('');
|
||||||
|
const [confirm, setConfirm] = useState('');
|
||||||
|
const [withRecovery, setWithRecovery] = useState(true);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [step, setStep] = useState<Step>('enter');
|
||||||
|
const [recoveryCode, setRecoveryCode] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const submit = useCallback(async () => {
|
||||||
|
if (pin.length !== 6 || confirm.length !== 6) {
|
||||||
|
setError('PIN muss 6 Ziffern haben.'); return;
|
||||||
|
}
|
||||||
|
if (pin !== confirm) {
|
||||||
|
setError('PINs stimmen nicht überein.'); return;
|
||||||
|
}
|
||||||
|
setBusy(true); setError(null);
|
||||||
|
try {
|
||||||
|
const r = await setupNewUserIdentity({ userId, pin, withRecovery });
|
||||||
|
setRecoveryCode(r.recoveryCode);
|
||||||
|
if (withRecovery) setStep('recovery');
|
||||||
|
else onComplete();
|
||||||
|
} catch (err: unknown) {
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
} finally { setBusy(false); }
|
||||||
|
}, [pin, confirm, withRecovery, userId, onComplete]);
|
||||||
|
|
||||||
|
if (step === 'recovery') {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 rounded-2xl border border-white/10 bg-ink-900/70 p-6 text-neutral-100">
|
||||||
|
<div className="flex items-center gap-2 text-amber-300">
|
||||||
|
<ShieldIcon className="h-5 w-5" />
|
||||||
|
<h2 className="text-lg font-semibold">Recovery-Code</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-neutral-300">
|
||||||
|
Speichere diesen Code an einem sicheren Ort (Passwortmanager, ausgedruckt).
|
||||||
|
Mit ihm kannst du deinen Account auch ohne PIN entsperren.
|
||||||
|
</p>
|
||||||
|
<div className="select-all rounded-lg bg-ink-800 p-4 text-center font-mono text-lg tracking-widest">
|
||||||
|
{recoveryCode}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button type="button"
|
||||||
|
className="flex-1 rounded-lg border border-white/10 bg-ink-800 px-3 py-2 text-sm text-neutral-200 hover:bg-white/5"
|
||||||
|
onClick={onComplete}
|
||||||
|
>Überspringen</button>
|
||||||
|
<button type="button"
|
||||||
|
className="flex-1 rounded-lg bg-brand-500 px-3 py-2 text-sm font-semibold text-white hover:bg-brand-400"
|
||||||
|
onClick={onComplete}
|
||||||
|
>Habe ich gespeichert</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => { e.preventDefault(); void submit(); }}
|
||||||
|
className="space-y-4 rounded-2xl border border-white/10 bg-ink-900/70 p-6 text-neutral-100"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-xs font-medium uppercase tracking-wide text-neutral-400">PIN eingeben</label>
|
||||||
|
<PinInput value={pin} onChange={setPin} ariaLabel="PIN eingeben" autoFocus />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-xs font-medium uppercase tracking-wide text-neutral-400">PIN bestätigen</label>
|
||||||
|
<PinInput value={confirm} onChange={setConfirm} ariaLabel="PIN bestätigen" />
|
||||||
|
</div>
|
||||||
|
<label className="flex items-center gap-2 text-xs text-neutral-300">
|
||||||
|
<input type="checkbox" checked={withRecovery} onChange={(e) => setWithRecovery(e.target.checked)} />
|
||||||
|
Recovery-Code erstellen (empfohlen)
|
||||||
|
</label>
|
||||||
|
{error && <p role="alert" className="text-sm text-rose-300">{error}</p>}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={busy}
|
||||||
|
className="inline-flex w-full items-center justify-center gap-2 rounded-lg bg-brand-500 px-4 py-3 text-sm font-semibold text-white hover:bg-brand-400 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
{busy && <SpinnerIcon className="h-4 w-4" />}
|
||||||
|
Weiter
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user