201 lines
6.3 KiB
TypeScript
201 lines
6.3 KiB
TypeScript
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 },
|
|
});
|