feat(mobile): SecurityCenter — PIN change, recovery, reset, migration retry

This commit is contained in:
byGalax
2026-05-16 17:23:04 +02:00
parent ccac822cb9
commit ffaa6ceb70
+200
View File
@@ -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<string | null>(null);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [report, setReport] = useState<LegacyMigrationReport | null>(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 (
<ScrollView contentContainerStyle={styles.container}>
<Text style={styles.section}>PIN ändern</Text>
<Text style={styles.label}>Alte PIN</Text>
<PinInput value={oldPin} onChange={setOldPin} ariaLabel="Alte PIN" />
<Text style={styles.label}>Neue PIN</Text>
<PinInput value={newPin} onChange={setNewPin} ariaLabel="Neue PIN" />
<Pressable style={styles.primary} onPress={handleChangePin} disabled={submitting}>
{submitting ? (
<ActivityIndicator color={colors.text} />
) : (
<Text style={styles.primaryText}>PIN aktualisieren</Text>
)}
</Pressable>
<Text style={styles.section}>Recovery-Code</Text>
<Pressable style={styles.primary} onPress={handleRegenerateRecovery} disabled={submitting}>
<Text style={styles.primaryText}>Neuen Recovery-Code erzeugen</Text>
</Pressable>
{newRecovery && (
<View style={styles.codeBox}>
<Text style={styles.codeText} selectable>
{newRecovery}
</Text>
</View>
)}
<Text style={styles.section}>Migration</Text>
<Pressable style={styles.primary} onPress={handleRetryMigration} disabled={submitting}>
<Text style={styles.primaryText}>Migration erneut versuchen</Text>
</Pressable>
{report && (
<View style={styles.report}>
<Text style={styles.reportLine}>Geräte (Server): {report.serverDevices}</Text>
<Text style={styles.reportLine}>
Lokale Schlüssel im Vault:{' '}
{report.strongholdKeysFromServerDevices + report.strongholdKeysFromBundleScan}
</Text>
<Text style={styles.reportLine}>
Versucht: {report.attempted}, Erfolgreich: {report.migrated}
</Text>
<Text style={styles.reportLine}>
Übersprungen: kein lokaler Schlüssel = {report.noStrongholdKey}, Decrypt-Fehler ={' '}
{report.decryptFailed}, RPC-Fehler = {report.rpcFailed}
</Text>
</View>
)}
<Text style={styles.section}>Gefahrenbereich</Text>
<Pressable style={[styles.primary, styles.danger]} onPress={handleReset} disabled={submitting}>
<Text style={[styles.primaryText, styles.dangerText]}>Identität zurücksetzen</Text>
</Pressable>
{error && <Text style={styles.error}>{error}</Text>}
</ScrollView>
);
}
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 },
});