From ffaa6ceb70da12bdb0703cdaca0af7aa9ac4d921 Mon Sep 17 00:00:00 2001 From: byGalax Date: Sat, 16 May 2026 17:23:04 +0200 Subject: [PATCH] =?UTF-8?q?feat(mobile):=20SecurityCenter=20=E2=80=94=20PI?= =?UTF-8?q?N=20change,=20recovery,=20reset,=20migration=20retry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/mobile/app/(app)/settings/security.tsx | 200 ++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 apps/mobile/app/(app)/settings/security.tsx diff --git a/apps/mobile/app/(app)/settings/security.tsx b/apps/mobile/app/(app)/settings/security.tsx new file mode 100644 index 0000000..92ec164 --- /dev/null +++ b/apps/mobile/app/(app)/settings/security.tsx @@ -0,0 +1,200 @@ +import { useState } from 'react'; +import { + ActivityIndicator, + Alert, + Pressable, + ScrollView, + StyleSheet, + Text, + View, +} from 'react-native'; + +import { PinInput } from '../../../components/PinInput'; +import { useAuth } from '../../../lib/authContext'; +import { + changePin, + regenerateRecoveryCode, + resetIdentity, + retryLegacyMigration, + type LegacyMigrationReport, +} from '../../../lib/userIdentity'; +import { colors } from '../../../theme/colors'; + +export default function SecuritySettings() { + const { userId, refreshUserKeyState } = useAuth(); + const [oldPin, setOldPin] = useState(''); + const [newPin, setNewPin] = useState(''); + const [newRecovery, setNewRecovery] = useState(null); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + const [report, setReport] = useState(null); + + async function handleChangePin() { + if (!userId) return; + if (oldPin.length !== 6 || newPin.length !== 6) { + setError('Beide PINs müssen 6 Ziffern haben.'); + return; + } + setSubmitting(true); + setError(null); + try { + await changePin({ userId, oldPin, newPin }); + setOldPin(''); + setNewPin(''); + Alert.alert('PIN geändert', 'Die neue PIN gilt sofort auf allen Geräten.'); + } catch (e) { + setError(e instanceof Error ? e.message : 'PIN-Änderung fehlgeschlagen'); + } finally { + setSubmitting(false); + } + } + + async function handleRegenerateRecovery() { + if (!userId) return; + setSubmitting(true); + setError(null); + try { + const code = await regenerateRecoveryCode({ userId }); + setNewRecovery(code); + } catch (e) { + setError(e instanceof Error ? e.message : 'Recovery-Code-Erzeugung fehlgeschlagen'); + } finally { + setSubmitting(false); + } + } + + function handleReset() { + if (!userId) return; + Alert.alert( + 'Identität zurücksetzen?', + 'Alle bisherigen Chats werden für dich unlesbar. Diese Aktion kann nicht rückgängig gemacht werden.', + [ + { text: 'Abbrechen', style: 'cancel' }, + { + text: 'Zurücksetzen', + style: 'destructive', + onPress: async () => { + setSubmitting(true); + try { + await resetIdentity({ userId, pin: '000000' }); + await refreshUserKeyState(); + } catch (e) { + setError(e instanceof Error ? e.message : 'Reset fehlgeschlagen'); + } finally { + setSubmitting(false); + } + }, + }, + ], + ); + } + + async function handleRetryMigration() { + if (!userId) return; + setSubmitting(true); + setError(null); + try { + const r = await retryLegacyMigration(userId); + setReport(r); + } catch (e) { + setError(e instanceof Error ? e.message : 'Migration fehlgeschlagen'); + } finally { + setSubmitting(false); + } + } + + return ( + + PIN ändern + Alte PIN + + Neue PIN + + + {submitting ? ( + + ) : ( + PIN aktualisieren + )} + + + Recovery-Code + + Neuen Recovery-Code erzeugen + + {newRecovery && ( + + + {newRecovery} + + + )} + + Migration + + Migration erneut versuchen + + {report && ( + + Geräte (Server): {report.serverDevices} + + Lokale Schlüssel im Vault:{' '} + {report.strongholdKeysFromServerDevices + report.strongholdKeysFromBundleScan} + + + Versucht: {report.attempted}, Erfolgreich: {report.migrated} + + + Übersprungen: kein lokaler Schlüssel = {report.noStrongholdKey}, Decrypt-Fehler ={' '} + {report.decryptFailed}, RPC-Fehler = {report.rpcFailed} + + + )} + + Gefahrenbereich + + Identität zurücksetzen + + + {error && {error}} + + ); +} + +const styles = StyleSheet.create({ + container: { padding: 16, gap: 8, backgroundColor: colors.bg }, + section: { color: colors.text, fontSize: 16, fontWeight: '700', marginTop: 16 }, + label: { color: colors.textMuted, fontSize: 12, marginTop: 4 }, + primary: { + backgroundColor: colors.accent, + paddingVertical: 14, + borderRadius: 12, + alignItems: 'center', + marginTop: 8, + }, + primaryText: { color: colors.text, fontWeight: '700' }, + danger: { backgroundColor: 'transparent', borderColor: colors.danger, borderWidth: 1 }, + dangerText: { color: colors.danger }, + codeBox: { + backgroundColor: colors.surface, + padding: 12, + borderRadius: 10, + marginTop: 4, + }, + codeText: { + color: colors.text, + fontFamily: 'Courier', + fontSize: 16, + letterSpacing: 1.2, + textAlign: 'center', + }, + report: { + backgroundColor: colors.surface, + padding: 12, + borderRadius: 10, + marginTop: 4, + gap: 4, + }, + reportLine: { color: colors.text, fontSize: 13 }, + error: { color: colors.danger, marginTop: 12 }, +});