mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-19 05:47:43 -04:00
Mobile app logout flow and AppContext refactor (#1274)
This commit is contained in:
committed by
Leendert de Borst
parent
46c364bbb4
commit
e5d924a094
@@ -10,6 +10,7 @@ import Toast from 'react-native-toast-message';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
import emitter from '@/utils/EventEmitter';
|
||||
import { VaultAuthenticationError } from '@/utils/types/errors/VaultAuthenticationError';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
@@ -27,9 +28,8 @@ import { CollapsibleHeader } from '@/components/ui/CollapsibleHeader';
|
||||
import { RobustPressable } from '@/components/ui/RobustPressable';
|
||||
import { SkeletonLoader } from '@/components/ui/SkeletonLoader';
|
||||
import { TitleContainer } from '@/components/ui/TitleContainer';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useApp } from '@/context/AppContext';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
|
||||
/**
|
||||
* Credentials screen.
|
||||
@@ -37,7 +37,6 @@ import { useWebApi } from '@/context/WebApiContext';
|
||||
export default function CredentialsScreen() : React.ReactNode {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const { syncVault } = useVaultSync();
|
||||
const webApi = useWebApi();
|
||||
const colors = useColors();
|
||||
const { t } = useTranslation();
|
||||
const flatListRef = useRef<FlatList>(null);
|
||||
@@ -54,7 +53,7 @@ export default function CredentialsScreen() : React.ReactNode {
|
||||
const { executeVaultMutation, isLoading, syncStatus } = useVaultMutate();
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
|
||||
const authContext = useAuth();
|
||||
const authContext = useApp();
|
||||
const dbContext = useDb();
|
||||
|
||||
const isAuthenticated = authContext.isLoggedIn;
|
||||
@@ -172,11 +171,11 @@ export default function CredentialsScreen() : React.ReactNode {
|
||||
setRefreshing(false);
|
||||
setIsLoadingCredentials(false);
|
||||
|
||||
// Show modal with error message
|
||||
/**
|
||||
* Authentication errors are handled in useVaultSync
|
||||
* For other errors, show alert
|
||||
*/
|
||||
Alert.alert(t('common.error'), error);
|
||||
|
||||
// Logout user
|
||||
await webApi.logout(error);
|
||||
},
|
||||
/**
|
||||
* On upgrade required.
|
||||
@@ -189,13 +188,17 @@ export default function CredentialsScreen() : React.ReactNode {
|
||||
console.error('Error refreshing credentials:', err);
|
||||
setRefreshing(false);
|
||||
setIsLoadingCredentials(false);
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: t('credentials.vaultSyncFailed'),
|
||||
text2: err instanceof Error ? err.message : 'Unknown error',
|
||||
});
|
||||
|
||||
// Authentication errors are already handled in useVaultSync
|
||||
if (!(err instanceof VaultAuthenticationError)) {
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: t('credentials.vaultSyncFailed'),
|
||||
text2: err instanceof Error ? err.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [syncVault, loadCredentials, setIsLoadingCredentials, setRefreshing, webApi, authContext, router, t]);
|
||||
}, [syncVault, loadCredentials, setIsLoadingCredentials, setRefreshing, authContext, router, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated || !isDatabaseAvailable) {
|
||||
|
||||
@@ -16,18 +16,16 @@ import { CollapsibleHeader } from '@/components/ui/CollapsibleHeader';
|
||||
import { InlineSkeletonLoader } from '@/components/ui/InlineSkeletonLoader';
|
||||
import { TitleContainer } from '@/components/ui/TitleContainer';
|
||||
import { UsernameDisplay } from '@/components/ui/UsernameDisplay';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
import { useApp } from '@/context/AppContext';
|
||||
|
||||
/**
|
||||
* Settings screen.
|
||||
*/
|
||||
export default function SettingsScreen() : React.ReactNode {
|
||||
const webApi = useWebApi();
|
||||
const colors = useColors();
|
||||
const { t } = useTranslation();
|
||||
const { getAuthMethodDisplayKey, shouldShowAutofillReminder } = useAuth();
|
||||
const { getAutoLockTimeout, getClipboardClearTimeout } = useAuth();
|
||||
const { getAuthMethodDisplayKey, shouldShowAutofillReminder, logout } = useApp();
|
||||
const { getAutoLockTimeout, getClipboardClearTimeout } = useApp();
|
||||
const { loadApiUrl, getDisplayUrl } = useApiUrl();
|
||||
const scrollY = useRef(new Animated.Value(0)).current;
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
@@ -121,7 +119,7 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
* Handle the logout.
|
||||
*/
|
||||
onPress: async () : Promise<void> => {
|
||||
await webApi.logout();
|
||||
await logout();
|
||||
router.replace('/login');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ import { ThemedScrollView } from '@/components/themed/ThemedScrollView';
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { ThemedTextInput } from '@/components/themed/ThemedTextInput';
|
||||
import { UsernameDisplay } from '@/components/ui/UsernameDisplay';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useApp } from '@/context/AppContext';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
|
||||
/**
|
||||
@@ -26,7 +26,7 @@ import { useWebApi } from '@/context/WebApiContext';
|
||||
export default function DeleteAccountScreen(): React.ReactNode {
|
||||
const colors = useColors();
|
||||
const webApi = useWebApi();
|
||||
const { username, verifyPassword, logout } = useAuth();
|
||||
const { username, verifyPassword, logout } = useApp();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [confirmUsername, setConfirmUsername] = useState('');
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useColors, useColorScheme } from '@/hooks/useColorScheme';
|
||||
import SpaceMono from '@/assets/fonts/SpaceMono-Regular.ttf';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { AliasVaultToast } from '@/components/Toast';
|
||||
import { AppProvider } from '@/context/AppContext';
|
||||
import { AuthProvider } from '@/context/AuthContext';
|
||||
import { ClipboardCountdownProvider } from '@/context/ClipboardCountdownContext';
|
||||
import { DbProvider } from '@/context/DbContext';
|
||||
@@ -185,13 +186,15 @@ export default function RootLayout() : React.ReactNode {
|
||||
<DbProvider>
|
||||
<AuthProvider>
|
||||
<WebApiProvider>
|
||||
<ClipboardCountdownProvider>
|
||||
<KeyboardProvider>
|
||||
<GestureHandlerRootView>
|
||||
<RootLayoutNav />
|
||||
</GestureHandlerRootView>
|
||||
</KeyboardProvider>
|
||||
</ClipboardCountdownProvider>
|
||||
<AppProvider>
|
||||
<ClipboardCountdownProvider>
|
||||
<KeyboardProvider>
|
||||
<GestureHandlerRootView>
|
||||
<RootLayoutNav />
|
||||
</GestureHandlerRootView>
|
||||
</KeyboardProvider>
|
||||
</ClipboardCountdownProvider>
|
||||
</AppProvider>
|
||||
</WebApiProvider>
|
||||
</AuthProvider>
|
||||
</DbProvider>
|
||||
|
||||
@@ -7,10 +7,10 @@ import { useVaultSync } from '@/hooks/useVaultSync';
|
||||
|
||||
import LoadingIndicator from '@/components/LoadingIndicator';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useApp } from '@/context/AppContext';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
import NativeVaultManager from '@/specs/NativeVaultManager';
|
||||
import { VaultVersionIncompatibleError } from '@/utils/types/errors/VaultVersionError';
|
||||
|
||||
/**
|
||||
* Initialize page that handles all boot logic.
|
||||
@@ -22,10 +22,9 @@ export default function Initialize() : React.ReactNode {
|
||||
const hasInitialized = useRef(false);
|
||||
const offlineButtonTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const { t } = useTranslation();
|
||||
const { initializeAuth, setOfflineMode } = useAuth();
|
||||
const app = useApp();
|
||||
const { syncVault } = useVaultSync();
|
||||
const dbContext = useDb();
|
||||
const webApi = useWebApi();
|
||||
|
||||
/**
|
||||
* Handle offline scenario - show alert with options to open local vault or retry sync.
|
||||
@@ -42,7 +41,7 @@ export default function Initialize() : React.ReactNode {
|
||||
*/
|
||||
onPress: async () : Promise<void> => {
|
||||
setStatus(t('app.status.openingVaultReadOnly'));
|
||||
const { enabledAuthMethods } = await initializeAuth();
|
||||
const { enabledAuthMethods } = await app.initializeAuth();
|
||||
|
||||
try {
|
||||
const hasEncryptedDatabase = await NativeVaultManager.hasEncryptedDatabase();
|
||||
@@ -54,7 +53,7 @@ export default function Initialize() : React.ReactNode {
|
||||
}
|
||||
|
||||
// Set offline mode
|
||||
setOfflineMode(true);
|
||||
app.setOfflineMode(true);
|
||||
|
||||
// FaceID not enabled
|
||||
const isFaceIDEnabled = enabledAuthMethods.includes('faceid');
|
||||
@@ -86,7 +85,11 @@ export default function Initialize() : React.ReactNode {
|
||||
|
||||
// Success - navigate to credentials
|
||||
router.replace('/(tabs)/credentials');
|
||||
} catch {
|
||||
} catch (err) {
|
||||
if (err instanceof VaultVersionIncompatibleError) {
|
||||
await app.logout(t(err.message));
|
||||
return;
|
||||
}
|
||||
router.replace('/unlock');
|
||||
}
|
||||
}
|
||||
@@ -116,7 +119,7 @@ export default function Initialize() : React.ReactNode {
|
||||
}
|
||||
]
|
||||
);
|
||||
}, [dbContext, router, initializeAuth, t, setOfflineMode]);
|
||||
}, [dbContext, router, app, t]);
|
||||
|
||||
useEffect(() => {
|
||||
// Ensure this only runs once.
|
||||
@@ -134,7 +137,7 @@ export default function Initialize() : React.ReactNode {
|
||||
* Handle vault unlocking process.
|
||||
*/
|
||||
async function handleVaultUnlock() : Promise<void> {
|
||||
const { enabledAuthMethods } = await initializeAuth();
|
||||
const { enabledAuthMethods } = await app.initializeAuth();
|
||||
|
||||
try {
|
||||
const hasEncryptedDatabase = await NativeVaultManager.hasEncryptedDatabase();
|
||||
@@ -168,7 +171,12 @@ export default function Initialize() : React.ReactNode {
|
||||
router.replace('/unlock');
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
} catch (err) {
|
||||
if (err instanceof VaultVersionIncompatibleError) {
|
||||
await app.logout(t(err.message));
|
||||
return;
|
||||
}
|
||||
|
||||
router.replace('/unlock');
|
||||
return;
|
||||
}
|
||||
@@ -178,7 +186,7 @@ export default function Initialize() : React.ReactNode {
|
||||
* Initialize the app.
|
||||
*/
|
||||
const initialize = async () : Promise<void> => {
|
||||
const { isLoggedIn } = await initializeAuth();
|
||||
const { isLoggedIn } = await app.initializeAuth();
|
||||
|
||||
if (!isLoggedIn) {
|
||||
router.replace('/login');
|
||||
@@ -225,11 +233,11 @@ export default function Initialize() : React.ReactNode {
|
||||
* Handle error during vault sync.
|
||||
*/
|
||||
onError: async (error: string) => {
|
||||
// Show modal with error message
|
||||
/**
|
||||
* Authentication errors are already handled in useVaultSync
|
||||
* Show modal with error message for other errors
|
||||
*/
|
||||
Alert.alert(t('common.error'), error);
|
||||
|
||||
// The logout user and navigate to the login screen.
|
||||
await webApi.logout(error);
|
||||
router.replace('/login');
|
||||
},
|
||||
/**
|
||||
@@ -252,7 +260,7 @@ export default function Initialize() : React.ReactNode {
|
||||
clearTimeout(offlineButtonTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [dbContext, syncVault, initializeAuth, webApi, router, t, handleOfflineFlow]);
|
||||
}, [dbContext, syncVault, app, router, t, handleOfflineFlow]);
|
||||
|
||||
/**
|
||||
* Handle offline button press by calling the stored offline handler.
|
||||
|
||||
@@ -25,7 +25,7 @@ import LoadingIndicator from '@/components/LoadingIndicator';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { InAppBrowserView } from '@/components/ui/InAppBrowserView';
|
||||
import { RobustPressable } from '@/components/ui/RobustPressable';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useApp } from '@/context/AppContext';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function LoginScreen() : React.ReactNode {
|
||||
const [loginStatus, setLoginStatus] = useState<string | null>(null);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const authContext = useAuth();
|
||||
const authContext = useApp();
|
||||
const dbContext = useDb();
|
||||
const webApi = useWebApi();
|
||||
const { syncVault } = useVaultSync();
|
||||
@@ -192,7 +192,7 @@ export default function LoginScreen() : React.ReactNode {
|
||||
|
||||
// Show modal with error message
|
||||
Alert.alert(t('common.error'), message);
|
||||
webApi.logout(message);
|
||||
// Error will trigger logout through the sync process
|
||||
setIsLoading(false);
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -9,16 +9,17 @@ import { useVaultSync } from '@/hooks/useVaultSync';
|
||||
import LoadingIndicator from '@/components/LoadingIndicator';
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useApp } from '@/context/AppContext';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import NativeVaultManager from '@/specs/NativeVaultManager';
|
||||
import { VaultVersionIncompatibleError } from '@/utils/types/errors/VaultVersionError';
|
||||
|
||||
/**
|
||||
* Reinitialize screen which is triggered when the app was still open but the database in memory
|
||||
* was cleared because of a time-out. When this happens, we need to re-initialize and unlock the vault.
|
||||
*/
|
||||
export default function ReinitializeScreen() : React.ReactNode {
|
||||
const authContext = useAuth();
|
||||
const authContext = useApp();
|
||||
const dbContext = useDb();
|
||||
const { syncVault } = useVaultSync();
|
||||
const [status, setStatus] = useState('');
|
||||
@@ -27,6 +28,7 @@ export default function ReinitializeScreen() : React.ReactNode {
|
||||
const offlineButtonTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const colors = useColors();
|
||||
const { t } = useTranslation();
|
||||
const app = useApp();
|
||||
|
||||
/**
|
||||
* Handle offline scenario - show alert with options to open local vault or retry sync.
|
||||
@@ -43,7 +45,7 @@ export default function ReinitializeScreen() : React.ReactNode {
|
||||
*/
|
||||
onPress: async () : Promise<void> => {
|
||||
setStatus(t('app.status.openingVaultReadOnly'));
|
||||
const { enabledAuthMethods } = await authContext.initializeAuth();
|
||||
const { enabledAuthMethods } = await app.initializeAuth();
|
||||
|
||||
try {
|
||||
const hasEncryptedDatabase = await NativeVaultManager.hasEncryptedDatabase();
|
||||
@@ -117,7 +119,12 @@ export default function ReinitializeScreen() : React.ReactNode {
|
||||
}
|
||||
}, 0);
|
||||
authContext.setReturnUrl(null);
|
||||
} catch {
|
||||
} catch (err) {
|
||||
if (err instanceof VaultVersionIncompatibleError) {
|
||||
await authContext.logout(t(err.message));
|
||||
return;
|
||||
}
|
||||
|
||||
router.replace('/unlock');
|
||||
}
|
||||
}
|
||||
@@ -203,7 +210,7 @@ export default function ReinitializeScreen() : React.ReactNode {
|
||||
* Handle vault unlocking process.
|
||||
*/
|
||||
async function handleVaultUnlock() : Promise<void> {
|
||||
const { enabledAuthMethods } = await authContext.initializeAuth();
|
||||
const { enabledAuthMethods } = await app.initializeAuth();
|
||||
|
||||
try {
|
||||
const hasEncryptedDatabase = await NativeVaultManager.hasEncryptedDatabase();
|
||||
@@ -233,7 +240,12 @@ export default function ReinitializeScreen() : React.ReactNode {
|
||||
}
|
||||
|
||||
router.replace('/unlock');
|
||||
} catch {
|
||||
} catch (err) {
|
||||
if (err instanceof VaultVersionIncompatibleError) {
|
||||
await authContext.logout(t(err.message));
|
||||
return;
|
||||
}
|
||||
|
||||
router.replace('/unlock');
|
||||
}
|
||||
}
|
||||
@@ -242,7 +254,7 @@ export default function ReinitializeScreen() : React.ReactNode {
|
||||
* Initialize the app.
|
||||
*/
|
||||
const initialize = async () : Promise<void> => {
|
||||
const { isLoggedIn } = await authContext.initializeAuth();
|
||||
const { isLoggedIn } = await app.initializeAuth();
|
||||
|
||||
// If user is not logged in, navigate to login immediately
|
||||
if (!isLoggedIn) {
|
||||
@@ -285,6 +297,15 @@ export default function ReinitializeScreen() : React.ReactNode {
|
||||
onSuccess: async () => {
|
||||
await handleVaultUnlock();
|
||||
},
|
||||
/**
|
||||
* Handle error during vault sync.
|
||||
* Authentication errors are already handled in useVaultSync.
|
||||
*/
|
||||
onError: (error: string) => {
|
||||
console.error('Vault sync error during reinitialize:', error);
|
||||
// Even if sync fails, try to continue with local vault if available
|
||||
handleVaultUnlock();
|
||||
},
|
||||
/**
|
||||
* Handle offline state and prompt user for action.
|
||||
*/
|
||||
|
||||
@@ -17,22 +17,21 @@ import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { Avatar } from '@/components/ui/Avatar';
|
||||
import { RobustPressable } from '@/components/ui/RobustPressable';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useApp } from '@/context/AppContext';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
import { VaultVersionIncompatibleError } from '@/utils/types/errors/VaultVersionError';
|
||||
|
||||
/**
|
||||
* Unlock screen.
|
||||
*/
|
||||
export default function UnlockScreen() : React.ReactNode {
|
||||
const { isLoggedIn, username, isBiometricsEnabled, getBiometricDisplayNameKey, getEncryptionKeyDerivationParams } = useAuth();
|
||||
const { isLoggedIn, username, isBiometricsEnabled, getBiometricDisplayNameKey, getEncryptionKeyDerivationParams, logout } = useApp();
|
||||
const dbContext = useDb();
|
||||
const [password, setPassword] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isBiometricsAvailable, setIsBiometricsAvailable] = useState(false);
|
||||
const colors = useColors();
|
||||
const { t } = useTranslation();
|
||||
const webApi = useWebApi();
|
||||
const [biometricDisplayName, setBiometricDisplayName] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
@@ -43,12 +42,12 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
const getKeyDerivationParams = useCallback(async () : Promise<{ salt: string; encryptionType: string; encryptionSettings: string } | null> => {
|
||||
const params = await getEncryptionKeyDerivationParams();
|
||||
if (!params) {
|
||||
await webApi.logout();
|
||||
await logout();
|
||||
router.replace('/login');
|
||||
return null;
|
||||
}
|
||||
return params;
|
||||
}, [webApi, getEncryptionKeyDerivationParams]);
|
||||
}, [logout, getEncryptionKeyDerivationParams]);
|
||||
|
||||
useEffect(() => {
|
||||
getKeyDerivationParams();
|
||||
@@ -114,6 +113,11 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
Alert.alert(t('common.error'), t('auth.errors.incorrectPassword'));
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof VaultVersionIncompatibleError) {
|
||||
await logout(t(error.message));
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('Unlock error:', error);
|
||||
Alert.alert(t('common.error'), t('auth.errors.incorrectPassword'));
|
||||
} finally {
|
||||
@@ -129,7 +133,7 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
* Clear any stored tokens or session data
|
||||
* This will be handled by the auth context
|
||||
*/
|
||||
await webApi.logout();
|
||||
await logout();
|
||||
router.replace('/login');
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { Avatar } from '@/components/ui/Avatar';
|
||||
import { RobustPressable } from '@/components/ui/RobustPressable';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useApp } from '@/context/AppContext';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
import NativeVaultManager from '@/specs/NativeVaultManager';
|
||||
@@ -26,7 +26,8 @@ import NativeVaultManager from '@/specs/NativeVaultManager';
|
||||
* Upgrade screen.
|
||||
*/
|
||||
export default function UpgradeScreen() : React.ReactNode {
|
||||
const { username } = useAuth();
|
||||
const { username, logout } = useApp();
|
||||
const webApi = useWebApi();
|
||||
const { sqliteClient } = useDb();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentVersion, setCurrentVersion] = useState<VaultVersion | null>(null);
|
||||
@@ -34,7 +35,6 @@ export default function UpgradeScreen() : React.ReactNode {
|
||||
const [upgradeStatus, setUpgradeStatus] = useState('');
|
||||
const colors = useColors();
|
||||
const { t } = useTranslation();
|
||||
const webApi = useWebApi();
|
||||
const { executeVaultMutation, isLoading: isVaultMutationLoading, syncStatus } = useVaultMutate();
|
||||
const { syncVault } = useVaultSync();
|
||||
|
||||
@@ -217,7 +217,7 @@ export default function UpgradeScreen() : React.ReactNode {
|
||||
* Clear any stored tokens or session data
|
||||
* This will be handled by the auth context
|
||||
*/
|
||||
await webApi.logout();
|
||||
await logout();
|
||||
router.replace('/login');
|
||||
};
|
||||
|
||||
|
||||
162
apps/mobile-app/context/AppContext.tsx
Normal file
162
apps/mobile-app/context/AppContext.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import React, { createContext, useContext, useMemo, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
|
||||
import { logoutEventEmitter } from '@/events/LogoutEventEmitter';
|
||||
|
||||
import i18n from '@/i18n';
|
||||
|
||||
type AppContextType = {
|
||||
isLoggedIn: boolean;
|
||||
isInitialized: boolean;
|
||||
username: string | null;
|
||||
isOffline: boolean;
|
||||
logout: (errorMessage?: string) => Promise<void>;
|
||||
initializeAuth: () => Promise<{ isLoggedIn: boolean; enabledAuthMethods: AuthMethod[] }>;
|
||||
setAuthTokens: (username: string, accessToken: string, refreshToken: string) => Promise<void>;
|
||||
login: () => Promise<void>;
|
||||
isLoggingOut: boolean;
|
||||
// Auth methods from AuthContext
|
||||
getEnabledAuthMethods: () => Promise<AuthMethod[]>;
|
||||
isBiometricsEnabled: () => Promise<boolean>;
|
||||
setAuthMethods: (methods: AuthMethod[]) => Promise<void>;
|
||||
getAuthMethodDisplayKey: () => Promise<string>;
|
||||
getAutoLockTimeout: () => Promise<number>;
|
||||
setAutoLockTimeout: (timeout: number) => Promise<void>;
|
||||
getClipboardClearTimeout: () => Promise<number>;
|
||||
setClipboardClearTimeout: (timeout: number) => Promise<void>;
|
||||
getBiometricDisplayNameKey: () => Promise<string>;
|
||||
isBiometricsEnabledOnDevice: () => Promise<boolean>;
|
||||
setOfflineMode: (isOffline: boolean) => void;
|
||||
verifyPassword: (password: string) => Promise<string | null>;
|
||||
getEncryptionKeyDerivationParams: () => Promise<{ salt: string; encryptionType: string; encryptionSettings: string } | null>;
|
||||
// Autofill methods
|
||||
shouldShowAutofillReminder: boolean;
|
||||
markAutofillConfigured: () => Promise<void>;
|
||||
// Return URL methods
|
||||
returnUrl: { path: string; params?: object } | null;
|
||||
setReturnUrl: (url: { path: string; params?: object } | null) => void;
|
||||
}
|
||||
|
||||
export type AuthMethod = 'faceid' | 'password';
|
||||
|
||||
const AppContext = createContext<AppContextType | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* AppProvider that coordinates between auth, db, and webApi contexts.
|
||||
*/
|
||||
export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const auth = useAuth();
|
||||
const webApi = useWebApi();
|
||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||
|
||||
/**
|
||||
* Logout the user by revoking tokens and clearing the auth tokens from storage.
|
||||
* Prevents recursive logout calls by tracking logout state.
|
||||
*/
|
||||
const logout = useCallback(async (errorMessage?: string): Promise<void> => {
|
||||
// Prevent recursive logout calls
|
||||
if (isLoggingOut) {
|
||||
console.debug('Logout already in progress, ignoring duplicate call');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoggingOut(true);
|
||||
await webApi.revokeTokens();
|
||||
await auth.clearAuth(errorMessage);
|
||||
} catch (error) {
|
||||
console.error('Error during logout:', error);
|
||||
} finally {
|
||||
setIsLoggingOut(false);
|
||||
}
|
||||
}, [auth, webApi, isLoggingOut]);
|
||||
|
||||
/**
|
||||
* Subscribe to logout events from WebApiService.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const unsubscribe = logoutEventEmitter.subscribe(async (errorKey: string) => {
|
||||
await logout(i18n.t(errorKey));
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [logout]);
|
||||
|
||||
const contextValue = useMemo(() => ({
|
||||
// Pass through auth state
|
||||
isInitialized: auth.isInitialized,
|
||||
isLoggedIn: auth.isLoggedIn,
|
||||
username: auth.username,
|
||||
isOffline: auth.isOffline,
|
||||
shouldShowAutofillReminder: auth.shouldShowAutofillReminder,
|
||||
returnUrl: auth.returnUrl,
|
||||
// Wrap auth methods
|
||||
logout,
|
||||
initializeAuth: auth.initializeAuth,
|
||||
setAuthTokens: auth.setAuthTokens,
|
||||
login: auth.login,
|
||||
isLoggingOut,
|
||||
// Pass through other auth methods
|
||||
getEnabledAuthMethods: auth.getEnabledAuthMethods,
|
||||
isBiometricsEnabled: auth.isBiometricsEnabled,
|
||||
setAuthMethods: auth.setAuthMethods,
|
||||
getAuthMethodDisplayKey: auth.getAuthMethodDisplayKey,
|
||||
getAutoLockTimeout: auth.getAutoLockTimeout,
|
||||
setAutoLockTimeout: auth.setAutoLockTimeout,
|
||||
getClipboardClearTimeout: auth.getClipboardClearTimeout,
|
||||
setClipboardClearTimeout: auth.setClipboardClearTimeout,
|
||||
getBiometricDisplayNameKey: auth.getBiometricDisplayNameKey,
|
||||
isBiometricsEnabledOnDevice: auth.isBiometricsEnabledOnDevice,
|
||||
setOfflineMode: auth.setOfflineMode,
|
||||
verifyPassword: auth.verifyPassword,
|
||||
getEncryptionKeyDerivationParams: auth.getEncryptionKeyDerivationParams,
|
||||
markAutofillConfigured: auth.markAutofillConfigured,
|
||||
setReturnUrl: auth.setReturnUrl,
|
||||
}), [
|
||||
auth.isInitialized,
|
||||
auth.isLoggedIn,
|
||||
auth.username,
|
||||
auth.isOffline,
|
||||
auth.shouldShowAutofillReminder,
|
||||
auth.returnUrl,
|
||||
auth.initializeAuth,
|
||||
auth.setAuthTokens,
|
||||
auth.login,
|
||||
auth.getEnabledAuthMethods,
|
||||
auth.isBiometricsEnabled,
|
||||
auth.setAuthMethods,
|
||||
auth.getAuthMethodDisplayKey,
|
||||
auth.getAutoLockTimeout,
|
||||
auth.setAutoLockTimeout,
|
||||
auth.getClipboardClearTimeout,
|
||||
auth.setClipboardClearTimeout,
|
||||
auth.getBiometricDisplayNameKey,
|
||||
auth.isBiometricsEnabledOnDevice,
|
||||
auth.setOfflineMode,
|
||||
auth.verifyPassword,
|
||||
auth.getEncryptionKeyDerivationParams,
|
||||
auth.markAutofillConfigured,
|
||||
auth.setReturnUrl,
|
||||
logout,
|
||||
isLoggingOut,
|
||||
]);
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to use the AppContext.
|
||||
*/
|
||||
export const useApp = (): AppContextType => {
|
||||
const context = useContext(AppContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useApp must be used within an AppProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import NativeVaultManager from '@/specs/NativeVaultManager';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
// Create a navigation reference
|
||||
export const navigationRef = React.createRef<NavigationContainerRef<ParamListBase>>();
|
||||
@@ -27,7 +28,7 @@ type AuthContextType = {
|
||||
setAuthTokens: (username: string, accessToken: string, refreshToken: string) => Promise<void>;
|
||||
initializeAuth: () => Promise<{ isLoggedIn: boolean; enabledAuthMethods: AuthMethod[] }>;
|
||||
login: () => Promise<void>;
|
||||
logout: (errorMessage?: string) => Promise<void>;
|
||||
clearAuth: (errorMessage?: string) => Promise<void>;
|
||||
setAuthMethods: (methods: AuthMethod[]) => Promise<void>;
|
||||
getAuthMethodDisplayKey: () => Promise<string>;
|
||||
getAutoLockTimeout: () => Promise<number>;
|
||||
@@ -161,9 +162,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Logout the user and clear the auth tokens from chrome storage.
|
||||
* Clear authentication data and tokens from storage.
|
||||
* This is called by AppContext after revoking tokens on the server.
|
||||
*/
|
||||
const logout = useCallback(async (errorMessage?: string): Promise<void> => {
|
||||
const clearAuth = useCallback(async (errorMessage?: string): Promise<void> => {
|
||||
await AsyncStorage.removeItem('username');
|
||||
await AsyncStorage.removeItem('accessToken');
|
||||
await AsyncStorage.removeItem('refreshToken');
|
||||
@@ -172,7 +174,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
|
||||
if (errorMessage) {
|
||||
// Show alert
|
||||
Alert.alert(errorMessage);
|
||||
Alert.alert(i18n.t('common.error'), errorMessage);
|
||||
}
|
||||
|
||||
setUsername(null);
|
||||
@@ -429,7 +431,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
setAuthTokens,
|
||||
initializeAuth,
|
||||
login,
|
||||
logout,
|
||||
clearAuth,
|
||||
setAuthMethods,
|
||||
getAuthMethodDisplayKey,
|
||||
isBiometricsEnabledOnDevice,
|
||||
@@ -455,7 +457,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
setAuthTokens,
|
||||
initializeAuth,
|
||||
login,
|
||||
logout,
|
||||
clearAuth,
|
||||
setAuthMethods,
|
||||
getAuthMethodDisplayKey,
|
||||
isBiometricsEnabledOnDevice,
|
||||
|
||||
@@ -166,27 +166,22 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
* @returns true if the database is working, false otherwise
|
||||
*/
|
||||
const testDatabaseConnection = useCallback(async (derivedKey: string): Promise<boolean> => {
|
||||
try {
|
||||
// Store the encryption key
|
||||
await sqliteClient.storeEncryptionKey(derivedKey);
|
||||
// Store the encryption key
|
||||
await sqliteClient.storeEncryptionKey(derivedKey);
|
||||
|
||||
// Initialize the database
|
||||
const unlocked = await unlockVault();
|
||||
if (!unlocked) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to get the database version as a simple test query
|
||||
const version = await sqliteClient.getDatabaseVersion();
|
||||
if (version && version.version && version.version.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch {
|
||||
// Error testing database connection, return false
|
||||
// Initialize the database
|
||||
const unlocked = await unlockVault();
|
||||
if (!unlocked) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to get the database version as a simple test query
|
||||
const version = await sqliteClient.getDatabaseVersion();
|
||||
if (version && version.version && version.version.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [sqliteClient, unlockVault]);
|
||||
|
||||
const contextValue = useMemo(() => ({
|
||||
|
||||
@@ -2,32 +2,21 @@ import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { WebApiService } from '@/utils/WebApiService';
|
||||
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
|
||||
const WebApiContext = createContext<WebApiService | null>(null);
|
||||
|
||||
/**
|
||||
* WebApiProvider to provide the WebApiService to the app that components can use.
|
||||
*/
|
||||
export const WebApiProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { logout } = useAuth();
|
||||
const [webApiService, setWebApiService] = useState<WebApiService | null>(null);
|
||||
|
||||
/**
|
||||
* Initialize WebApiService
|
||||
*/
|
||||
useEffect(() : void => {
|
||||
const service = new WebApiService(
|
||||
(statusError: string | null) => {
|
||||
if (statusError) {
|
||||
logout(statusError);
|
||||
} else {
|
||||
logout();
|
||||
}
|
||||
}
|
||||
);
|
||||
const service = new WebApiService();
|
||||
setWebApiService(service);
|
||||
}, [logout]);
|
||||
}, []);
|
||||
|
||||
if (!webApiService) {
|
||||
return null;
|
||||
|
||||
38
apps/mobile-app/events/LogoutEventEmitter.ts
Normal file
38
apps/mobile-app/events/LogoutEventEmitter.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
type LogoutListener = (errorMessage: string) => void | Promise<void>;
|
||||
|
||||
/**
|
||||
* Simple event emitter for logout events to avoid circular dependencies
|
||||
* between WebApiService and Auth contexts.
|
||||
*/
|
||||
class LogoutEventEmitter {
|
||||
private listeners: Set<LogoutListener> = new Set();
|
||||
|
||||
/**
|
||||
* Subscribe to logout events.
|
||||
* Returns an unsubscribe function.
|
||||
*/
|
||||
public subscribe(listener: LogoutListener): () => void {
|
||||
this.listeners.add(listener);
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a logout event to all listeners.
|
||||
*
|
||||
* @param errorTranslationKey - The translation key of the error message to emit.
|
||||
*/
|
||||
public emit(errorTranslationKey: string): void {
|
||||
this.listeners.forEach(listener => {
|
||||
try {
|
||||
listener(errorTranslationKey);
|
||||
} catch (error) {
|
||||
console.error('Error in logout listener:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const logoutEventEmitter = new LogoutEventEmitter();
|
||||
@@ -11,7 +11,7 @@ import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
|
||||
import { useVaultSync } from '@/hooks/useVaultSync';
|
||||
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useApp } from '@/context/AppContext';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
import NativeVaultManager from '@/specs/NativeVaultManager';
|
||||
@@ -39,7 +39,7 @@ export function useVaultMutate() : {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [syncStatus, setSyncStatus] = useState(t('vault.syncingVault'));
|
||||
const authContext = useAuth();
|
||||
const authContext = useApp();
|
||||
const dbContext = useDb();
|
||||
const webApi = useWebApi();
|
||||
const { syncVault } = useVaultSync();
|
||||
@@ -201,7 +201,7 @@ export function useVaultMutate() : {
|
||||
await NativeVaultManager.unlockVault();
|
||||
} catch {
|
||||
// If any part of this fails, we need logout the user as the local vault and stored encryption key are now potentially corrupt.
|
||||
authContext.logout(t('vault.errors.errorDuringPasswordChange'));
|
||||
await authContext.logout(t('vault.errors.errorDuringPasswordChange'));
|
||||
}
|
||||
|
||||
// Generate SRP password change data
|
||||
|
||||
@@ -2,13 +2,15 @@ import { useCallback } from 'react';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
import type { VaultResponse } from '@/utils/dist/shared/models/webapi';
|
||||
import { VaultAuthenticationError } from '@/utils/types/errors/VaultAuthenticationError';
|
||||
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useApp } from '@/context/AppContext';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { useWebApi } from '@/context/WebApiContext';
|
||||
import NativeVaultManager from '@/specs/NativeVaultManager';
|
||||
import { VaultVersionIncompatibleError } from '@/utils/types/errors/VaultVersionError';
|
||||
|
||||
/**
|
||||
* Utility function to ensure a minimum time has elapsed for an operation
|
||||
@@ -50,7 +52,7 @@ export const useVaultSync = () : {
|
||||
syncVault: (options?: VaultSyncOptions) => Promise<boolean>;
|
||||
} => {
|
||||
const { t } = useTranslation();
|
||||
const authContext = useAuth();
|
||||
const app = useApp();
|
||||
const dbContext = useDb();
|
||||
const webApi = useWebApi();
|
||||
|
||||
@@ -61,7 +63,7 @@ export const useVaultSync = () : {
|
||||
const enableDelay = initialSync;
|
||||
|
||||
try {
|
||||
const { isLoggedIn } = await authContext.initializeAuth();
|
||||
const { isLoggedIn } = await app.initializeAuth();
|
||||
|
||||
if (!isLoggedIn) {
|
||||
// Not authenticated, return false immediately
|
||||
@@ -91,7 +93,7 @@ export const useVaultSync = () : {
|
||||
}
|
||||
|
||||
// Check if the SRP salt has changed compared to locally stored encryption key derivation params
|
||||
const keyDerivationParams = await authContext.getEncryptionKeyDerivationParams();
|
||||
const keyDerivationParams = await app.getEncryptionKeyDerivationParams();
|
||||
if (keyDerivationParams && statusResponse.srpSalt && statusResponse.srpSalt !== keyDerivationParams.salt) {
|
||||
/**
|
||||
* Server SRP salt has changed compared to locally stored value, which means the user has changed
|
||||
@@ -99,12 +101,12 @@ export const useVaultSync = () : {
|
||||
* longer valid and the user needs to re-authenticate. We trigger a logout but do not revoke tokens
|
||||
* as these were already revoked by the server upon password change.
|
||||
*/
|
||||
await webApi.logout(t('vault.errors.passwordChanged'), false);
|
||||
await app.logout(t('vault.errors.passwordChanged'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we get here, it means we have a valid connection to the server.
|
||||
authContext.setOfflineMode(false);
|
||||
app.setOfflineMode(false);
|
||||
|
||||
// Compare vault revisions
|
||||
const vaultMetadata = await dbContext.getVaultMetadata();
|
||||
@@ -116,16 +118,8 @@ export const useVaultSync = () : {
|
||||
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson as VaultResponse);
|
||||
if (vaultError) {
|
||||
// Only logout if it's an authentication error, not a network error
|
||||
if (vaultError.includes('authentication') || vaultError.includes('unauthorized')) {
|
||||
await webApi.logout(vaultError);
|
||||
onError?.(vaultError);
|
||||
return false;
|
||||
}
|
||||
|
||||
// For other errors, go into offline mode
|
||||
authContext.setOfflineMode(true);
|
||||
return true;
|
||||
// Throw authentication error which will be caught and handled
|
||||
throw new VaultAuthenticationError(vaultError);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -139,7 +133,12 @@ export const useVaultSync = () : {
|
||||
|
||||
onSuccess?.(true);
|
||||
return true;
|
||||
} catch {
|
||||
} catch (err) {
|
||||
if (err instanceof VaultVersionIncompatibleError) {
|
||||
await app.logout(t(err.message));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vault could not be decrypted, throw an error
|
||||
throw new Error(t('vault.errors.vaultDecryptFailed'));
|
||||
}
|
||||
@@ -154,19 +153,31 @@ export const useVaultSync = () : {
|
||||
await withMinimumDelay(() => Promise.resolve(onSuccess?.(false)), 300, enableDelay);
|
||||
return false;
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : t('common.errors.unknownError');
|
||||
console.error('Vault sync error:', err);
|
||||
|
||||
// Handle authentication errors
|
||||
if (err instanceof VaultAuthenticationError) {
|
||||
await app.logout(err.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (err instanceof VaultVersionIncompatibleError) {
|
||||
await app.logout(t(err.message));
|
||||
return false;
|
||||
}
|
||||
|
||||
const errorMessage = err instanceof Error ? err.message : t('common.errors.unknownError');
|
||||
|
||||
// Check if it's a network error
|
||||
if (errorMessage.includes('network') || errorMessage.includes('timeout')) {
|
||||
authContext.setOfflineMode(true);
|
||||
app.setOfflineMode(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
onError?.(errorMessage);
|
||||
return false;
|
||||
}
|
||||
}, [authContext, dbContext, webApi, t]);
|
||||
}, [app, dbContext, webApi, t]);
|
||||
|
||||
return { syncVault };
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { Buffer } from 'buffer';
|
||||
import type { EncryptionKeyDerivationParams, VaultMetadata } from '@/utils/dist/shared/models/metadata';
|
||||
import type { Attachment, Credential, EncryptionKey, PasswordSettings, TotpCode } from '@/utils/dist/shared/models/vault';
|
||||
import { VaultSqlGenerator, VaultVersion } from '@/utils/dist/shared/vault-sql';
|
||||
import { VaultVersionIncompatibleError } from '@/utils/types/errors/VaultVersionError';
|
||||
|
||||
import NativeVaultManager from '@/specs/NativeVaultManager';
|
||||
|
||||
@@ -625,43 +626,38 @@ class SqliteClient {
|
||||
* Returns null if no matching version is found.
|
||||
*/
|
||||
public async getDatabaseVersion(): Promise<VaultVersion> {
|
||||
try {
|
||||
let currentVersion = '';
|
||||
let currentVersion = '';
|
||||
|
||||
// Query the migrations history table for the latest migration
|
||||
const results = await this.executeQuery<{ MigrationId: string }>(`
|
||||
SELECT MigrationId
|
||||
FROM __EFMigrationsHistory
|
||||
ORDER BY MigrationId DESC
|
||||
LIMIT 1`);
|
||||
// Query the migrations history table for the latest migration
|
||||
const results = await this.executeQuery<{ MigrationId: string }>(`
|
||||
SELECT MigrationId
|
||||
FROM __EFMigrationsHistory
|
||||
ORDER BY MigrationId DESC
|
||||
LIMIT 1`);
|
||||
|
||||
if (results.length === 0) {
|
||||
throw new Error('No migrations found');
|
||||
}
|
||||
|
||||
// Extract version using regex - matches patterns like "20240917191243_1.4.1-RenameAttachmentsPlural"
|
||||
const migrationId = results[0].MigrationId;
|
||||
const versionRegex = /_(\d+\.\d+\.\d+)-/;
|
||||
const versionMatch = versionRegex.exec(migrationId);
|
||||
|
||||
if (versionMatch?.[1]) {
|
||||
currentVersion = versionMatch[1];
|
||||
}
|
||||
|
||||
// Get all available vault versions to get the revision number of the current version.
|
||||
const vaultSqlGenerator = new VaultSqlGenerator();
|
||||
const allVersions = vaultSqlGenerator.getAllVersions();
|
||||
const currentVersionRevision = allVersions.find(v => v.version === currentVersion);
|
||||
|
||||
if (!currentVersionRevision) {
|
||||
throw new Error(`This app is outdated and cannot be used to access this vault. Please update this app to continue.`);
|
||||
}
|
||||
|
||||
return currentVersionRevision;
|
||||
} catch (error) {
|
||||
console.error('Error getting database version:', error);
|
||||
throw error;
|
||||
if (results.length === 0) {
|
||||
throw new Error('No migrations found');
|
||||
}
|
||||
|
||||
// Extract version using regex - matches patterns like "20240917191243_1.4.1-RenameAttachmentsPlural"
|
||||
const migrationId = results[0].MigrationId;
|
||||
const versionRegex = /_(\d+\.\d+\.\d+)-/;
|
||||
const versionMatch = versionRegex.exec(migrationId);
|
||||
|
||||
if (versionMatch?.[1]) {
|
||||
currentVersion = versionMatch[1];
|
||||
}
|
||||
|
||||
// Get all available vault versions to get the revision number of the current version.
|
||||
const vaultSqlGenerator = new VaultSqlGenerator();
|
||||
const allVersions = vaultSqlGenerator.getAllVersions();
|
||||
const currentVersionRevision = allVersions.find(v => v.version === currentVersion);
|
||||
|
||||
if (!currentVersionRevision) {
|
||||
throw new VaultVersionIncompatibleError('vault.errors.appOutdated');
|
||||
}
|
||||
|
||||
return currentVersionRevision;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { StatusResponse, VaultResponse, AuthLogModel, RefreshToken } from '
|
||||
import i18n from '@/i18n';
|
||||
|
||||
import { LocalAuthError } from './types/errors/LocalAuthError';
|
||||
import { logoutEventEmitter } from '@/events/LogoutEventEmitter';
|
||||
|
||||
type RequestInit = globalThis.RequestInit;
|
||||
|
||||
@@ -23,10 +24,8 @@ type TokenResponse = {
|
||||
export class WebApiService {
|
||||
/**
|
||||
* Constructor for the WebApiService class.
|
||||
*
|
||||
* @param {Function} authContextLogout - Function to handle logout.
|
||||
*/
|
||||
public constructor(private readonly authContextLogout: (statusError: string | null) => void) { }
|
||||
public constructor() { }
|
||||
|
||||
/**
|
||||
* Get the base URL for the API from settings.
|
||||
@@ -86,7 +85,7 @@ export class WebApiService {
|
||||
|
||||
return parseJson ? retryResponse.json() : retryResponse as unknown as T;
|
||||
} else {
|
||||
this.authContextLogout(null);
|
||||
logoutEventEmitter.emit('auth.errors.sessionExpired');
|
||||
throw new Error(i18n.t('auth.errors.sessionExpired'));
|
||||
}
|
||||
}
|
||||
@@ -188,7 +187,7 @@ export class WebApiService {
|
||||
this.updateTokens(tokenResponse.token, tokenResponse.refreshToken);
|
||||
return tokenResponse.token;
|
||||
} catch {
|
||||
this.authContextLogout(i18n.t('auth.errors.sessionExpired'));
|
||||
logoutEventEmitter.emit('auth.errors.sessionExpired');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -259,29 +258,21 @@ export class WebApiService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout and revoke tokens via WebApi and remove local storage tokens via AuthContext.
|
||||
* Revoke tokens via WebApi called when logging out.
|
||||
*/
|
||||
public async logout(statusError: string | null = null, revokeTokens: boolean = true): Promise<void> {
|
||||
// Logout and revoke tokens via WebApi.
|
||||
public async revokeTokens(): Promise<void> {
|
||||
// Revoke tokens via WebApi.
|
||||
try {
|
||||
if (revokeTokens) {
|
||||
const refreshToken = await this.getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We do not await this as we want to continue with the logout even if the revoke fails or takes a long time.
|
||||
this.post('Auth/revoke', {
|
||||
const refreshToken = await this.getRefreshToken();
|
||||
if (refreshToken) {
|
||||
await this.post('Auth/revoke', {
|
||||
token: await this.getAccessToken(),
|
||||
refreshToken: refreshToken,
|
||||
}, false);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('WebApi logout error:', err);
|
||||
console.error('WebApi revoke tokens error:', err);
|
||||
}
|
||||
|
||||
// Logout and remove tokens from local storage via AuthContext.
|
||||
this.authContextLogout(statusError);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -296,7 +287,7 @@ export class WebApiService {
|
||||
* If session expired, logout the user immediately as otherwise this would
|
||||
* trigger a server offline banner.
|
||||
*/
|
||||
this.authContextLogout(error.message);
|
||||
logoutEventEmitter.emit('auth.errors.sessionExpired');
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -307,7 +298,8 @@ export class WebApiService {
|
||||
return {
|
||||
clientVersionSupported: true,
|
||||
serverVersion: '0.0.0',
|
||||
vaultRevision: 0
|
||||
vaultRevision: 0,
|
||||
srpSalt: ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Custom error class for vault authentication errors.
|
||||
* Thrown when the vault sync fails due to authentication issues.
|
||||
*/
|
||||
export class VaultAuthenticationError extends Error {
|
||||
/**
|
||||
* Creates a new instance of VaultAuthenticationError.
|
||||
*
|
||||
* @param message - The error message (translation key or translated message).
|
||||
*/
|
||||
public constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'VaultAuthenticationError';
|
||||
|
||||
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, VaultAuthenticationError);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
apps/mobile-app/utils/types/errors/VaultVersionError.ts
Normal file
19
apps/mobile-app/utils/types/errors/VaultVersionError.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Custom error class for vault version incompatibility issues.
|
||||
* Thrown when the mobile app is outdated and cannot handle the vault version.
|
||||
*/
|
||||
export class VaultVersionIncompatibleError extends Error {
|
||||
/**
|
||||
* Creates a new instance of the VaultVersionIncompatibleError class.
|
||||
* @param message - The error message.
|
||||
*/
|
||||
public constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'VaultVersionIncompatibleError';
|
||||
|
||||
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, VaultVersionIncompatibleError);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user