164 lines
7.0 KiB
TypeScript
164 lines
7.0 KiB
TypeScript
import { useState } from 'react';
|
|
|
|
import { isWipeOnCloseEnabled, setWipeOnClose } from '../lib/memoryWipeSettings';
|
|
import {
|
|
changePin,
|
|
type LegacyMigrationReport,
|
|
regenerateRecoveryCode,
|
|
resetIdentity,
|
|
retryLegacyMigration,
|
|
} 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);
|
|
const [migration, setMigration] = useState<LegacyMigrationReport | null>(null);
|
|
const [wipeOnClose, setWipeOnCloseState] = useState<boolean>(() => isWipeOnCloseEnabled());
|
|
|
|
async function handleRetryMigration() {
|
|
setBusy(true); setMsg(null); setMigration(null);
|
|
try {
|
|
const report = await retryLegacyMigration(userId);
|
|
setMigration(report);
|
|
} catch (err) {
|
|
setMsg(err instanceof Error ? err.message : String(err));
|
|
} finally { setBusy(false); }
|
|
}
|
|
|
|
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-fg-muted">Schlüssel-Migration reparieren</h3>
|
|
<p className="mb-2 text-xs text-fg-muted">
|
|
Versucht, alte Conversation-Schlüssel erneut für deine neue Identität zu re-wrappen.
|
|
Sicher zu klicken wenn Nachrichten verschlüsselt bleiben oder du nicht senden kannst.
|
|
</p>
|
|
<button type="button" disabled={busy} onClick={() => void handleRetryMigration()}
|
|
className="rounded-md border border-line bg-surface-3 px-3 py-2 text-sm hover:bg-surface-2"
|
|
>
|
|
{busy && <SpinnerIcon className="mr-1 inline h-4 w-4" />}Migration erneut ausführen
|
|
</button>
|
|
{migration && (
|
|
<div className="mt-2 rounded border border-line bg-surface-3 p-3 text-xs text-fg-muted">
|
|
<div>Geräte (Server): {migration.serverDevices}</div>
|
|
<div>Lokale Schlüssel im Vault: {migration.strongholdKeysFromServerDevices}
|
|
{migration.strongholdKeysFromBundleScan > 0 && (
|
|
<> (+{migration.strongholdKeysFromBundleScan} aus Bundle-Scan)</>
|
|
)}
|
|
</div>
|
|
<div>Versucht: {migration.attempted}, Erfolgreich: <span className="text-emerald-500">{migration.migrated}</span></div>
|
|
<div>Übersprungen (kein lokaler Schlüssel): {migration.noStrongholdKey}</div>
|
|
<div>Entschlüsselung gescheitert: {migration.decryptFailed}</div>
|
|
<div>Server-Fehler: {migration.rpcFailed}</div>
|
|
{migration.attempted > 0 && migration.migrated === 0 && (
|
|
<p className="mt-2 text-rose-300">
|
|
Keine Bundles migriert. Vermutlich hast du den ursprünglichen Geräteschlüssel nicht mehr lokal.
|
|
Nutze "Identität zurücksetzen" wenn du neu starten willst (alte Chats gehen verloren).
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
<section>
|
|
<h3 className="mb-2 text-xs font-medium uppercase tracking-wide text-fg-muted">
|
|
Cache beim Schließen leeren
|
|
</h3>
|
|
<p className="mb-2 text-xs text-fg-muted">
|
|
Beim Beenden der App werden alle entschlüsselten Caches gelöscht. Beim nächsten Start
|
|
musst du wieder deine PIN eingeben. Empfohlen für gemeinsam genutzte Rechner.
|
|
</p>
|
|
<label className="inline-flex cursor-pointer items-center gap-2 text-sm">
|
|
<input
|
|
type="checkbox"
|
|
checked={wipeOnClose}
|
|
onChange={(e) => {
|
|
setWipeOnClose(e.target.checked);
|
|
setWipeOnCloseState(e.target.checked);
|
|
}}
|
|
/>
|
|
<span>Aktivieren</span>
|
|
</label>
|
|
</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>
|
|
);
|
|
}
|