From b374f2e5a176bfe4d3ea2e4cda5767d3416c5566 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Tue, 2 Sep 2025 10:20:54 +0000 Subject: [PATCH] feat: implement ActiveDid migration to active_identity table - Add $getActiveIdentity() method to PlatformServiceMixin interface - Update HomeView.vue to use new active_identity API methods - Update ContactsView.vue to use new active_identity API methods - Fix apiServer default handling in PlatformServiceMixin - Ensure DEFAULT_ENDORSER_API_SERVER is used when apiServer is empty - Add comprehensive logging for debugging ActiveDid migration - Resolve TypeScript interface issues with Vue mixins --- src/db-sql/migration.ts | 51 +++++++ src/utils/PlatformServiceMixin.ts | 231 ++++++++++++++++++++++++------ src/views/ContactsView.vue | 40 +++++- src/views/HomeView.vue | 193 ++++++++++++++++++++++--- 4 files changed, 448 insertions(+), 67 deletions(-) diff --git a/src/db-sql/migration.ts b/src/db-sql/migration.ts index 4bf0921c..c0358f20 100644 --- a/src/db-sql/migration.ts +++ b/src/db-sql/migration.ts @@ -4,6 +4,7 @@ import { } from "../services/migrationService"; import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app"; import { arrayBufferToBase64 } from "@/libs/crypto"; +import { logger } from "@/utils/logger"; // Generate a random secret for the secret table @@ -151,6 +152,50 @@ const MIGRATIONS = [ AND EXISTS (SELECT 1 FROM settings WHERE id = 1 AND activeDid IS NOT NULL AND activeDid != ''); `, }, + { + name: "004_remove_activeDid_from_settings", + sql: ` + -- Remove activeDid column from settings table (moved to active_identity) + -- Note: SQLite doesn't support DROP COLUMN in older versions + -- This migration will be skipped if DROP COLUMN is not supported + -- The activeDid column will remain but won't be used by the application + + -- Try to drop the activeDid column (works in SQLite 3.35.0+) + ALTER TABLE settings DROP COLUMN activeDid; + `, + }, + { + name: "005_eliminate_master_settings_key", + sql: ` + -- Eliminate MASTER_SETTINGS_KEY concept - remove confusing id=1 row + -- This creates clean separation: active_identity for current identity, settings for identity config + + -- Delete the confusing MASTER_SETTINGS_KEY row (id=1 with accountDid=NULL) + DELETE FROM settings WHERE id = 1 AND accountDid IS NULL; + + -- Reset auto-increment to start from 1 again + DELETE FROM sqlite_sequence WHERE name = 'settings'; + `, + }, + { + name: "006_add_unique_constraint_accountDid", + sql: ` + -- Add unique constraint to prevent duplicate accountDid values + -- This ensures data integrity: each identity can only have one settings record + + -- First, remove any duplicate accountDid entries (keep the most recent one) + DELETE FROM settings + WHERE id NOT IN ( + SELECT MAX(id) + FROM settings + WHERE accountDid IS NOT NULL + GROUP BY accountDid + ) AND accountDid IS NOT NULL; + + -- Add unique constraint on accountDid + CREATE UNIQUE INDEX IF NOT EXISTS idx_settings_accountDid_unique ON settings(accountDid); + `, + }, ]; /** @@ -162,8 +207,14 @@ export async function runMigrations( sqlQuery: (sql: string, params?: unknown[]) => Promise, extractMigrationNames: (result: T) => Set, ): Promise { + logger.info("[Migration] Starting database migrations"); + for (const migration of MIGRATIONS) { + logger.debug("[Migration] Registering migration:", migration.name); registerMigration(migration); } + + logger.info("[Migration] Running migration service"); await runMigrationsService(sqlExec, sqlQuery, extractMigrationNames); + logger.info("[Migration] Database migrations completed"); } diff --git a/src/utils/PlatformServiceMixin.ts b/src/utils/PlatformServiceMixin.ts index 18edda88..958478b8 100644 --- a/src/utils/PlatformServiceMixin.ts +++ b/src/utils/PlatformServiceMixin.ts @@ -45,7 +45,6 @@ import type { PlatformCapabilities, } from "@/services/PlatformService"; import { - MASTER_SETTINGS_KEY, type Settings, type SettingsWithJsonStrings, } from "@/db/tables/settings"; @@ -58,8 +57,6 @@ import { generateInsertStatement, generateUpdateStatement, } from "@/utils/sqlHelpers"; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import { ActiveIdentity } from "@/db/tables/activeIdentity"; // ================================================= // TYPESCRIPT INTERFACES @@ -198,6 +195,80 @@ export const PlatformServiceMixin = { // SELF-CONTAINED UTILITY METHODS (no databaseUtil dependency) // ================================================= + /** + * Ensure active_identity table is populated with data from settings + * This is a one-time fix for the migration gap + */ + async $ensureActiveIdentityPopulated(): Promise { + try { + logger.info( + "[PlatformServiceMixin] $ensureActiveIdentityPopulated() called", + ); + + // Check if active_identity has data + const activeIdentity = await this.$dbQuery( + "SELECT activeDid FROM active_identity WHERE id = 1", + ); + + const currentActiveDid = activeIdentity?.values?.[0]?.[0] as string; + logger.info( + "[PlatformServiceMixin] Current active_identity table state:", + { currentActiveDid, hasData: !!currentActiveDid }, + ); + + if (!currentActiveDid) { + logger.info( + "[PlatformServiceMixin] Active identity table empty, populating from settings", + ); + + // Get activeDid from settings (any row with accountDid) + const settings = await this.$dbQuery( + "SELECT accountDid FROM settings WHERE accountDid IS NOT NULL LIMIT 1", + ); + + const settingsAccountDid = settings?.values?.[0]?.[0] as string; + logger.info("[PlatformServiceMixin] Found settings accountDid:", { + settingsAccountDid, + }); + + if (settingsAccountDid) { + await this.$dbExec( + "UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1", + [settingsAccountDid], + ); + logger.info( + `[PlatformServiceMixin] Populated active_identity with: ${settingsAccountDid}`, + ); + } else { + // If no settings found, try to get any account DID + const accounts = await this.$dbQuery( + "SELECT did FROM accounts LIMIT 1", + ); + const accountDid = accounts?.values?.[0]?.[0] as string; + + if (accountDid) { + await this.$dbExec( + "UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1", + [accountDid], + ); + logger.info( + `[PlatformServiceMixin] Populated active_identity with account DID: ${accountDid}`, + ); + } else { + logger.warn( + "[PlatformServiceMixin] No accountDid found in settings or accounts table", + ); + } + } + } + } catch (error) { + logger.warn( + "[PlatformServiceMixin] Failed to populate active_identity:", + error, + ); + } + }, + /** * Update the current activeDid and trigger change detection * This method should be called when the user switches identities @@ -213,22 +284,18 @@ export const PlatformServiceMixin = { `[PlatformServiceMixin] ActiveDid updated from ${oldDid} to ${newDid}`, ); - // Dual-write to both tables for backward compatibility + // Write only to active_identity table (single source of truth) try { await this.$dbExec( "UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1", [newDid || ""], ); - await this.$dbExec("UPDATE settings SET activeDid = ? WHERE id = ?", [ - newDid || "", - MASTER_SETTINGS_KEY, - ]); logger.debug( - `[PlatformServiceMixin] ActiveDid dual-write completed for ${newDid}`, + `[PlatformServiceMixin] ActiveDid updated in active_identity table: ${newDid}`, ); } catch (error) { logger.error( - `[PlatformServiceMixin] Error in dual-write for activeDid ${newDid}:`, + `[PlatformServiceMixin] Error updating activeDid in active_identity table ${newDid}:`, error, ); // Continue with in-memory update even if database write fails @@ -468,10 +535,18 @@ export const PlatformServiceMixin = { fallback: Settings | null = null, ): Promise { try { - // Master settings: query by id + // Get current active identity + const activeIdentity = await this.$getActiveIdentity(); + const activeDid = activeIdentity.activeDid; + + if (!activeDid) { + return fallback; + } + + // Get identity-specific settings const result = await this.$dbQuery( - "SELECT * FROM settings WHERE id = ?", - [MASTER_SETTINGS_KEY], + "SELECT * FROM settings WHERE accountDid = ?", + [activeDid], ); if (!result?.values?.length) { @@ -508,7 +583,6 @@ export const PlatformServiceMixin = { * Handles the common pattern of layered settings */ async $getMergedSettings( - defaultKey: string, accountDid?: string, defaultFallback: Settings = {}, ): Promise { @@ -564,7 +638,6 @@ export const PlatformServiceMixin = { return mergedSettings; } catch (error) { logger.error(`[Settings Trace] ❌ Failed to get merged settings:`, { - defaultKey, accountDid, error, }); @@ -578,12 +651,29 @@ export const PlatformServiceMixin = { */ async $getActiveIdentity(): Promise<{ activeDid: string }> { try { + logger.info( + "[PlatformServiceMixin] $getActiveIdentity() called - API layer verification", + ); + + // Ensure the table is populated before reading + await this.$ensureActiveIdentityPopulated(); + + logger.debug( + "[PlatformServiceMixin] Getting active identity from active_identity table", + ); const result = await this.$dbQuery( "SELECT activeDid FROM active_identity WHERE id = 1", ); if (result?.values?.length) { const activeDid = result.values[0][0] as string; + logger.debug("[PlatformServiceMixin] Active identity found:", { + activeDid, + }); + logger.info( + "[PlatformServiceMixin] $getActiveIdentity(): activeDid resolved", + { activeDid }, + ); // Validate activeDid exists in accounts if (activeDid) { @@ -593,9 +683,15 @@ export const PlatformServiceMixin = { ); if (accountExists?.values?.length) { + logger.debug( + "[PlatformServiceMixin] Active identity validated in accounts", + ); return { activeDid }; } else { // Clear corrupted activeDid + logger.warn( + "[PlatformServiceMixin] Active identity not found in accounts, clearing", + ); await this.$dbExec( "UPDATE active_identity SET activeDid = '', lastUpdated = datetime('now') WHERE id = 1", ); @@ -604,6 +700,9 @@ export const PlatformServiceMixin = { } } + logger.debug( + "[PlatformServiceMixin] No active identity found, returning empty", + ); return { activeDid: "" }; } catch (error) { logger.error( @@ -825,14 +924,14 @@ export const PlatformServiceMixin = { return defaults; } - // FIXED: Remove forced override - respect user preferences + // FIXED: Set default apiServer for all platforms, not just Electron // Only set default if no user preference exists - if (!settings.apiServer && process.env.VITE_PLATFORM === "electron") { + if (!settings.apiServer) { // Import constants dynamically to get platform-specific values const { DEFAULT_ENDORSER_API_SERVER } = await import( "../constants/app" ); - // Only set if user hasn't specified a preference + // Set default for all platforms when apiServer is empty settings.apiServer = DEFAULT_ENDORSER_API_SERVER; } @@ -858,10 +957,9 @@ export const PlatformServiceMixin = { return defaults; } - // Determine which DID to use - prioritize new active_identity table, fallback to settings + // Get DID from active_identity table (single source of truth) const activeIdentity = await this.$getActiveIdentity(); - const targetDid = - did || activeIdentity.activeDid || defaultSettings.activeDid; + const targetDid = did || activeIdentity.activeDid; // If no target DID, return default settings if (!targetDid) { @@ -870,27 +968,29 @@ export const PlatformServiceMixin = { // Get merged settings using existing method const mergedSettings = await this.$getMergedSettings( - MASTER_SETTINGS_KEY, targetDid, defaultSettings, ); - // Ensure activeDid comes from new table when available - if (activeIdentity.activeDid) { - mergedSettings.activeDid = activeIdentity.activeDid; - } + // Set activeDid from active_identity table (single source of truth) + mergedSettings.activeDid = activeIdentity.activeDid; + logger.debug( + "[PlatformServiceMixin] Using activeDid from active_identity table:", + { activeDid: activeIdentity.activeDid }, + ); + logger.info( + "[PlatformServiceMixin] $accountSettings() returning activeDid:", + { activeDid: mergedSettings.activeDid }, + ); - // FIXED: Remove forced override - respect user preferences + // FIXED: Set default apiServer for all platforms, not just Electron // Only set default if no user preference exists - if ( - !mergedSettings.apiServer && - process.env.VITE_PLATFORM === "electron" - ) { + if (!mergedSettings.apiServer) { // Import constants dynamically to get platform-specific values const { DEFAULT_ENDORSER_API_SERVER } = await import( "../constants/app" ); - // Only set if user hasn't specified a preference + // Set default for all platforms when apiServer is empty mergedSettings.apiServer = DEFAULT_ENDORSER_API_SERVER; } @@ -928,16 +1028,36 @@ export const PlatformServiceMixin = { async $saveSettings(changes: Partial): Promise { try { // Remove fields that shouldn't be updated - const { accountDid, id, ...safeChanges } = changes; + const { + accountDid, + id, + activeDid: activeDidField, + ...safeChanges + } = changes; // eslint-disable-next-line @typescript-eslint/no-unused-vars void accountDid; // eslint-disable-next-line @typescript-eslint/no-unused-vars void id; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + void activeDidField; + + logger.debug( + "[PlatformServiceMixin] $saveSettings - Original changes:", + changes, + ); + logger.debug( + "[PlatformServiceMixin] $saveSettings - Safe changes:", + safeChanges, + ); if (Object.keys(safeChanges).length === 0) return true; // Convert settings for database storage (handles searchBoxes conversion) const convertedChanges = this._convertSettingsForStorage(safeChanges); + logger.debug( + "[PlatformServiceMixin] $saveSettings - Converted changes:", + convertedChanges, + ); const setParts: string[] = []; const params: unknown[] = []; @@ -949,17 +1069,33 @@ export const PlatformServiceMixin = { } }); + logger.debug( + "[PlatformServiceMixin] $saveSettings - Set parts:", + setParts, + ); + logger.debug("[PlatformServiceMixin] $saveSettings - Params:", params); + if (setParts.length === 0) return true; - params.push(MASTER_SETTINGS_KEY); - await this.$dbExec( - `UPDATE settings SET ${setParts.join(", ")} WHERE id = ?`, - params, - ); + // Get current active DID and update that identity's settings + const activeIdentity = await this.$getActiveIdentity(); + const currentActiveDid = activeIdentity.activeDid; + + if (currentActiveDid) { + params.push(currentActiveDid); + await this.$dbExec( + `UPDATE settings SET ${setParts.join(", ")} WHERE accountDid = ?`, + params, + ); + } else { + logger.warn( + "[PlatformServiceMixin] No active DID found, cannot save settings", + ); + } // Update activeDid tracking if it changed - if (changes.activeDid !== undefined) { - await this.$updateActiveDid(changes.activeDid); + if (activeDidField !== undefined) { + await this.$updateActiveDid(activeDidField); } return true; @@ -1409,13 +1545,16 @@ export const PlatformServiceMixin = { fields: string[], did?: string, ): Promise { - // Use correct settings table schema - const whereClause = did ? "WHERE accountDid = ?" : "WHERE id = ?"; - const params = did ? [did] : [MASTER_SETTINGS_KEY]; + // Use current active DID if no specific DID provided + const targetDid = did || (await this.$getActiveIdentity()).activeDid; + + if (!targetDid) { + return undefined; + } return await this.$one( - `SELECT ${fields.join(", ")} FROM settings ${whereClause}`, - params, + `SELECT ${fields.join(", ")} FROM settings WHERE accountDid = ?`, + [targetDid], ); }, @@ -1655,7 +1794,6 @@ export const PlatformServiceMixin = { // Get merged settings const mergedSettings = await this.$getMergedSettings( - MASTER_SETTINGS_KEY, did, defaultSettings || {}, ); @@ -1697,6 +1835,7 @@ export interface IPlatformServiceMixin { accountDid?: string, defaultFallback?: Settings, ): Promise; + $getActiveIdentity(): Promise<{ activeDid: string }>; $withTransaction(callback: () => Promise): Promise; isCapacitor: boolean; isWeb: boolean; diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index 2ed7611f..777e8cca 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -174,7 +174,7 @@ import { logger } from "../utils/logger"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { isDatabaseError } from "@/interfaces/common"; import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; -import { APP_SERVER } from "@/constants/app"; +import { APP_SERVER, DEFAULT_ENDORSER_API_SERVER } from "@/constants/app"; import { QRNavigationService } from "@/services/QRNavigationService"; import { NOTIFY_CONTACT_NO_INFO, @@ -294,10 +294,19 @@ export default class ContactsView extends Vue { this.notify = createNotifyHelpers(this.$notify); const settings = await this.$accountSettings(); - this.activeDid = settings.activeDid || ""; - this.apiServer = settings.apiServer || ""; + // Get activeDid from active_identity table (single source of truth) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const activeIdentity = await (this as any).$getActiveIdentity(); + this.activeDid = activeIdentity.activeDid || ""; + this.apiServer = settings.apiServer || DEFAULT_ENDORSER_API_SERVER; this.isRegistered = !!settings.isRegistered; + logger.info("[ContactsView] Created with settings:", { + activeDid: this.activeDid, + apiServer: this.apiServer, + isRegistered: this.isRegistered, + }); + // if these detect a query parameter, they can and then redirect to this URL without a query parameter // to avoid problems when they reload or they go forward & back and it tries to reprocess await this.processContactJwt(); @@ -346,15 +355,37 @@ export default class ContactsView extends Vue { // this happens when a platform (eg iOS) doesn't include anything after the "=" in a shared link. this.notify.error(NOTIFY_BLANK_INVITE.message, TIMEOUTS.VERY_LONG); } else if (importedInviteJwt) { + logger.info("[ContactsView] Processing invite JWT, current activeDid:", { + activeDid: this.activeDid, + }); + + // Ensure active_identity is populated before processing invite + await this.$ensureActiveIdentityPopulated(); + + // Re-fetch settings after ensuring active_identity is populated + const updatedSettings = await this.$accountSettings(); + this.activeDid = updatedSettings.activeDid || ""; + this.apiServer = updatedSettings.apiServer || DEFAULT_ENDORSER_API_SERVER; + // Identity creation should be handled by router guard, but keep as fallback for invite processing if (!this.activeDid) { logger.info( "[ContactsView] No active DID found, creating identity as fallback for invite processing", ); this.activeDid = await generateSaveAndActivateIdentity(); + logger.info("[ContactsView] Created new identity:", { + activeDid: this.activeDid, + }); } // send invite directly to server, with auth for this user const headers = await getHeaders(this.activeDid); + logger.info("[ContactsView] Making API request to claim invite:", { + apiServer: this.apiServer, + activeDid: this.activeDid, + hasApiServer: !!this.apiServer, + apiServerLength: this.apiServer?.length || 0, + fullUrl: this.apiServer + "/api/v2/claim", + }); try { const response = await this.axios.post( this.apiServer + "/api/v2/claim", @@ -376,6 +407,9 @@ export default class ContactsView extends Vue { const payload: JWTPayload = decodeEndorserJwt(importedInviteJwt).payload; const registration = payload as VerifiableCredential; + logger.info( + "[ContactsView] Opening ContactNameDialog for invite processing", + ); (this.$refs.contactNameDialog as ContactNameDialog).open( "Who Invited You?", "", diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 45a5d5bb..19e27022 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -238,7 +238,7 @@ Raymer * @version 1.0.0 */