Files
ChatApp/apps/desktop/src/components/SecurityCenter.tsx
T
byGalax b789f4b10d feat(desktop): Settings security center (PIN change / recovery / reset)
Drops the manual backup-string flow; replaces it with PIN change,
recovery-code regeneration, and identity reset (all sealed via the new
user_keys table).
2026-05-15 23:09:05 +02:00

91 lines
3.7 KiB
TypeScript

import { useState } from 'react';
import { changePin, regenerateRecoveryCode, resetIdentity } from '../lib/userIdentity';
import { PinInput } from './PinInput';
import { ShieldIcon, SpinnerIcon } from './icons';
interface Props { userId: string }
export function SecurityCenter({ userId }: Props) {
const [pinOld, setPinOld] = useState('');
const [pinNew, setPinNew] = useState('');
const [busy, setBusy] = useState(false);
const [msg, setMsg] = useState<string | null>(null);
const [recovery, setRecovery] = useState<string | null>(null);
async function handleChangePin() {
setBusy(true); setMsg(null);
try {
await changePin({ userId, oldPin: pinOld, newPin: pinNew });
setMsg('PIN geändert.'); setPinOld(''); setPinNew('');
} catch (err) {
setMsg(err instanceof Error ? err.message : String(err));
} finally { setBusy(false); }
}
async function handleRegenerateRecovery() {
setBusy(true); setMsg(null);
try { setRecovery(await regenerateRecoveryCode({ userId })); }
catch (err) { setMsg(err instanceof Error ? err.message : String(err)); }
finally { setBusy(false); }
}
async function handleReset() {
if (!window.confirm('Identität wirklich zurücksetzen? Alle bisherigen Chats werden für dich unlesbar.')) return;
setBusy(true); setMsg(null);
try {
const code = await resetIdentity({ userId, pin: pinNew || pinOld });
setRecovery(code);
setMsg('Identität zurückgesetzt.');
} catch (err) {
setMsg(err instanceof Error ? err.message : String(err));
} finally { setBusy(false); }
}
return (
<div className="space-y-6 rounded-xl border border-line bg-surface-2 p-5">
<div className="flex items-center gap-2">
<ShieldIcon className="h-4 w-4 text-accent" />
<h2 className="text-sm font-semibold">Sicherheit</h2>
</div>
<section>
<h3 className="mb-2 text-xs font-medium uppercase tracking-wide text-fg-muted">PIN ändern</h3>
<div className="space-y-2">
<PinInput ariaLabel="Aktuelle PIN" value={pinOld} onChange={setPinOld} />
<PinInput ariaLabel="Neue PIN" value={pinNew} onChange={setPinNew} />
<button type="button"
disabled={busy || pinOld.length !== 6 || pinNew.length !== 6}
onClick={() => void handleChangePin()}
className="rounded-md bg-accent px-3 py-2 text-sm font-semibold text-accent-fg disabled:opacity-60"
>
{busy && <SpinnerIcon className="mr-1 inline h-4 w-4" />}PIN ändern
</button>
</div>
</section>
<section>
<h3 className="mb-2 text-xs font-medium uppercase tracking-wide text-fg-muted">Recovery-Code</h3>
<button type="button" disabled={busy} onClick={() => void handleRegenerateRecovery()}
className="rounded-md border border-line bg-surface-3 px-3 py-2 text-sm hover:bg-surface-2"
>Neuen Recovery-Code erzeugen</button>
{recovery && (
<div className="mt-2 select-all rounded bg-surface-3 px-2 py-1.5 font-mono text-sm tracking-widest">
{recovery}
</div>
)}
</section>
<section>
<h3 className="mb-2 text-xs font-medium uppercase tracking-wide text-rose-400">Identität zurücksetzen</h3>
<p className="mb-2 text-xs text-fg-muted">Erstellt einen neuen Schlüssel. Alle alten Chats werden unlesbar.</p>
<button type="button" disabled={busy} onClick={() => void handleReset()}
className="rounded-md border border-rose-500/40 bg-rose-500/10 px-3 py-2 text-sm text-rose-200 hover:bg-rose-500/20"
>Identität zurücksetzen</button>
</section>
{msg && <p role="status" className="text-sm text-fg-muted">{msg}</p>}
</div>
);
}