mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-20 07:39:07 -04:00
Add credential update scaffolding (#771)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useLocalSearchParams } from 'expo-router';
|
||||
import { useLocalSearchParams, useNavigation, useRouter } from 'expo-router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ActivityIndicator, View, Text, useColorScheme, StyleSheet, TouchableOpacity, Linking } from 'react-native';
|
||||
import { ActivityIndicator, View, Text, StyleSheet, TouchableOpacity, Linking } from 'react-native';
|
||||
import Toast from 'react-native-toast-message';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { ThemedView } from '@/components/ThemedView';
|
||||
@@ -13,14 +13,42 @@ import { AliasDetails } from '@/components/credentialDetails/AliasDetails';
|
||||
import { NotesSection } from '@/components/credentialDetails/NotesSection';
|
||||
import { EmailPreview } from '@/components/credentialDetails/EmailPreview';
|
||||
import { TotpSection } from '@/components/credentialDetails/TotpSection';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import emitter from '@/utils/EventEmitter';
|
||||
|
||||
export default function CredentialDetailsScreen() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [credential, setCredential] = useState<Credential | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const dbContext = useDb();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDarkMode = colorScheme === 'dark';
|
||||
const navigation = useNavigation();
|
||||
const colors = useColors();
|
||||
const router = useRouter();
|
||||
|
||||
// Set header buttons
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: () => (
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<TouchableOpacity
|
||||
onPress={handleEdit}
|
||||
style={{ padding: 10, paddingRight: 0 }}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="edit"
|
||||
size={24}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
),
|
||||
});
|
||||
}, [navigation, credential]);
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/(tabs)/credentials/add-edit?id=${id}`);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const loadCredential = async () => {
|
||||
@@ -38,7 +66,16 @@ export default function CredentialDetailsScreen() {
|
||||
|
||||
loadCredential();
|
||||
|
||||
// Add listener for credential changes
|
||||
const credentialChangedSub = emitter.addListener('credentialChanged', async (changedId: string) => {
|
||||
if (changedId === id) {
|
||||
console.log('This credential was changed, refreshing details');
|
||||
await loadCredential();
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
credentialChangedSub.remove();
|
||||
Toast.hide();
|
||||
};
|
||||
}, [id, dbContext.dbAvailable]);
|
||||
@@ -68,7 +105,7 @@ export default function CredentialDetailsScreen() {
|
||||
</ThemedText>
|
||||
{credential.ServiceUrl && (
|
||||
<TouchableOpacity onPress={() => Linking.openURL(credential.ServiceUrl!)}>
|
||||
<Text style={[styles.serviceUrl, { color: isDarkMode ? '#60a5fa' : '#2563eb' }]}>
|
||||
<Text style={[styles.serviceUrl, { color: colors.primary }]}>
|
||||
{credential.ServiceUrl}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { StyleSheet, View, TextInput, TouchableOpacity, ScrollView, Platform, Animated } from 'react-native';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useLocalSearchParams, useNavigation, useRouter } from 'expo-router';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { ThemedView } from '@/components/ThemedView';
|
||||
@@ -10,6 +10,7 @@ import { Credential } from '@/utils/types/Credential';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import Toast from 'react-native-toast-message';
|
||||
import { Gender } from "@/utils/generators/Identity/types/Gender";
|
||||
import emitter from '@/utils/EventEmitter';
|
||||
|
||||
type CredentialMode = 'random' | 'manual';
|
||||
|
||||
@@ -21,6 +22,7 @@ export default function AddEditCredentialScreen() {
|
||||
const [mode, setMode] = useState<CredentialMode>('random');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const navigation = useNavigation();
|
||||
const serviceNameInputRef = useRef<TextInput>(null);
|
||||
const [credential, setCredential] = useState<Partial<Credential>>({
|
||||
Id: "",
|
||||
Username: "",
|
||||
@@ -64,10 +66,10 @@ export default function AddEditCredentialScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set navigation options
|
||||
// Set header buttons
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
title: isEditMode ? 'Edit Credential' : 'Add Credential',
|
||||
headerLeft: () => (
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
@@ -118,9 +120,31 @@ export default function AddEditCredentialScreen() {
|
||||
ServiceName: serviceName,
|
||||
// ... other form fields
|
||||
}));
|
||||
|
||||
// In create mode, autofocus the service name field and select all default text
|
||||
// so user can start renaming the service immediately if they want.
|
||||
if (!isEditMode) {
|
||||
setTimeout(() => {
|
||||
serviceNameInputRef.current?.focus();
|
||||
if (serviceUrl) {
|
||||
// If serviceUrl is provided, select all text
|
||||
serviceNameInputRef.current?.setSelection(0, serviceName.length || 0);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
}, [serviceUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
// Focus and select text logic
|
||||
if (!isEditMode) {
|
||||
// In create mode, always focus the service name field
|
||||
setTimeout(() => {
|
||||
serviceNameInputRef.current?.focus();
|
||||
}, 100);
|
||||
}
|
||||
}, [isEditMode, serviceUrl]);
|
||||
|
||||
const loadExistingCredential = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
@@ -183,19 +207,33 @@ export default function AddEditCredentialScreen() {
|
||||
credentialToSave = generateRandomValues();
|
||||
}
|
||||
|
||||
// Always use createCredential since updateCredentialById doesn't exist
|
||||
await dbContext.sqliteClient!.createCredential(credentialToSave);
|
||||
Toast.show({
|
||||
type: 'success',
|
||||
text1: 'Credential saved successfully',
|
||||
position: 'bottom'
|
||||
});
|
||||
if (isEditMode) {
|
||||
// Update existing credential
|
||||
await dbContext.sqliteClient!.updateCredentialById(credentialToSave);
|
||||
Toast.show({
|
||||
type: 'success',
|
||||
text1: 'Credential updated successfully',
|
||||
position: 'bottom'
|
||||
});
|
||||
} else {
|
||||
// Create new credential
|
||||
await dbContext.sqliteClient!.createCredential(credentialToSave);
|
||||
Toast.show({
|
||||
type: 'success',
|
||||
text1: 'Credential created successfully',
|
||||
position: 'bottom'
|
||||
});
|
||||
}
|
||||
|
||||
// Emit an event to notify list and detail views to refresh
|
||||
emitter.emit('credentialChanged', credentialToSave.Id);
|
||||
|
||||
router.back();
|
||||
} catch (error) {
|
||||
console.error('Error saving credential:', error);
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: 'Failed to save credential',
|
||||
text1: isEditMode ? 'Failed to update credential' : 'Failed to create credential',
|
||||
text2: error instanceof Error ? error.message : 'Unknown error',
|
||||
position: 'bottom'
|
||||
});
|
||||
@@ -291,8 +329,6 @@ export default function AddEditCredentialScreen() {
|
||||
},
|
||||
});
|
||||
|
||||
console.log('credential serviceName: ', credential.ServiceName);
|
||||
|
||||
return (
|
||||
<ThemedSafeAreaView style={styles.container}>
|
||||
<ThemedView style={styles.content}>
|
||||
@@ -321,6 +357,7 @@ export default function AddEditCredentialScreen() {
|
||||
<View style={styles.section}>
|
||||
<ThemedText style={styles.sectionTitle}>Service Information</ThemedText>
|
||||
<TextInput
|
||||
ref={serviceNameInputRef}
|
||||
style={styles.input}
|
||||
placeholder="Service Name"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function CredentialsScreen() {
|
||||
setIsTabFocused(false);
|
||||
});
|
||||
|
||||
const sub = emitter.addListener('tabPress', (routeName: string) => {
|
||||
const tabPressSub = emitter.addListener('tabPress', (routeName: string) => {
|
||||
if (routeName === 'credentials' && isTabFocused) {
|
||||
console.log('Credentials tab re-pressed while focused: reset screen');
|
||||
setSearchQuery(''); // Reset search
|
||||
@@ -53,8 +53,15 @@ export default function CredentialsScreen() {
|
||||
}
|
||||
});
|
||||
|
||||
// Add listener for credential changes
|
||||
const credentialChangedSub = emitter.addListener('credentialChanged', async () => {
|
||||
console.log('Credential changed, refreshing list');
|
||||
await loadCredentials();
|
||||
});
|
||||
|
||||
return () => {
|
||||
sub.remove();
|
||||
tabPressSub.remove();
|
||||
credentialChangedSub.remove();
|
||||
unsubscribeFocus();
|
||||
unsubscribeBlur();
|
||||
};
|
||||
|
||||
@@ -542,6 +542,140 @@ class SqliteClient {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing credential with associated entities
|
||||
* @param credential The credential object to update
|
||||
* @returns The number of rows modified
|
||||
*/
|
||||
public async updateCredentialById(credential: Credential): Promise<number> {
|
||||
try {
|
||||
await NativeVaultManager.beginTransaction();
|
||||
const currentDateTime = new Date().toISOString()
|
||||
.replace('T', ' ')
|
||||
.replace('Z', '')
|
||||
.substring(0, 23);
|
||||
|
||||
// Get existing credential to compare changes
|
||||
const existingCredential = await this.getCredentialById(credential.Id);
|
||||
if (!existingCredential) {
|
||||
throw new Error('Credential not found');
|
||||
}
|
||||
|
||||
// 1. Update Service
|
||||
// TODO: make Logo update optional, currently not supported as its becoming null.
|
||||
// Logo = ?,
|
||||
|
||||
const serviceQuery = `
|
||||
UPDATE Services
|
||||
SET Name = ?,
|
||||
Url = ?,
|
||||
UpdatedAt = ?
|
||||
WHERE Id = (
|
||||
SELECT ServiceId
|
||||
FROM Credentials
|
||||
WHERE Id = ?
|
||||
)`;
|
||||
|
||||
/*let logoData = null;
|
||||
try {
|
||||
if (credential.Logo) {
|
||||
if (typeof credential.Logo === 'object' && !ArrayBuffer.isView(credential.Logo)) {
|
||||
const values = Object.values(credential.Logo);
|
||||
logoData = new Uint8Array(values);
|
||||
} else if (Array.isArray(credential.Logo) || credential.Logo instanceof ArrayBuffer || credential.Logo instanceof Uint8Array) {
|
||||
logoData = new Uint8Array(credential.Logo);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to convert logo to Uint8Array:', error);
|
||||
logoData = null;
|
||||
}*/
|
||||
|
||||
await this.executeUpdate(serviceQuery, [
|
||||
credential.ServiceName,
|
||||
credential.ServiceUrl ?? null,
|
||||
//logoData,
|
||||
currentDateTime,
|
||||
credential.Id
|
||||
]);
|
||||
|
||||
// 2. Update Alias
|
||||
const aliasQuery = `
|
||||
UPDATE Aliases
|
||||
SET FirstName = ?,
|
||||
LastName = ?,
|
||||
NickName = ?,
|
||||
BirthDate = ?,
|
||||
Gender = ?,
|
||||
Email = ?,
|
||||
UpdatedAt = ?
|
||||
WHERE Id = (
|
||||
SELECT AliasId
|
||||
FROM Credentials
|
||||
WHERE Id = ?
|
||||
)`;
|
||||
|
||||
// Only update BirthDate if it's actually different (accounting for format differences)
|
||||
let birthDate = credential.Alias.BirthDate;
|
||||
if (birthDate && existingCredential.Alias.BirthDate) {
|
||||
const newDate = new Date(birthDate);
|
||||
const existingDate = new Date(existingCredential.Alias.BirthDate);
|
||||
if (newDate.getTime() === existingDate.getTime()) {
|
||||
birthDate = existingCredential.Alias.BirthDate;
|
||||
}
|
||||
}
|
||||
|
||||
await this.executeUpdate(aliasQuery, [
|
||||
credential.Alias.FirstName ?? null,
|
||||
credential.Alias.LastName ?? null,
|
||||
credential.Alias.NickName ?? null,
|
||||
birthDate ?? null,
|
||||
credential.Alias.Gender ?? null,
|
||||
credential.Alias.Email ?? null,
|
||||
currentDateTime,
|
||||
credential.Id
|
||||
]);
|
||||
|
||||
// 3. Update Credential
|
||||
const credentialQuery = `
|
||||
UPDATE Credentials
|
||||
SET Username = ?,
|
||||
Notes = ?,
|
||||
UpdatedAt = ?
|
||||
WHERE Id = ?`;
|
||||
|
||||
await this.executeUpdate(credentialQuery, [
|
||||
credential.Username ?? null,
|
||||
credential.Notes ?? null,
|
||||
currentDateTime,
|
||||
credential.Id
|
||||
]);
|
||||
|
||||
// 4. Update Password if changed
|
||||
if (credential.Password !== existingCredential.Password) {
|
||||
const passwordQuery = `
|
||||
UPDATE Passwords
|
||||
SET Value = ?,
|
||||
UpdatedAt = ?
|
||||
WHERE CredentialId = ?`;
|
||||
|
||||
await this.executeUpdate(passwordQuery, [
|
||||
credential.Password,
|
||||
currentDateTime,
|
||||
credential.Id
|
||||
]);
|
||||
}
|
||||
|
||||
await NativeVaultManager.commitTransaction();
|
||||
return 1;
|
||||
|
||||
} catch (error) {
|
||||
await NativeVaultManager.rollbackTransaction();
|
||||
console.error('Error updating credential:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SqliteClient;
|
||||
Reference in New Issue
Block a user