mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-18 13:28:12 -04:00
Add vault update check to main layout scaffolding (#771)
This commit is contained in:
@@ -31,7 +31,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const dbContext = useDb();
|
||||
|
||||
/**
|
||||
* Check for tokens in chrome storage on initial load.
|
||||
* Check for tokens in browser local storage on initial load.
|
||||
*/
|
||||
useEffect(() => {
|
||||
/**
|
||||
@@ -52,7 +52,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Set auth tokens in chrome storage as part of the login process. After db is initialized, the login method should be called as well.
|
||||
* Set auth tokens in browser local storage as part of the login process. After db is initialized, the login method should be called as well.
|
||||
*/
|
||||
const setAuthTokens = useCallback(async (username: string, accessToken: string, refreshToken: string) : Promise<void> => {
|
||||
await storage.setItem('local:username', username);
|
||||
|
||||
@@ -3,12 +3,14 @@ import { useFonts } from 'expo-font';
|
||||
import { Stack } from 'expo-router';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useCallback, useRef } from 'react';
|
||||
import { AppState, AppStateStatus } from 'react-native';
|
||||
import 'react-native-reanimated';
|
||||
// Required for certain modules such as secure-remote-password which relies on crypto.getRandomValues
|
||||
// and this is not available in react-native without this polyfill
|
||||
import 'react-native-get-random-values';
|
||||
import Toast from 'react-native-toast-message';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { LoadingProvider } from '@/context/LoadingContext';
|
||||
@@ -17,6 +19,8 @@ import { AuthProvider } from '@/context/AuthContext';
|
||||
import { WebApiProvider } from '@/context/WebApiContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
import { VaultResponse } from '@/utils/types/webapi/VaultResponse';
|
||||
import { View, ActivityIndicator, Text } from 'react-native';
|
||||
|
||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||
@@ -53,6 +57,8 @@ function RootLayoutNav() {
|
||||
const colorScheme = useColorScheme();
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const webApi = useWebApi();
|
||||
const isFirstMount = useRef(true);
|
||||
|
||||
// Check if user is authenticated and database is available
|
||||
const isFullyInitialized = authContext.isInitialized && dbContext.dbInitialized;
|
||||
@@ -60,6 +66,98 @@ function RootLayoutNav() {
|
||||
const isDatabaseAvailable = dbContext.dbAvailable;
|
||||
const requireLoginOrUnlock = isFullyInitialized && (!isAuthenticated || !isDatabaseAvailable);
|
||||
|
||||
/**
|
||||
* Check for vault updates and fetch if necessary
|
||||
*/
|
||||
const checkVaultUpdates = useCallback(async (isStartup: boolean = false) => {
|
||||
const isLoggedIn = await authContext.initializeAuth();
|
||||
|
||||
if (!isLoggedIn) {
|
||||
console.log('Start check vault update: Not authenticated');
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: add check to know if the encryption key is available in the keychain
|
||||
// If not, we should redirect to the unlock screen.
|
||||
// Or do this check in the "isLoggedIn" check above?
|
||||
const passwordHashBase64 = await AsyncStorage.getItem('passwordHash');
|
||||
if (!passwordHashBase64) {
|
||||
//console.log('Vault check error: Password hash not found');
|
||||
//await webApi.logout('Password hash not found');
|
||||
//rreturn;
|
||||
}
|
||||
|
||||
console.log('Start check vault updates');
|
||||
|
||||
try {
|
||||
// On startup, always check. Otherwise, check the time elapsed
|
||||
if (!isStartup) {
|
||||
const lastCheckStr = await AsyncStorage.getItem('lastVaultCheck');
|
||||
const lastCheck = lastCheckStr ? parseInt(lastCheckStr, 10) : 0;
|
||||
const now = Date.now();
|
||||
|
||||
// Only check if more than 1 hour has passed since last check
|
||||
if (now - lastCheck < 3600000) {
|
||||
console.log('Vault check skipped: Not enough time has passed since last check');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Checking vault updates');
|
||||
|
||||
// Update last check time
|
||||
await AsyncStorage.setItem('lastVaultCheck', Date.now().toString());
|
||||
|
||||
// Do status check to ensure this mobile app is supported and check vault revision
|
||||
const statusResponse = await webApi.getStatus();
|
||||
const statusError = webApi.validateStatusResponse(statusResponse);
|
||||
if (statusError !== null) {
|
||||
console.log('Vault check error:', statusError);
|
||||
await webApi.logout(statusError);
|
||||
return;
|
||||
}
|
||||
r
|
||||
// If the vault revision is higher, fetch the latest vault
|
||||
if (statusResponse.vaultRevision > dbContext.vaultRevision) {
|
||||
const vaultResponseJson = await webApi.get<VaultResponse>('Vault');
|
||||
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson);
|
||||
if (vaultError) {
|
||||
console.log('Vault check error:', vaultError);
|
||||
console.log('Vault response:', vaultResponseJson);
|
||||
await webApi.logout(vaultError);
|
||||
return;
|
||||
}
|
||||
// Initialize the SQLite context again with the newly retrieved decrypted blob
|
||||
await dbContext.initializeDatabase(vaultResponseJson);
|
||||
}
|
||||
else {
|
||||
console.log('Vault check finished: Vault revision is the same, no action requiredr');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Vault check error:', err);
|
||||
}
|
||||
}, [isAuthenticated, dbContext, webApi]);
|
||||
|
||||
// Handle app state changes and initial mount
|
||||
useEffect(() => {
|
||||
// On first mount (cold boot), always check
|
||||
if (isFirstMount.current) {
|
||||
isFirstMount.current = false;
|
||||
checkVaultUpdates(true);
|
||||
}
|
||||
|
||||
const subscription = AppState.addEventListener('change', (nextAppState: AppStateStatus) => {
|
||||
if (nextAppState === 'active') {
|
||||
checkVaultUpdates(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.remove();
|
||||
};
|
||||
}, [checkVaultUpdates]);
|
||||
|
||||
// Show loading screen while initializing
|
||||
if (!isFullyInitialized) {
|
||||
return (
|
||||
|
||||
@@ -7,6 +7,7 @@ type AuthContextType = {
|
||||
isInitialized: boolean;
|
||||
username: string | null;
|
||||
setAuthTokens: (username: string, accessToken: string, refreshToken: string) => Promise<void>;
|
||||
initializeAuth: () => Promise<boolean>;
|
||||
login: () => Promise<void>;
|
||||
logout: (errorMessage?: string) => Promise<void>;
|
||||
globalMessage: string | null;
|
||||
@@ -29,28 +30,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const dbContext = useDb();
|
||||
|
||||
/**
|
||||
* Check for tokens in chrome storage on initial load.
|
||||
*/
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Initialize the authentication state.
|
||||
*/
|
||||
const initializeAuth = async () : Promise<void> => {
|
||||
const accessToken = await AsyncStorage.getItem('accessToken') as string;
|
||||
const refreshToken = await AsyncStorage.getItem('refreshToken') as string;
|
||||
const username = await AsyncStorage.getItem('username') as string;
|
||||
if (accessToken && refreshToken && username) {
|
||||
setUsername(username);
|
||||
setIsLoggedIn(true);
|
||||
}
|
||||
setIsInitialized(true);
|
||||
};
|
||||
|
||||
initializeAuth();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Set auth tokens in chrome storage as part of the login process. After db is initialized, the login method should be called as well.
|
||||
* Set auth tokens in storage as part of the login process. After db is initialized, the login method should be called as well.
|
||||
*/
|
||||
const setAuthTokens = useCallback(async (username: string, accessToken: string, refreshToken: string) : Promise<void> => {
|
||||
await AsyncStorage.setItem('username', username);
|
||||
@@ -60,6 +40,24 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
setUsername(username);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Initialize the authentication state, called on initial load by _layout.tsx.
|
||||
* @returns boolean indicating whether the user is logged in
|
||||
*/
|
||||
const initializeAuth = useCallback(async () : Promise<boolean> => {
|
||||
const accessToken = await AsyncStorage.getItem('accessToken') as string;
|
||||
const refreshToken = await AsyncStorage.getItem('refreshToken') as string;
|
||||
const username = await AsyncStorage.getItem('username') as string;
|
||||
let isAuthenticated = false;
|
||||
if (accessToken && refreshToken && username) {
|
||||
setUsername(username);
|
||||
setIsLoggedIn(true);
|
||||
isAuthenticated = true;
|
||||
}
|
||||
setIsInitialized(true);
|
||||
return isAuthenticated;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Set logged in status to true which refreshes the app.
|
||||
*/
|
||||
@@ -96,12 +94,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
isLoggedIn,
|
||||
isInitialized,
|
||||
username,
|
||||
initializeAuth,
|
||||
setAuthTokens,
|
||||
login,
|
||||
logout,
|
||||
globalMessage,
|
||||
clearGlobalMessage,
|
||||
}), [isLoggedIn, isInitialized, username, globalMessage, setAuthTokens, login, logout, clearGlobalMessage]);
|
||||
}), [isLoggedIn, isInitialized, username, globalMessage, setAuthTokens, login, logout, clearGlobalMessage, initializeAuth]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={contextValue}>
|
||||
|
||||
@@ -23,9 +23,9 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
const credentialManager = NativeModules.CredentialManager;
|
||||
|
||||
/**
|
||||
* SQLite client.
|
||||
* SQLite client is initialized in constructor as it passes SQL queries to the native module.
|
||||
*/
|
||||
const [sqliteClient, setSqliteClient] = useState<SqliteClient | null>(null);
|
||||
const sqliteClient = new SqliteClient();
|
||||
|
||||
/**
|
||||
* Database initialization state. If true, the database has been initialized and the dbAvailable state is correct.
|
||||
@@ -52,20 +52,16 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
*/
|
||||
const [privateEmailDomains, setPrivateEmailDomains] = useState<string[]>([]);
|
||||
|
||||
const initializeDatabase = useCallback(async (vaultResponse: VaultResponse, derivedKey: string) => {
|
||||
// Attempt to decrypt the blob.
|
||||
/*console.log('attempt to decrypt vault');
|
||||
const decryptedBlob = await EncryptionUtility.symmetricDecrypt(
|
||||
vaultResponse.vault.blob,
|
||||
derivedKey
|
||||
);*/
|
||||
const initializeDatabase = useCallback(async (vaultResponse: VaultResponse, derivedKey: string | null = null) => {
|
||||
// If the derived key is provided, store it in the keychain.
|
||||
// Otherwise we assume the encryption key is already stored in the keychain.
|
||||
if (derivedKey) {
|
||||
await sqliteClient.storeEncryptionKey(derivedKey);
|
||||
}
|
||||
|
||||
// Initialize the SQLite client.
|
||||
const client = new SqliteClient();
|
||||
await client.storeEncryptionKey(derivedKey);
|
||||
await client.storeEncryptedDatabase(vaultResponse.vault.blob);
|
||||
await sqliteClient.storeEncryptedDatabase(vaultResponse.vault.blob);
|
||||
|
||||
setSqliteClient(client);
|
||||
setDbInitialized(true);
|
||||
setDbAvailable(true);
|
||||
setPublicEmailDomains(vaultResponse.vault.publicEmailDomainList);
|
||||
@@ -83,9 +79,6 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
try {
|
||||
const isVaultInitialized = credentialManager.isVaultInitialized();
|
||||
if (isVaultInitialized) {
|
||||
// TODO: sqlclient can be initialized in constructor instead?
|
||||
const client = new SqliteClient();
|
||||
setSqliteClient(client);
|
||||
setDbInitialized(true);
|
||||
setDbAvailable(true);
|
||||
} else {
|
||||
@@ -97,7 +90,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
setDbInitialized(true);
|
||||
setDbAvailable(false);
|
||||
}
|
||||
|
||||
|
||||
/*try {
|
||||
const response = await sendMessage('GET_VAULT', {}, 'background') as messageVaultResponse;
|
||||
if (response?.vault) {
|
||||
|
||||
Reference in New Issue
Block a user