Add onUpgradeRequired and executeRaw logic to iOS (#957)

This commit is contained in:
Leendert de Borst
2025-06-23 22:11:48 +02:00
committed by Leendert de Borst
parent 925455b5d6
commit b4c4603868
13 changed files with 619 additions and 12 deletions

View File

@@ -175,6 +175,12 @@ export default function CredentialsScreen() : React.ReactNode {
// Logout user
await webApi.logout(error);
},
/**
* On upgrade required.
*/
onUpgradeRequired: () : void => {
router.replace('/upgrade');
},
});
} catch (err) {
console.error('Error refreshing credentials:', err);
@@ -186,7 +192,7 @@ export default function CredentialsScreen() : React.ReactNode {
text2: err instanceof Error ? err.message : 'Unknown error',
});
}
}, [syncVault, loadCredentials, setIsLoadingCredentials, setRefreshing, webApi, authContext]);
}, [syncVault, loadCredentials, setIsLoadingCredentials, setRefreshing, webApi, authContext, router]);
useEffect(() => {
if (!isAuthenticated || !isDatabaseAvailable) {

View File

@@ -160,7 +160,14 @@ function RootLayoutNav() : React.ReactNode {
await webApi.logout(error);
setRedirectTarget('/login');
setBootComplete(true);
}
},
/**
* On upgrade required.
*/
onUpgradeRequired: () : void => {
setRedirectTarget('/upgrade');
setBootComplete(true);
},
});
};
@@ -262,6 +269,7 @@ function RootLayoutNav() : React.ReactNode {
<Stack.Screen name="login-settings" options={{ title: 'Login Settings' }} />
<Stack.Screen name="reinitialize" options={{ headerShown: false }} />
<Stack.Screen name="unlock" options={{ headerShown: false }} />
<Stack.Screen name="upgrade" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" options={{ title: 'Not Found' }} />
</Stack>

View File

@@ -192,6 +192,7 @@ export default function LoginScreen() : React.ReactNode {
await dbContext.storeEncryptionKeyDerivationParams(encryptionKeyDerivationParams);
await dbContext.initializeDatabase(vaultResponseJson);
let checkSuccess = true;
/**
* After setting auth tokens, execute a server status check immediately
* which takes care of certain sanity checks such as ensuring client/server
@@ -203,12 +204,32 @@ export default function LoginScreen() : React.ReactNode {
* Handle the status update.
*/
onError: (message) => {
checkSuccess = false;
// Show modal with error message
Alert.alert('Error', message);
webApi.logout(message);
}
},
/**
* On upgrade required.
*/
onUpgradeRequired: async () : Promise<void> => {
checkSuccess = false;
// Still login to ensure the user is logged in.
await authContext.login();
// But after login, redirect to upgrade screen immediately.
router.replace('/upgrade');
return;
},
});
if (!checkSuccess) {
// If the syncvault checks have failed, we can't continue with the login process.
return;
}
await authContext.login();
authContext.setOfflineMode(false);

View File

@@ -163,7 +163,13 @@ export default function ReinitializeScreen() : React.ReactNode {
}
]
);
}
},
/**
* On upgrade required.
*/
onUpgradeRequired: () : void => {
router.replace('/upgrade');
},
});
};

View File

