feat(mobile): SecurityCenter — PIN change, recovery, reset, migration retry
This commit is contained in:
@@ -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 },
|
||||
});
|
||||
Reference in New Issue
Block a user