From e292df82f01f485757a84f4beb18c9d8006d6bf3 Mon Sep 17 00:00:00 2001 From: byGalax Date: Fri, 15 May 2026 23:00:18 +0200 Subject: [PATCH] feat(desktop): UserKeyUnlock screen (PIN entry + recovery fallback) --- apps/desktop/src/components/UserKeyUnlock.tsx | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 apps/desktop/src/components/UserKeyUnlock.tsx diff --git a/apps/desktop/src/components/UserKeyUnlock.tsx b/apps/desktop/src/components/UserKeyUnlock.tsx new file mode 100644 index 0000000..24905d6 --- /dev/null +++ b/apps/desktop/src/components/UserKeyUnlock.tsx @@ -0,0 +1,86 @@ +import { useCallback, useState } from 'react'; + +import { loadOrUnlockUserKey } from '../lib/userIdentity'; +import { PinInput } from './PinInput'; +import { LockIcon, SpinnerIcon } from './icons'; + +interface Props { + userId: string; + hasRecovery: boolean; + lockedUntil?: string | null; + onUnlocked: () => void; +} + +type Mode = 'pin' | 'recovery'; + +export function UserKeyUnlock({ userId, hasRecovery, lockedUntil, onUnlocked }: Props) { + const [mode, setMode] = useState('pin'); + const [value, setValue] = useState(''); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + + const submit = useCallback(async () => { + setBusy(true); setError(null); + try { + const res = await loadOrUnlockUserKey({ + userId, pin: value, isRecoveryCode: mode === 'recovery', + }); + if (res.kind === 'unlocked') onUnlocked(); + else if (res.kind === 'locked') setError('Konto gesperrt bis ' + res.lockedUntil); + else setError('Schlüssel auf dem Server nicht gefunden.'); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : String(err)); + } finally { setBusy(false); } + }, [userId, value, mode, onUnlocked]); + + return ( +
{ e.preventDefault(); void submit(); }} + className="space-y-4 rounded-2xl border border-white/10 bg-ink-900/70 p-6 text-neutral-100" + > +
+ +

Account entsperren

+
+ {lockedUntil && ( +

+ Zu viele Fehlversuche. Gesperrt bis {lockedUntil}. +

+ )} + {mode === 'pin' ? ( +
+ + void submit()} /> +
+ ) : ( +
+ + setValue(e.target.value)} + spellCheck={false} + className="w-full rounded-lg border border-white/10 bg-ink-800 px-3 py-2 font-mono tracking-widest text-white" + /> +
+ )} + {hasRecovery && ( + + )} + {error &&

{error}

} + +
+ ); +}