@@ -0,0 +1,472 @@
import { LinearGradient } from 'expo-linear-gradient';
import { router } from 'expo-router';
import { useState, useEffect, useCallback } from 'react';
import { StyleSheet, View, TouchableOpacity, Alert, KeyboardAvoidingView, Platform, ScrollView, Dimensions, TouchableWithoutFeedback, Keyboard, Text } from 'react-native';
import type { VaultVersion } from '@/utils/dist/shared/vault-sql';
import { VaultSqlGenerator } from '@/utils/dist/shared/vault-sql';
import { useColors } from '@/hooks/useColorScheme';
import { useVaultSync } from '@/hooks/useVaultSync';
import Logo from '@/assets/images/logo.svg';
import LoadingIndicator from '@/components/LoadingIndicator';
import { ThemedText } from '@/components/themed/ThemedText';
import { ThemedView } from '@/components/themed/ThemedView';
import { Avatar } from '@/components/ui/Avatar';
import { useAuth } from '@/context/AuthContext';
import { useDb } from '@/context/DbContext';
import { useWebApi } from '@/context/WebApiContext';
import NativeVaultManager from '@/specs/NativeVaultManager';
/**
* Upgrade screen.
*/
export default function UpgradeScreen() : React.ReactNode {
const { username } = useAuth();
const { sqliteClient } = useDb();
const [isLoading, setIsLoading] = useState(false);
const [currentVersion, setCurrentVersion] = useState<VaultVersion | null>(null);
const [latestVersion, setLatestVersion] = useState<VaultVersion | null>(null);
const [upgradeStatus, setUpgradeStatus] = useState('Preparing upgrade...');
const colors = useColors();
const webApi = useWebApi();
const { syncVault } = useVaultSync();
/**
* Load version information from the database.
*/
const loadVersionInfo = useCallback(async () => {
try {
if (sqliteClient) {
const current = await sqliteClient.getDatabaseVersion();
const latest = await sqliteClient.getLatestDatabaseVersion();
setCurrentVersion(current);
setLatestVersion(latest);
}
} catch (error) {
console.error('Failed to load version information:', error);
}
}, [sqliteClient]);
useEffect(() => {
loadVersionInfo();
}, [loadVersionInfo]);
/**
* Handle the vault upgrade.
*/
const handleUpgrade = async () : Promise<void> => {
if (!sqliteClient || !currentVersion || !latestVersion) {
Alert.alert('Error', 'Unable to get version information. Please try again.');
return;
}
setIsLoading(true);
setUpgradeStatus('Preparing upgrade...');
try {
// Get upgrade SQL commands from vault-sql shared library
setUpgradeStatus('Generating upgrade SQL...');
const vaultSqlGenerator = new VaultSqlGenerator();
const upgradeResult = vaultSqlGenerator.getUpgradeVaultSql(currentVersion.revision, latestVersion.revision);
if (!upgradeResult.success) {
throw new Error(upgradeResult.error ?? 'Failed to generate upgrade SQL');
}
if (upgradeResult.sqlCommands.length === 0) {
// No upgrade needed, vault is already up to date
setUpgradeStatus('Vault is already up to date');
await new Promise(resolve => setTimeout(resolve, 1000));
await handleUpgradeSuccess();
return;
}
// Begin transaction
setUpgradeStatus('Starting database transaction...');
await NativeVaultManager.beginTransaction();
// Execute each SQL command
setUpgradeStatus('Applying database migrations...');
for (let i = 0; i < upgradeResult.sqlCommands.length; i++) {
const sqlCommand = upgradeResult.sqlCommands[i];
console.log('update sql command', i);
console.log('Executing SQL command:', sqlCommand);
setUpgradeStatus(`Applying migration ${i + 1} of ${upgradeResult.sqlCommands.length}...`);
try {
await NativeVaultManager.executeRaw(sqlCommand);
} catch (error) {
console.error(`Error executing SQL command ${i + 1}:`, sqlCommand, error);
await NativeVaultManager.rollbackTransaction();
throw new Error(`Failed to apply migration ${i + 1}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Commit transaction
setUpgradeStatus('Committing changes...');
await NativeVaultManager.commitTransaction();
// Upload to server
setUpgradeStatus('Uploading vault to server...');
await uploadVaultToServer();
// Sync and navigate to credentials
setUpgradeStatus('Finalizing upgrade...');
await handleUpgradeSuccess();
} catch (error) {
console.error('Upgrade failed:', error);
Alert.alert('Upgrade Failed', error instanceof Error ? error.message : 'An unknown error occurred during the upgrade. Please try again.');
} finally {
setIsLoading(false);
setUpgradeStatus('Preparing upgrade...');
}
};
/**
* Upload the upgraded vault to the server.
*/
const uploadVaultToServer = async () : Promise<void> => {
try {
// Get the current vault revision number
const currentRevision = await NativeVaultManager.getCurrentVaultRevisionNumber();
// Get the encrypted database
const encryptedDb = await NativeVaultManager.getEncryptedDatabase();
if (!encryptedDb) {
throw new Error('Failed to get encrypted database');
}
// Get all private email domains from credentials in order to claim them on server
const privateEmailDomains = await sqliteClient!.getPrivateEmailDomains();
const credentials = await sqliteClient!.getAllCredentials();
const privateEmailAddresses = credentials
.filter(cred => cred.Alias?.Email != null)
.map(cred => cred.Alias!.Email!)
.filter((email, index, self) => self.indexOf(email) === index)
.filter(email => {
return privateEmailDomains.some(domain => email.toLowerCase().endsWith(`@${domain.toLowerCase()}`));
});
// Get username from the auth context
if (!username) {
throw new Error('Username not found');
}
// Create vault object for upload
const newVault = {
blob: encryptedDb,
createdAt: new Date().toISOString(),
credentialsCount: credentials.length,
currentRevisionNumber: currentRevision,
emailAddressList: privateEmailAddresses,
privateEmailDomainList: [], // Empty on purpose, API will not use this for vault updates
publicEmailDomainList: [], // Empty on purpose, API will not use this for vault updates
encryptionPublicKey: '', // Empty on purpose, only required if new public/private key pair is generated
client: '', // Empty on purpose, API will not use this for vault updates
updatedAt: new Date().toISOString(),
username: username,
version: (await sqliteClient!.getDatabaseVersion())?.version ?? '0.0.0'
};
// Upload to server
const response = await webApi.post<typeof newVault, { status: number; newRevisionNumber: number }>('Vault', newVault);
if (response.status === 0) {
await NativeVaultManager.setCurrentVaultRevisionNumber(response.newRevisionNumber);
} else if (response.status === 1) {
throw new Error('Vault merge required. Please login via the web app to merge the multiple pending updates to your vault.');
} else {
throw new Error('Failed to upload vault to server');
}
} catch (error) {
console.error('Error uploading vault:', error);
throw error;
}
};
/**
* Handle successful upgrade completion.
*/
const handleUpgradeSuccess = async () : Promise<void> => {
try {
// Sync vault to ensure we have the latest data
await syncVault({
/**
* Handle the status update.
*/
onStatus: (message) => setUpgradeStatus(message),
/**
* Handle successful vault sync and navigate to credentials.
*/
onSuccess: () => {
// Navigate to credentials index
router.replace('/(tabs)/credentials');
},
/**
* Handle sync error and still navigate to credentials.
*/
onError: (error) => {
console.error('Sync error after upgrade:', error);
// Still navigate to credentials even if sync fails
router.replace('/(tabs)/credentials');
}
});
} catch (error) {
console.error('Error during post-upgrade sync:', error);
// Navigate to credentials even if sync fails
router.replace('/(tabs)/credentials');
}
};
/**
* Handle the logout.
*/
const handleLogout = async () : Promise<void> => {
/*
* Clear any stored tokens or session data
* This will be handled by the auth context
*/
await webApi.logout();
router.replace('/login');
};
const styles = StyleSheet.create({
appName: {
color: colors.text,
fontSize: 32,
fontWeight: 'bold',
textAlign: 'center',
},
avatarContainer: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'center',
marginBottom: 16,
},
button: {
alignItems: 'center',
backgroundColor: colors.primary,
borderRadius: 8,
height: 50,
justifyContent: 'center',
marginBottom: 16,
width: '100%',
},
buttonText: {
color: colors.primarySurfaceText,
fontSize: 16,
fontWeight: '600',
},
container: {
flex: 1,
},
content: {
backgroundColor: colors.accentBackground,
borderRadius: 10,
padding: 20,
width: '100%',
},
faceIdButton: {
alignItems: 'center',
height: 50,
justifyContent: 'center',
width: '100%',
},
faceIdButtonText: {
color: colors.primary,
fontSize: 16,
fontWeight: '600',
},
gradientContainer: {
height: Dimensions.get('window').height * 0.4,
left: 0,
position: 'absolute',
right: 0,
top: 0,
},
headerSection: {
paddingBottom: 24,
paddingHorizontal: 16,
paddingTop: 24,
},
input: {
backgroundColor: colors.background,
borderRadius: 8,
color: colors.text,
flex: 1,
fontSize: 16,
height: 50,
paddingHorizontal: 16,
},
inputContainer: {
alignItems: 'center',
backgroundColor: colors.background,
borderColor: colors.accentBorder,
borderRadius: 8,
borderWidth: 1,
flexDirection: 'row',
marginBottom: 16,
},
inputIcon: {
padding: 12,
},
keyboardAvoidingView: {
flex: 1,
},
loadingContainer: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
},
logoContainer: {
alignItems: 'center',
marginBottom: 8,
},
logoutButton: {
alignSelf: 'center',
justifyContent: 'center',
marginTop: 16,
},
logoutButtonText: {
color: colors.red,
fontSize: 16,
},
mainContent: {
flex: 1,
justifyContent: 'center',
paddingBottom: 40,
paddingHorizontal: 20,
},
currentVersionValue: {
color: colors.primary,
},
latestVersionValue: {
color: '#10B981',
},
scrollContent: {
flexGrow: 1,
},
subtitle: {
color: colors.text,
fontSize: 16,
marginBottom: 24,
opacity: 0.7,
textAlign: 'center',
},
username: {
color: colors.text,
fontSize: 18,
opacity: 0.8,
textAlign: 'center',
},
versionContainer: {
backgroundColor: colors.background,
borderRadius: 8,
marginBottom: 16,
padding: 16,
},
versionLabel: {
color: colors.text,
fontSize: 14,
fontWeight: '500',
opacity: 0.7,
},
versionRow: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 8,
},
versionTitle: {
color: colors.text,
fontSize: 18,
fontWeight: '600',
marginBottom: 12,
textAlign: 'center',
},
versionValue: {
color: colors.text,
fontSize: 16,
fontWeight: 'bold',
},
});
return (
<ThemedView style={styles.container}>
{isLoading ? (
<View style={styles.loadingContainer}>
<LoadingIndicator status={upgradeStatus} />
</View>
) : (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardAvoidingView}
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
>
<LinearGradient
colors={[colors.loginHeader, colors.background]}
style={styles.gradientContainer}
/>
<View style={styles.mainContent}>
<View style={styles.headerSection}>
<View style={styles.logoContainer}>
<Logo width={80} height={80} />
<Text style={styles.appName}>Upgrade Vault</Text>
</View>
</View>
<View style={styles.content}>
<View style={styles.avatarContainer}>
<Avatar />
<ThemedText style={styles.username}>{username}</ThemedText>
</View>
<ThemedText style={styles.subtitle}>AliasVault has updated and your vault needs to be upgraded.</ThemedText>
<View style={styles.versionContainer}>
<ThemedText style={styles.versionTitle}>Version Information</ThemedText>
<View style={styles.versionRow}>
<ThemedText style={styles.versionLabel}>Your vault:</ThemedText>
<ThemedText style={[styles.versionValue, styles.currentVersionValue]}>
{currentVersion?.releaseVersion ?? '...'}
</ThemedText>
</View>
<View style={styles.versionRow}>
<ThemedText style={styles.versionLabel}>New version:</ThemedText>
<ThemedText style={[styles.versionValue, styles.latestVersionValue]}>
{latestVersion?.releaseVersion ?? '...'}
</ThemedText>
</View>
</View>
<TouchableOpacity
style={styles.button}
onPress={handleUpgrade}
disabled={isLoading}
>
<ThemedText style={styles.buttonText}>
{isLoading ? 'Upgrading...' : 'Upgrade'}
</ThemedText>
</TouchableOpacity>
<TouchableOpacity
style={styles.logoutButton}
onPress={handleLogout}
>
<ThemedText style={styles.logoutButtonText}>Logout</ThemedText>
</TouchableOpacity>
</View>
</View>
</ScrollView>
</TouchableWithoutFeedback>
</KeyboardAvoidingView>
)}
</ThemedView>
);
}

View File

@@ -13,6 +13,7 @@ type DbContextType = {
storeEncryptionKey: (derivedKey: string) => Promise<void>;
storeEncryptionKeyDerivationParams: (keyDerivationParams: EncryptionKeyDerivationParams) => Promise<void>;
initializeDatabase: (vaultResponse: VaultResponse) => Promise<void>;
hasPendingMigrations: () => Promise<boolean>;
clearDatabase: () => void;
getVaultMetadata: () => Promise<VaultMetadata | null>;
testDatabaseConnection: (derivedKey: string) => Promise<boolean>;
@@ -97,6 +98,16 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
setDbAvailable(true);
}, [sqliteClient, unlockVault]);
/**
* Check if there are any pending migrations.
*/
const hasPendingMigrations = useCallback(async () => {
const currentVersion = await sqliteClient.getDatabaseVersion();
const latestVersion = await sqliteClient.getLatestDatabaseVersion();
return currentVersion.revision < latestVersion.revision;
}, [sqliteClient]);
const checkStoredVault = useCallback(async () => {
try {
const hasEncryptedDatabase = await NativeVaultManager.hasEncryptedDatabase();
@@ -166,7 +177,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
// Try to get the database version as a simple test query
const version = await sqliteClient.getDatabaseVersion();
if (version && version.length > 0) {
if (version && version.version && version.version.length > 0) {
return true;
}
@@ -182,13 +193,14 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
dbInitialized,
dbAvailable,
initializeDatabase,
hasPendingMigrations,
clearDatabase,
getVaultMetadata,
testDatabaseConnection,
unlockVault,
storeEncryptionKey,
storeEncryptionKeyDerivationParams,
}), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, clearDatabase, getVaultMetadata, testDatabaseConnection, unlockVault, storeEncryptionKey, storeEncryptionKeyDerivationParams]);
}), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, hasPendingMigrations, clearDatabase, getVaultMetadata, testDatabaseConnection, unlockVault, storeEncryptionKey, storeEncryptionKeyDerivationParams]);
return (
<DbContext.Provider value={contextValue}>

View File

@@ -85,7 +85,7 @@ export function useVaultMutate() : {
client: '', // Empty on purpose, API will not use this for vault updates
updatedAt: new Date().toISOString(),
username: username,
version: await dbContext.sqliteClient!.getDatabaseVersion() ?? '0.0.0'
version: (await dbContext.sqliteClient!.getDatabaseVersion())?.version ?? '0.0.0'
};
}, [dbContext, authContext]);

View File

@@ -1,3 +1,4 @@
import { useRouter } from 'expo-router';
import { useCallback } from 'react';
import { AppInfo } from '@/utils/AppInfo';
@@ -37,6 +38,7 @@ type VaultSyncOptions = {
onError?: (error: string) => void;
onStatus?: (message: string) => void;
onOffline?: () => void;
onUpgradeRequired?: () => void;
}
/**
@@ -50,7 +52,7 @@ export const useVaultSync = () : {
const webApi = useWebApi();
const syncVault = useCallback(async (options: VaultSyncOptions = {}) => {
const { initialSync = false, onSuccess, onError, onStatus, onOffline } = options;
const { initialSync = false, onSuccess, onError, onStatus, onOffline, onUpgradeRequired } = options;
// For the initial sync, we add an artifical delay to various steps which makes it feel more fluid.
const enableDelay = initialSync;
@@ -112,6 +114,15 @@ export const useVaultSync = () : {
try {
await dbContext.initializeDatabase(vaultResponseJson as VaultResponse);
// Check if the vault is up to date, if not, redirect to the upgrade page.
console.log('Checking for pending migrations');
if (await dbContext.hasPendingMigrations()) {
console.log('Pending migrations, redirecting to upgrade page');
onUpgradeRequired?.();
return false;
}
onSuccess?.(true);
return true;
} catch {
@@ -120,6 +131,14 @@ export const useVaultSync = () : {
}
}
// Check if the vault is up to date, if not, redirect to the upgrade page.
console.log('Checking for pending migrations');
if (await dbContext.hasPendingMigrations()) {
console.log('Pending migrations, redirecting to upgrade page');
onUpgradeRequired?.();
return false;
}
await withMinimumDelay(() => Promise.resolve(onSuccess?.(false)), 300, enableDelay);
return false;
} catch (err) {

View File

@@ -45,6 +45,10 @@
[vaultManager executeUpdate:query params:params resolver:resolve rejecter:reject];
}
- (void)executeRaw:(NSString *)query resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
[vaultManager executeRaw:query resolver:resolve rejecter:reject];
}
- (void)beginTransaction:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
[vaultManager beginTransaction:resolve rejecter:reject];
}

View File

@@ -163,6 +163,19 @@ public class VaultManager: NSObject {
}
}
@objc
func executeRaw(_ query: String,
resolver resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock) {
do {
// Execute the raw query through the vault store
try vaultStore.executeRaw(query)
resolve(nil)
} catch {
reject("RAW_ERROR", "Failed to execute raw query: \(error.localizedDescription)", error)
}
}
@objc
func clearVault() {
do {

View File

@@ -74,6 +74,30 @@ extension VaultStore {
return dbConnection.changes
}
/// Execute a raw SQL command on the database without parameters (for DDL operations like CREATE TABLE).
public func executeRaw(_ query: String) throws {
guard let dbConnection = self.dbConnection else {
throw NSError(domain: "VaultStore", code: 4, userInfo: [NSLocalizedDescriptionKey: "Database not initialized"])
}
// Split the query by semicolons to handle multiple statements
let statements = query.components(separatedBy: ";")
for statement in statements {
let trimmedStatement = statement.trimmingCharacters(in: .whitespacesAndNewlines)
// Skip empty statements and transaction control statements (handled externally)
if trimmedStatement.isEmpty ||
trimmedStatement.uppercased().hasPrefix("BEGIN TRANSACTION") ||
trimmedStatement.uppercased().hasPrefix("COMMIT") ||
trimmedStatement.uppercased().hasPrefix("ROLLBACK") {
continue
}
try dbConnection.execute(trimmedStatement)
}
}
/// Begin a transaction on the database. This is required for all database operations that modify the database.
public func beginTransaction() throws {
guard let dbConnection = self.dbConnection else {

View File

@@ -27,6 +27,7 @@ export interface Spec extends TurboModule {
// SQL operations
executeQuery(query: string, params: (string | number | null)[]): Promise<string[]>;
executeUpdate(query: string, params:(string | number | null)[]): Promise<number>;
executeRaw(query: string): Promise<void>;
beginTransaction(): Promise<void>;
commitTransaction(): Promise<void>;
rollbackTransaction(): Promise<void>;

View File

@@ -1,5 +1,6 @@
import type { EncryptionKeyDerivationParams, VaultMetadata } from '@/utils/dist/shared/models/metadata';
import type { Credential, EncryptionKey, PasswordSettings, TotpCode } from '@/utils/dist/shared/models/vault';
import { VaultSqlGenerator, VaultVersion } from '@/utils/dist/shared/vault-sql';
import NativeVaultManager from '@/specs/NativeVaultManager';
@@ -560,8 +561,10 @@ class SqliteClient {
* Returns the semantic version (e.g., "1.4.1") from the latest migration.
* Returns null if no migrations are found.
*/
public async getDatabaseVersion(): Promise<string | null> {
public async getDatabaseVersion(): Promise<VaultVersion> {
try {
let currentVersion = '';
// Query the migrations history table for the latest migration
const results = await this.executeQuery<{ MigrationId: string }>(`
SELECT MigrationId
@@ -570,7 +573,7 @@ class SqliteClient {
LIMIT 1`);
if (results.length === 0) {
return null;
throw new Error('No migrations found');
}
// Extract version using regex - matches patterns like "20240917191243_1.4.1-RenameAttachmentsPlural"
@@ -579,16 +582,34 @@ class SqliteClient {
const versionMatch = versionRegex.exec(migrationId);
if (versionMatch?.[1]) {
return versionMatch[1];
currentVersion = versionMatch[1];
}
return null;
// 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(`Current version ${currentVersion} not found in available vault versions.`);
}
return currentVersionRevision;
} catch (error) {
console.error('Error getting database version:', error);
throw error;
}
}
/**
* Returns the version info of the latest available vault migration.
*/
public async getLatestDatabaseVersion(): Promise<VaultVersion> {
const vaultSqlGenerator = new VaultSqlGenerator();
const allVersions = vaultSqlGenerator.getAllVersions();
return allVersions[allVersions.length - 1];
}
/**
* Get TOTP codes for a credential
* @param credentialId - The ID of the credential to get TOTP codes for