mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-19 22:06:08 -04:00
Add security settings nav scaffolding (#771)
This commit is contained in:
@@ -20,8 +20,6 @@ export default function CredentialsLayout(): React.ReactNode {
|
||||
options={{
|
||||
title: 'Add Credential',
|
||||
presentation: Platform.OS === 'ios' ? 'modal' : 'card',
|
||||
headerShown: true,
|
||||
gestureEnabled: true,
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
@@ -29,8 +27,6 @@ export default function CredentialsLayout(): React.ReactNode {
|
||||
name="add-edit-page"
|
||||
options={{
|
||||
title: 'Add Credential',
|
||||
headerShown: true,
|
||||
gestureEnabled: true,
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
@@ -39,7 +35,6 @@ export default function CredentialsLayout(): React.ReactNode {
|
||||
options={{
|
||||
title: 'Credential Created',
|
||||
presentation: Platform.OS === 'ios' ? 'modal' : 'card',
|
||||
headerShown: true,
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
@@ -47,7 +42,6 @@ export default function CredentialsLayout(): React.ReactNode {
|
||||
name="[id]"
|
||||
options={{
|
||||
title: 'Credential Details',
|
||||
headerShown: true,
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
@@ -55,7 +49,6 @@ export default function CredentialsLayout(): React.ReactNode {
|
||||
name="email/[id]"
|
||||
options={{
|
||||
title: 'Email Preview',
|
||||
headerShown: true,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -16,7 +16,6 @@ export default function EmailsLayout(): React.ReactNode {
|
||||
name="[id]"
|
||||
options={{
|
||||
title: 'Email',
|
||||
headerShown: true,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -19,7 +19,6 @@ export default function SettingsLayout(): React.ReactNode {
|
||||
options={{
|
||||
title: 'iOS Autofill',
|
||||
headerBackTitle: 'Settings',
|
||||
headerShown: true,
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
@@ -28,16 +27,50 @@ export default function SettingsLayout(): React.ReactNode {
|
||||
options={{
|
||||
title: 'Vault Unlock Method',
|
||||
headerBackTitle: 'Settings',
|
||||
headerShown: true,
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="auto-lock"
|
||||
options={{
|
||||
title: 'Auto-lock Settings',
|
||||
title: 'Auto-lock Timeout',
|
||||
headerBackTitle: 'Settings',
|
||||
headerShown: true,
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="security/index"
|
||||
options={{
|
||||
title: 'Security Settings',
|
||||
headerBackTitle: 'Settings',
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="security/change-password"
|
||||
options={{
|
||||
title: 'Change Password',
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="security/active-sessions"
|
||||
options={{
|
||||
title: 'Active Sessions',
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="security/auth-logs"
|
||||
options={{
|
||||
title: 'Auth Logs',
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="security/delete-account"
|
||||
options={{
|
||||
title: 'Delete Account',
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -258,6 +258,7 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
<ThemedText style={styles.settingItemBadgeText}>1</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
<Ionicons name="chevron-forward" size={20} color={colors.textMuted} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.separator} />
|
||||
@@ -300,6 +301,21 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<TouchableOpacity
|
||||
style={styles.settingItem}
|
||||
onPress={() => router.push('/(tabs)/settings/security')}
|
||||
>
|
||||
<View style={styles.settingItemIcon}>
|
||||
<Ionicons name="shield-checkmark" size={20} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.settingItemContent}>
|
||||
<ThemedText style={styles.settingItemText}>Security Settings</ThemedText>
|
||||
<Ionicons name="chevron-forward" size={20} color={colors.textMuted} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<TouchableOpacity
|
||||
style={styles.settingItem}
|
||||
|
||||
156
apps/mobile-app/app/(tabs)/settings/security/active-sessions.tsx
Normal file
156
apps/mobile-app/app/(tabs)/settings/security/active-sessions.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { StyleSheet, View, TouchableOpacity, Alert, FlatList } from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
import { InlineSkeletonLoader } from '@/components/ui/InlineSkeletonLoader';
|
||||
|
||||
interface Session {
|
||||
id: string;
|
||||
deviceName: string;
|
||||
lastActive: string;
|
||||
ipAddress: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Active sessions screen.
|
||||
*/
|
||||
export default function ActiveSessionsScreen() : React.ReactNode {
|
||||
const colors = useColors();
|
||||
const insets = useSafeAreaInsets();
|
||||
const webApi = useWebApi();
|
||||
|
||||
const [sessions, setSessions] = useState<Session[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingBottom: insets.bottom,
|
||||
paddingHorizontal: 14,
|
||||
},
|
||||
section: {
|
||||
backgroundColor: colors.accentBackground,
|
||||
borderRadius: 10,
|
||||
marginTop: 20,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
sessionItem: {
|
||||
padding: 16,
|
||||
},
|
||||
sessionHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
deviceName: {
|
||||
color: colors.text,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
sessionDetails: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
detailText: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 14,
|
||||
marginBottom: 4,
|
||||
},
|
||||
revokeButton: {
|
||||
color: colors.primary,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
emptyState: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
emptyStateText: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
const loadSessions = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await webApi.getActiveSessions();
|
||||
setSessions(response);
|
||||
} catch (error) {
|
||||
Alert.alert('Error', 'Failed to load active sessions');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRevokeSession = async (sessionId: string) => {
|
||||
Alert.alert(
|
||||
'Revoke Session',
|
||||
'Are you sure you want to revoke this session?',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Revoke',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
await webApi.revokeSession(sessionId);
|
||||
await loadSessions();
|
||||
} catch (error) {
|
||||
Alert.alert('Error', 'Failed to revoke session');
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadSessions();
|
||||
}, []);
|
||||
|
||||
const renderSession = ({ item }: { item: Session }) => (
|
||||
<View style={styles.sessionItem}>
|
||||
<View style={styles.sessionHeader}>
|
||||
<ThemedText style={styles.deviceName}>{item.deviceName}</ThemedText>
|
||||
<TouchableOpacity onPress={() => handleRevokeSession(item.id)}>
|
||||
<ThemedText style={styles.revokeButton}>Revoke</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.sessionDetails}>
|
||||
<ThemedText style={styles.detailText}>Last active: {item.lastActive}</ThemedText>
|
||||
<ThemedText style={styles.detailText}>IP Address: {item.ipAddress}</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<View style={styles.section}>
|
||||
{isLoading ? (
|
||||
<View style={styles.emptyState}>
|
||||
<InlineSkeletonLoader width={200} />
|
||||
</View>
|
||||
) : sessions.length === 0 ? (
|
||||
<View style={styles.emptyState}>
|
||||
<ThemedText style={styles.emptyStateText}>No active sessions</ThemedText>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={sessions}
|
||||
renderItem={renderSession}
|
||||
keyExtractor={(item) => item.id}
|
||||
scrollEnabled={false}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
161
apps/mobile-app/app/(tabs)/settings/security/auth-logs.tsx
Normal file
161
apps/mobile-app/app/(tabs)/settings/security/auth-logs.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { StyleSheet, View, TouchableOpacity, Animated, FlatList } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { TitleContainer } from '@/components/ui/TitleContainer';
|
||||
import { CollapsibleHeader } from '@/components/ui/CollapsibleHeader';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
import { InlineSkeletonLoader } from '@/components/ui/InlineSkeletonLoader';
|
||||
|
||||
interface IAuthLog {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
eventType: string;
|
||||
ipAddress: string;
|
||||
deviceName: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth logs screen.
|
||||
*/
|
||||
export default function AuthLogsScreen() : React.ReactNode {
|
||||
const colors = useColors();
|
||||
const insets = useSafeAreaInsets();
|
||||
const scrollY = useRef(new Animated.Value(0)).current;
|
||||
const webApi = useWebApi();
|
||||
|
||||
const [logs, setLogs] = useState<IAuthLog[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingBottom: insets.bottom,
|
||||
paddingHorizontal: 14,
|
||||
paddingTop: insets.top,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingBottom: 40,
|
||||
paddingTop: 42,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
section: {
|
||||
backgroundColor: colors.accentBackground,
|
||||
borderRadius: 10,
|
||||
marginTop: 20,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
logItem: {
|
||||
padding: 16,
|
||||
},
|
||||
logHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
eventType: {
|
||||
color: colors.text,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
status: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
statusSuccess: {
|
||||
color: colors.success,
|
||||
},
|
||||
statusFailure: {
|
||||
color: colors.error,
|
||||
},
|
||||
logDetails: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
detailText: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 14,
|
||||
marginBottom: 4,
|
||||
},
|
||||
emptyState: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
emptyStateText: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Loads the authentication logs from the server.
|
||||
*/
|
||||
const loadLogs = async () : Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await webApi.getAuthLogs();
|
||||
setLogs(response);
|
||||
} catch (error) {
|
||||
// Error handling is done by the WebApiService
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadLogs();
|
||||
}, []);
|
||||
|
||||
const renderLog = ({ item }: { item: IAuthLog }) => (
|
||||
<View style={styles.logItem}>
|
||||
<View style={styles.logHeader}>
|
||||
<ThemedText style={styles.eventType}>{item.eventType}</ThemedText>
|
||||
<ThemedText style={[
|
||||
styles.status,
|
||||
item.success ? styles.statusSuccess : styles.statusFailure
|
||||
]}>
|
||||
{item.success ? 'Success' : 'Failed'}
|
||||
</ThemedText>
|
||||
</View>
|
||||
<View style={styles.logDetails}>
|
||||
<ThemedText style={styles.detailText}>Time: {item.timestamp}</ThemedText>
|
||||
<ThemedText style={styles.detailText}>Device: {item.deviceName}</ThemedText>
|
||||
<ThemedText style={styles.detailText}>IP Address: {item.ipAddress}</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<View style={styles.scrollContent}>
|
||||
<View style={styles.section}>
|
||||
{isLoading ? (
|
||||
<View style={styles.emptyState}>
|
||||
<InlineSkeletonLoader width={200} />
|
||||
</View>
|
||||
) : logs.length === 0 ? (
|
||||
<View style={styles.emptyState}>
|
||||
<ThemedText style={styles.emptyStateText}>No auth logs found</ThemedText>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={logs}
|
||||
renderItem={renderLog}
|
||||
keyExtractor={(item) => item.id}
|
||||
scrollEnabled={false}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
144
apps/mobile-app/app/(tabs)/settings/security/change-password.tsx
Normal file
144
apps/mobile-app/app/(tabs)/settings/security/change-password.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import { StyleSheet, View, TouchableOpacity, Animated, Alert } from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { TitleContainer } from '@/components/ui/TitleContainer';
|
||||
import { CollapsibleHeader } from '@/components/ui/CollapsibleHeader';
|
||||
import { ThemedTextInput } from '@/components/themed/ThemedTextInput';
|
||||
import { ThemedButton } from '@/components/themed/ThemedButton';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
|
||||
/**
|
||||
* Change password screen.
|
||||
*/
|
||||
export default function ChangePasswordScreen() : React.ReactNode {
|
||||
const colors = useColors();
|
||||
const insets = useSafeAreaInsets();
|
||||
const scrollY = useRef(new Animated.Value(0)).current;
|
||||
const webApi = useWebApi();
|
||||
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingBottom: insets.bottom,
|
||||
paddingHorizontal: 14,
|
||||
paddingTop: insets.top,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingBottom: 40,
|
||||
paddingTop: 42,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
form: {
|
||||
backgroundColor: colors.accentBackground,
|
||||
borderRadius: 10,
|
||||
marginTop: 20,
|
||||
padding: 16,
|
||||
},
|
||||
inputContainer: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
label: {
|
||||
color: colors.text,
|
||||
fontSize: 16,
|
||||
marginBottom: 8,
|
||||
},
|
||||
button: {
|
||||
marginTop: 8,
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!currentPassword || !newPassword || !confirmPassword) {
|
||||
Alert.alert('Error', 'Please fill in all fields');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
Alert.alert('Error', 'New passwords do not match');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// Verify current password
|
||||
/*const isValid = await verifyPassword(currentPassword);
|
||||
if (!isValid) {
|
||||
Alert.alert('Error', 'Current password is incorrect');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new vault version with new password
|
||||
await createNewVersion(newPassword);
|
||||
|
||||
// Submit to server
|
||||
await webApi.submitVaultVersion(vault.getCurrentVersion());
|
||||
|
||||
Alert.alert('Success', 'Password changed successfully', [
|
||||
{ text: 'OK', onPress: () => router.back() }
|
||||
]);*/
|
||||
} catch (error) {
|
||||
Alert.alert('Error', 'Failed to change password. Please try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<View style={styles.scrollContent}>
|
||||
<View style={styles.form}>
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText style={styles.label}>Current Password</ThemedText>
|
||||
<ThemedTextInput
|
||||
secureTextEntry
|
||||
value={currentPassword}
|
||||
onChangeText={setCurrentPassword}
|
||||
placeholder="Enter current password"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText style={styles.label}>New Password</ThemedText>
|
||||
<ThemedTextInput
|
||||
secureTextEntry
|
||||
value={newPassword}
|
||||
onChangeText={setNewPassword}
|
||||
placeholder="Enter new password"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText style={styles.label}>Confirm New Password</ThemedText>
|
||||
<ThemedTextInput
|
||||
secureTextEntry
|
||||
value={confirmPassword}
|
||||
onChangeText={setConfirmPassword}
|
||||
placeholder="Confirm new password"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<ThemedButton
|
||||
title="Change Password"
|
||||
onPress={handleSubmit}
|
||||
loading={isLoading}
|
||||
style={styles.button}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
174
apps/mobile-app/app/(tabs)/settings/security/delete-account.tsx
Normal file
174
apps/mobile-app/app/(tabs)/settings/security/delete-account.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import { StyleSheet, View, TouchableOpacity, Animated, Alert } from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { TitleContainer } from '@/components/ui/TitleContainer';
|
||||
import { CollapsibleHeader } from '@/components/ui/CollapsibleHeader';
|
||||
import { ThemedTextInput } from '@/components/themed/ThemedTextInput';
|
||||
import { ThemedButton } from '@/components/themed/ThemedButton';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
|
||||
/**
|
||||
* Delete account screen.
|
||||
*/
|
||||
export default function DeleteAccountScreen() : React.ReactNode {
|
||||
const colors = useColors();
|
||||
const insets = useSafeAreaInsets();
|
||||
const scrollY = useRef(new Animated.Value(0)).current;
|
||||
const webApi = useWebApi();
|
||||
const { username } = useAuth();
|
||||
|
||||
const [confirmUsername, setConfirmUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [step, setStep] = useState<'username' | 'password'>('username');
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingBottom: insets.bottom,
|
||||
paddingHorizontal: 14,
|
||||
paddingTop: insets.top,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingBottom: 40,
|
||||
paddingTop: 42,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
form: {
|
||||
backgroundColor: colors.accentBackground,
|
||||
borderRadius: 10,
|
||||
marginTop: 20,
|
||||
padding: 16,
|
||||
},
|
||||
inputContainer: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
label: {
|
||||
color: colors.text,
|
||||
fontSize: 16,
|
||||
marginBottom: 8,
|
||||
},
|
||||
warningText: {
|
||||
color: colors.error,
|
||||
fontSize: 14,
|
||||
marginBottom: 16,
|
||||
textAlign: 'center',
|
||||
},
|
||||
button: {
|
||||
marginTop: 8,
|
||||
},
|
||||
buttonDanger: {
|
||||
backgroundColor: colors.error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleUsernameSubmit = () => {
|
||||
if (confirmUsername !== username) {
|
||||
Alert.alert('Error', 'Username does not match');
|
||||
return;
|
||||
}
|
||||
setStep('password');
|
||||
};
|
||||
|
||||
const handleDeleteAccount = async () => {
|
||||
if (!password) {
|
||||
Alert.alert('Error', 'Please enter your password');
|
||||
return;
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
'Delete Account',
|
||||
'Are you absolutely sure you want to delete your account? This action cannot be undone.',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Delete Account',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// Verify password
|
||||
/*const isValid = await vault.verifyPassword(password);
|
||||
if (!isValid) {
|
||||
Alert.alert('Error', 'Password is incorrect');
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete account
|
||||
await webApi.deleteAccount();*/
|
||||
|
||||
// Log out and return to login
|
||||
await webApi.logout();
|
||||
router.replace('/login');
|
||||
} catch (error) {
|
||||
Alert.alert('Error', 'Failed to delete account. Please try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<View style={styles.scrollContent}>
|
||||
<View style={styles.form}>
|
||||
{step === 'username' ? (
|
||||
<>
|
||||
<ThemedText style={styles.warningText}>
|
||||
Warning: This action cannot be undone. All your data will be permanently deleted.
|
||||
</ThemedText>
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText style={styles.label}>Enter your username to confirm</ThemedText>
|
||||
<ThemedTextInput
|
||||
value={confirmUsername}
|
||||
onChangeText={setConfirmUsername}
|
||||
placeholder="Enter username"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</View>
|
||||
<ThemedButton
|
||||
title="Continue"
|
||||
onPress={handleUsernameSubmit}
|
||||
style={styles.button}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ThemedText style={styles.warningText}>
|
||||
Please enter your password to confirm account deletion
|
||||
</ThemedText>
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText style={styles.label}>Password</ThemedText>
|
||||
<ThemedTextInput
|
||||
secureTextEntry
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
placeholder="Enter password"
|
||||
/>
|
||||
</View>
|
||||
<ThemedButton
|
||||
title="Delete Account"
|
||||
onPress={handleDeleteAccount}
|
||||
loading={isLoading}
|
||||
style={[styles.button, styles.buttonDanger]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
146
apps/mobile-app/app/(tabs)/settings/security/index.tsx
Normal file
146
apps/mobile-app/app/(tabs)/settings/security/index.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { StyleSheet, View, TouchableOpacity, Animated, ScrollView } from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRef } from 'react';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { TitleContainer } from '@/components/ui/TitleContainer';
|
||||
import { CollapsibleHeader } from '@/components/ui/CollapsibleHeader';
|
||||
|
||||
/**
|
||||
* Security settings screen.
|
||||
*/
|
||||
export default function SecuritySettingsScreen() : React.ReactNode {
|
||||
const colors = useColors();
|
||||
const insets = useSafeAreaInsets();
|
||||
const scrollY = useRef(new Animated.Value(0)).current;
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingBottom: insets.bottom,
|
||||
paddingHorizontal: 14,
|
||||
paddingTop: insets.top,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingBottom: 40,
|
||||
paddingTop: 42,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
section: {
|
||||
backgroundColor: colors.accentBackground,
|
||||
borderRadius: 10,
|
||||
marginTop: 20,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
separator: {
|
||||
backgroundColor: colors.accentBorder,
|
||||
height: StyleSheet.hairlineWidth,
|
||||
marginLeft: 52,
|
||||
},
|
||||
settingItem: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.accentBackground,
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 6,
|
||||
},
|
||||
settingItemContent: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
paddingVertical: 10,
|
||||
},
|
||||
settingItemIcon: {
|
||||
alignItems: 'center',
|
||||
height: 24,
|
||||
justifyContent: 'center',
|
||||
marginRight: 12,
|
||||
width: 24,
|
||||
},
|
||||
settingItemText: {
|
||||
color: colors.text,
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<Animated.ScrollView
|
||||
ref={scrollViewRef}
|
||||
onScroll={Animated.event(
|
||||
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
|
||||
{ useNativeDriver: true }
|
||||
)}
|
||||
scrollEventThrottle={16}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
scrollIndicatorInsets={{ bottom: 40 }}
|
||||
style={styles.scrollView}
|
||||
>
|
||||
<View style={styles.section}>
|
||||
<TouchableOpacity
|
||||
style={styles.settingItem}
|
||||
onPress={() => router.push('/settings/security/change-password')}
|
||||
>
|
||||
<View style={styles.settingItemIcon}>
|
||||
<Ionicons name="key" size={20} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.settingItemContent}>
|
||||
<ThemedText style={styles.settingItemText}>Change Master Password</ThemedText>
|
||||
<Ionicons name="chevron-forward" size={20} color={colors.textMuted} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.separator} />
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.settingItem}
|
||||
onPress={() => router.push('/settings/security/active-sessions')}
|
||||
>
|
||||
<View style={styles.settingItemIcon}>
|
||||
<Ionicons name="desktop" size={20} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.settingItemContent}>
|
||||
<ThemedText style={styles.settingItemText}>Active Sessions</ThemedText>
|
||||
<Ionicons name="chevron-forward" size={20} color={colors.textMuted} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.separator} />
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.settingItem}
|
||||
onPress={() => router.push('/settings/security/auth-logs')}
|
||||
>
|
||||
<View style={styles.settingItemIcon}>
|
||||
<Ionicons name="list" size={20} color={colors.text} />
|
||||
</View>
|
||||
<View style={styles.settingItemContent}>
|
||||
<ThemedText style={styles.settingItemText}>Recent Auth Logs</ThemedText>
|
||||
<Ionicons name="chevron-forward" size={20} color={colors.textMuted} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.separator} />
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.settingItem}
|
||||
onPress={() => router.push('/settings/security/delete-account')}
|
||||
>
|
||||
<View style={styles.settingItemIcon}>
|
||||
<Ionicons name="trash" size={20} color={colors.primary} />
|
||||
</View>
|
||||
<View style={styles.settingItemContent}>
|
||||
<ThemedText style={[styles.settingItemText, { color: colors.primary }]}>Delete Account</ThemedText>
|
||||
<Ionicons name="chevron-forward" size={20} color={colors.textMuted} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Animated.ScrollView>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
72
apps/mobile-app/components/themed/ThemedButton.tsx
Normal file
72
apps/mobile-app/components/themed/ThemedButton.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { TouchableOpacity, StyleSheet, ActivityIndicator, ViewStyle, TextStyle } from 'react-native';
|
||||
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
|
||||
type ThemedButtonProps = {
|
||||
title: string;
|
||||
onPress: () => void;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
style?: ViewStyle;
|
||||
textStyle?: TextStyle;
|
||||
};
|
||||
|
||||
/**
|
||||
* Themed button component that matches the app's design system.
|
||||
*/
|
||||
export const ThemedButton: React.FC<ThemedButtonProps> = ({
|
||||
title,
|
||||
onPress,
|
||||
loading = false,
|
||||
disabled = false,
|
||||
style,
|
||||
textStyle,
|
||||
}) => {
|
||||
const colors = useColors();
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 6,
|
||||
justifyContent: 'center',
|
||||
padding: 12,
|
||||
},
|
||||
buttonDisabled: {
|
||||
backgroundColor: colors.textMuted,
|
||||
opacity: 0.7,
|
||||
},
|
||||
buttonText: {
|
||||
color: colors.background,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
loadingContainer: {
|
||||
position: 'absolute',
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.button,
|
||||
disabled && styles.buttonDisabled,
|
||||
style,
|
||||
]}
|
||||
onPress={onPress}
|
||||
disabled={disabled || loading}
|
||||
>
|
||||
<ThemedText style={[styles.buttonText, textStyle]}>
|
||||
{title}
|
||||
</ThemedText>
|
||||
{loading && (
|
||||
<ActivityIndicator
|
||||
style={styles.loadingContainer}
|
||||
color={colors.background}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
49
apps/mobile-app/components/themed/ThemedTextInput.tsx
Normal file
49
apps/mobile-app/components/themed/ThemedTextInput.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { TextInput, TextInputProps, StyleSheet, View, Text } from 'react-native';
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
|
||||
type ThemedTextInputProps = TextInputProps & {
|
||||
error?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Themed text input component that matches the app's design system.
|
||||
*/
|
||||
export const ThemedTextInput: React.FC<ThemedTextInputProps> = ({ error, style, ...props }) => {
|
||||
const colors = useColors();
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
errorText: {
|
||||
color: 'red',
|
||||
fontSize: 12,
|
||||
marginTop: 4,
|
||||
},
|
||||
input: {
|
||||
color: colors.text,
|
||||
fontSize: 16,
|
||||
padding: 10,
|
||||
},
|
||||
inputContainer: {
|
||||
backgroundColor: colors.background,
|
||||
borderColor: error ? 'red' : colors.accentBorder,
|
||||
borderRadius: 6,
|
||||
borderWidth: 1,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.inputContainer}>
|
||||
<TextInput
|
||||
style={[styles.input, style]}
|
||||
placeholderTextColor={colors.textMuted}
|
||||
autoCapitalize="none"
|
||||
autoComplete="off"
|
||||
autoCorrect={false}
|
||||
{...props}
|
||||
/>
|
||||
</View>
|
||||
{error && <Text style={styles.errorText}>{error}</Text>}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user