diff --git a/doc/secure-storage-implementation.md b/doc/secure-storage-implementation.md index 3aedb1dd..e1fabd4d 100644 --- a/doc/secure-storage-implementation.md +++ b/doc/secure-storage-implementation.md @@ -209,12 +209,8 @@ When converting from Dexie.js to SQL-based implementation, follow these patterns ``` Key Considerations: -1. Always use parameterized queries to prevent SQL injection -2. Use the `generateUpdateStatement` helper for update operations -3. Use the `mapColumnsToValues` helper for processing query results -4. Handle transactions explicitly for batch operations -5. Use appropriate error handling with the StorageError class -6. Consider platform-specific capabilities when implementing features +- Use the `generateUpdateStatement` helper for update operations +- Use the `mapColumnsToValues` helper for processing query results Example Migration: ```typescript @@ -238,7 +234,12 @@ export async function updateSettings(settings: Settings): Promise { Remember to: - Create database access code to use the platform service, putting it in front of the Dexie version -- Instead of removing Dexie-specific code, keep it; if we use the results of the query, then check the USE_DEXIE_DB from app.ts and if it's true then use that instead of the SQL code. +- Instead of removing Dexie-specific code, keep it. + + - For creates & updates & deletes, the duplicate code is fine. + + - For queries where we use the results, make the setting from SQL into a 'let' variable, then wrap the Dexie code in a check for USE_DEXIE_DB from app.ts and if it's true then use that result instead of the SQL code's result. + - Test thoroughly after migration - Consider data migration needs, and warn if there are any potential migration problems diff --git a/src/constants/app.ts b/src/constants/app.ts index 6cfb4996..610d1326 100644 --- a/src/constants/app.ts +++ b/src/constants/app.ts @@ -7,6 +7,7 @@ export enum AppString { // This is used in titles and verbiage inside the app. // There is also an app name without spaces, for packaging in the package.json file used in the manifest. APP_NAME = "Time Safari", + APP_NAME_NO_SPACES = "TimeSafari", PROD_ENDORSER_API_SERVER = "https://api.endorser.ch", TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch", @@ -50,7 +51,7 @@ export const IMAGE_TYPE_PROFILE = "profile"; export const PASSKEYS_ENABLED = !!import.meta.env.VITE_PASSKEYS_ENABLED || false; -export const USE_DEXIE_DB = true; +export const USE_DEXIE_DB = false; /** * The possible values for "group" and "type" are in App.vue. diff --git a/src/db/databaseUtil.ts b/src/db/databaseUtil.ts index 4c803114..b2d0276d 100644 --- a/src/db/databaseUtil.ts +++ b/src/db/databaseUtil.ts @@ -26,38 +26,6 @@ export async function updateDefaultSettings( } } -const DEFAULT_SETTINGS: Settings = { - id: MASTER_SETTINGS_KEY, - activeDid: undefined, - apiServer: DEFAULT_ENDORSER_API_SERVER, -}; - -// retrieves default settings -export async function retrieveSettingsForDefaultAccount(): Promise { - const platform = PlatformServiceFactory.getInstance(); - const result = await platform.dbQuery("SELECT * FROM settings WHERE id = ?", MASTER_SETTINGS_KEY) - if (!result) { - return DEFAULT_SETTINGS; - } else { - return mapColumnsToValues(result.columns, result.values)[0] as Settings; - } -} - -export async function retrieveSettingsForActiveAccount(): Promise { - const defaultSettings = await retrieveSettingsForDefaultAccount(); - if (!defaultSettings.activeDid) { - return defaultSettings; - } else { - const platform = PlatformServiceFactory.getInstance(); - const result = await platform.dbQuery( - "SELECT * FROM settings WHERE accountDid = ?", - [defaultSettings.activeDid] - ); - const overrideSettings = result ? mapColumnsToValues(result.columns, result.values)[0] as Settings : {}; - return { ...defaultSettings, ...overrideSettings }; - } -} - export async function updateAccountSettings( accountDid: string, settingsChanges: Settings, @@ -92,6 +60,39 @@ export async function updateAccountSettings( } } +const DEFAULT_SETTINGS: Settings = { + id: MASTER_SETTINGS_KEY, + activeDid: undefined, + apiServer: DEFAULT_ENDORSER_API_SERVER, +}; + +// retrieves default settings +export async function retrieveSettingsForDefaultAccount(): Promise { + const platform = PlatformServiceFactory.getInstance(); + const result = await platform.dbQuery("SELECT * FROM settings WHERE id = ?", [MASTER_SETTINGS_KEY]) + if (!result) { + return DEFAULT_SETTINGS; + } else { + return mapColumnsToValues(result.columns, result.values)[0] as Settings; + } +} + +export async function retrieveSettingsForActiveAccount(): Promise { + const defaultSettings = await retrieveSettingsForDefaultAccount(); + if (!defaultSettings.activeDid) { + return defaultSettings; + } else { + const platform = PlatformServiceFactory.getInstance(); + const result = await platform.dbQuery( + "SELECT * FROM settings WHERE accountDid = ?", + [defaultSettings.activeDid] + ); + const overrideSettings = result ? mapColumnsToValues(result.columns, result.values)[0] as Settings : {}; + const overrideSettingsFiltered = Object.fromEntries(Object.entries(overrideSettings).filter(([_, v]) => v !== null)); + return { ...defaultSettings, ...overrideSettingsFiltered }; + } +} + export async function logToDb(message: string): Promise { const platform = PlatformServiceFactory.getInstance(); const todayKey = new Date().toDateString(); diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index ec27db2d..6cfd0e98 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -26,6 +26,7 @@ import { DEFAULT_IMAGE_API_SERVER, NotificationIface, APP_SERVER, + USE_DEXIE_DB, } from "../constants/app"; import { Contact } from "../db/tables/contacts"; import { accessToken, deriveAddress, nextDerivationPath } from "../libs/crypto"; @@ -49,6 +50,7 @@ import { CreateAndSubmitClaimResult, } from "../interfaces"; import { logger } from "../utils/logger"; +import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; /** * Standard context for schema.org data @@ -1363,7 +1365,14 @@ export async function setVisibilityUtil( if (resp.status === 200) { const success = resp.data.success; if (success) { - db.contacts.update(contact.did, { seesMe: visibility }); + const platformService = PlatformServiceFactory.getInstance(); + await platformService.dbExec( + "UPDATE contacts SET seesMe = ? WHERE did = ?", + [visibility, contact.did], + ); + if (USE_DEXIE_DB) { + db.contacts.update(contact.did, { seesMe: visibility }); + } } return { success }; } else { diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 543644b4..bd6916ad 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -999,6 +999,7 @@ import { DEFAULT_PUSH_SERVER, IMAGE_TYPE_PROFILE, NotificationIface, + USE_DEXIE_DB, } from "../constants/app"; import { db, @@ -1012,6 +1013,7 @@ import { DEFAULT_PASSKEY_EXPIRATION_MINUTES, MASTER_SETTINGS_KEY, } from "../db/tables/settings"; +import * as databaseUtil from "../db/databaseUtil"; import { clearPasskeyToken, EndorserRateLimits, @@ -1030,6 +1032,7 @@ import { } from "../libs/util"; import { UserProfile } from "@/libs/partnerServer"; import { logger } from "../utils/logger"; +import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; const inputImportFileNameRef = ref(); @@ -1145,9 +1148,14 @@ export default class AccountViewView extends Vue { if (error.status === 404) { // this is ok: the profile is not yet created } else { - logConsoleAndDb( + databaseUtil.logConsoleAndDb( "Error loading profile: " + errorStringForLog(error), ); + if (USE_DEXIE_DB) { + logConsoleAndDb( + "Error loading profile: " + errorStringForLog(error), + ); + } this.$notify( { group: "alert", @@ -1224,8 +1232,12 @@ export default class AccountViewView extends Vue { * Initializes component state with values from the database or defaults. */ async initializeState() { - await db.open(); - const settings = await retrieveSettingsForActiveAccount(); + let settings = await databaseUtil.retrieveSettingsForActiveAccount(); + if (USE_DEXIE_DB) { + await db.open(); + settings = await retrieveSettingsForActiveAccount(); + } + console.log("activeDid", settings.activeDid, "settings", settings); this.activeDid = settings.activeDid || ""; this.apiServer = settings.apiServer || ""; @@ -1268,42 +1280,67 @@ export default class AccountViewView extends Vue { async toggleShowContactAmounts() { this.showContactGives = !this.showContactGives; - await db.open(); - await db.settings.update(MASTER_SETTINGS_KEY, { + await databaseUtil.updateDefaultSettings({ showContactGivesInline: this.showContactGives, }); + if (USE_DEXIE_DB) { + await db.open(); + await db.settings.update(MASTER_SETTINGS_KEY, { + showContactGivesInline: this.showContactGives, + }); + } } async toggleShowGeneralAdvanced() { this.showGeneralAdvanced = !this.showGeneralAdvanced; - await db.open(); - await db.settings.update(MASTER_SETTINGS_KEY, { + await databaseUtil.updateDefaultSettings({ showGeneralAdvanced: this.showGeneralAdvanced, }); + if (USE_DEXIE_DB) { + await db.open(); + await db.settings.update(MASTER_SETTINGS_KEY, { + showGeneralAdvanced: this.showGeneralAdvanced, + }); + } } async toggleProdWarning() { this.warnIfProdServer = !this.warnIfProdServer; - await db.open(); - await db.settings.update(MASTER_SETTINGS_KEY, { + await databaseUtil.updateDefaultSettings({ warnIfProdServer: this.warnIfProdServer, }); + if (USE_DEXIE_DB) { + await db.open(); + await db.settings.update(MASTER_SETTINGS_KEY, { + warnIfProdServer: this.warnIfProdServer, + }); + } } async toggleTestWarning() { this.warnIfTestServer = !this.warnIfTestServer; - await db.open(); - await db.settings.update(MASTER_SETTINGS_KEY, { + await databaseUtil.updateDefaultSettings({ warnIfTestServer: this.warnIfTestServer, }); + if (USE_DEXIE_DB) { + await db.open(); + await db.settings.update(MASTER_SETTINGS_KEY, { + warnIfTestServer: this.warnIfTestServer, + }); + } } async toggleShowShortcutBvc() { this.showShortcutBvc = !this.showShortcutBvc; - await db.open(); - await db.settings.update(MASTER_SETTINGS_KEY, { + await databaseUtil.updateDefaultSettings({ showShortcutBvc: this.showShortcutBvc, }); + if (USE_DEXIE_DB) { + await db.open(); + await db.settings.update(MASTER_SETTINGS_KEY, { + showShortcutBvc: this.showShortcutBvc, + }); + } } readableDate(timeStr: string) { @@ -1314,9 +1351,18 @@ export default class AccountViewView extends Vue { * Processes the identity and updates the component's state. */ async processIdentity() { - const account: Account | undefined = await retrieveAccountMetadata( - this.activeDid, - ); + const platformService = PlatformServiceFactory.getInstance(); + const dbAccount = await platformService.dbQuery("SELECT * FROM accounts WHERE did = ?", [this.activeDid]); + console.log("activeDid", this.activeDid, "dbAccount", dbAccount); + let account: Account | undefined = undefined; + if (dbAccount) { + account = databaseUtil.mapColumnsToValues(dbAccount.columns, dbAccount.values)[0] as Account; + } + if (USE_DEXIE_DB) { + account = await retrieveAccountMetadata( + this.activeDid, + ); + } if (account?.identity) { const identity = JSON.parse(account.identity as string) as IIdentifier; this.publicHex = identity.keys[0].publicKeyHex; @@ -1359,9 +1405,14 @@ export default class AccountViewView extends Vue { this.$refs.pushNotificationPermission as PushNotificationPermission ).open(DAILY_CHECK_TITLE, async (success: boolean, timeText: string) => { if (success) { - await db.settings.update(MASTER_SETTINGS_KEY, { + await databaseUtil.updateDefaultSettings({ notifyingNewActivityTime: timeText, }); + if (USE_DEXIE_DB) { + await db.settings.update(MASTER_SETTINGS_KEY, { + notifyingNewActivityTime: timeText, + }); + } this.notifyingNewActivity = true; this.notifyingNewActivityTime = timeText; } @@ -1375,9 +1426,14 @@ export default class AccountViewView extends Vue { text: "", // unused, only here to satisfy type check callback: async (success) => { if (success) { - await db.settings.update(MASTER_SETTINGS_KEY, { + await databaseUtil.updateDefaultSettings({ notifyingNewActivityTime: "", }); + if (USE_DEXIE_DB) { + await db.settings.update(MASTER_SETTINGS_KEY, { + notifyingNewActivityTime: "", + }); + } this.notifyingNewActivity = false; this.notifyingNewActivityTime = ""; } @@ -1419,10 +1475,16 @@ export default class AccountViewView extends Vue { DIRECT_PUSH_TITLE, async (success: boolean, timeText: string, message?: string) => { if (success) { - await db.settings.update(MASTER_SETTINGS_KEY, { + await databaseUtil.updateDefaultSettings({ notifyingReminderMessage: message, notifyingReminderTime: timeText, }); + if (USE_DEXIE_DB) { + await db.settings.update(MASTER_SETTINGS_KEY, { + notifyingReminderMessage: message, + notifyingReminderTime: timeText, + }); + } this.notifyingReminder = true; this.notifyingReminderMessage = message || ""; this.notifyingReminderTime = timeText; @@ -1438,10 +1500,16 @@ export default class AccountViewView extends Vue { text: "", // unused, only here to satisfy type check callback: async (success) => { if (success) { - await db.settings.update(MASTER_SETTINGS_KEY, { + await databaseUtil.updateDefaultSettings({ notifyingReminderMessage: "", notifyingReminderTime: "", }); + if (USE_DEXIE_DB) { + await db.settings.update(MASTER_SETTINGS_KEY, { + notifyingReminderMessage: "", + notifyingReminderTime: "", + }); + } this.notifyingReminder = false; this.notifyingReminderMessage = ""; this.notifyingReminderTime = ""; @@ -1455,30 +1523,47 @@ export default class AccountViewView extends Vue { public async toggleHideRegisterPromptOnNewContact() { const newSetting = !this.hideRegisterPromptOnNewContact; - await db.open(); - await db.settings.update(MASTER_SETTINGS_KEY, { + await databaseUtil.updateDefaultSettings({ hideRegisterPromptOnNewContact: newSetting, }); + if (USE_DEXIE_DB) { + await db.open(); + await db.settings.update(MASTER_SETTINGS_KEY, { + hideRegisterPromptOnNewContact: newSetting, + }); + } this.hideRegisterPromptOnNewContact = newSetting; } public async updatePasskeyExpiration() { - await db.open(); - await db.settings.update(MASTER_SETTINGS_KEY, { + await databaseUtil.updateDefaultSettings({ passkeyExpirationMinutes: this.passkeyExpirationMinutes, }); + if (USE_DEXIE_DB) { + await db.open(); + await db.settings.update(MASTER_SETTINGS_KEY, { + passkeyExpirationMinutes: this.passkeyExpirationMinutes, + }); + } clearPasskeyToken(); this.passkeyExpirationDescription = tokenExpiryTimeDescription(); } public async turnOffNotifyingFlags() { // should tell the push server as well - await db.open(); - await db.settings.update(MASTER_SETTINGS_KEY, { + await databaseUtil.updateDefaultSettings({ notifyingNewActivityTime: "", notifyingReminderMessage: "", notifyingReminderTime: "", }); + if (USE_DEXIE_DB) { + await db.open(); + await db.settings.update(MASTER_SETTINGS_KEY, { + notifyingNewActivityTime: "", + notifyingReminderMessage: "", + notifyingReminderTime: "", + }); + } this.notifyingNewActivity = false; this.notifyingNewActivityTime = ""; this.notifyingReminder = false; @@ -1518,7 +1603,10 @@ export default class AccountViewView extends Vue { * @returns {Promise} The generated blob object. */ private async generateDatabaseBlob(): Promise { - return await db.export({ prettyJson: true }); + if (USE_DEXIE_DB) { + return await db.export({ prettyJson: true }); + } + throw new Error("Not implemented"); } /** @@ -1539,7 +1627,7 @@ export default class AccountViewView extends Vue { private downloadDatabaseBackup(url: string) { const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement; downloadAnchor.href = url; - downloadAnchor.download = `${db.name}-backup.json`; + downloadAnchor.download = `${AppString.APP_NAME_NO_SPACES}-backup.json`; downloadAnchor.click(); // doesn't work for some browsers, eg. DuckDuckGo } @@ -1620,7 +1708,8 @@ export default class AccountViewView extends Vue { */ async submitImportFile() { if (inputImportFileNameRef.value != null) { - await db + if (USE_DEXIE_DB) { + await db .delete() .then(async () => { // BulkError: settings.bulkAdd(): 1 of 21 operations failed. Errors: ConstraintError: Key already exists in the object store. @@ -1640,6 +1729,9 @@ export default class AccountViewView extends Vue { -1, ); }); + } else { + throw new Error("Not implemented"); + } } } @@ -1727,7 +1819,13 @@ export default class AccountViewView extends Vue { if (!this.isRegistered) { // the user was not known to be registered, but now they are (because we got no error) so let's record it try { - await updateAccountSettings(did, { isRegistered: true }); + await databaseUtil.updateAccountSettings(did, { isRegistered: true }); + if (USE_DEXIE_DB) { + await db.open(); + await db.settings.update(MASTER_SETTINGS_KEY, { + isRegistered: true, + }); + } this.isRegistered = true; } catch (err) { logger.error("Got an error updating settings:", err); @@ -1787,26 +1885,41 @@ export default class AccountViewView extends Vue { } async onClickSaveApiServer() { - await db.open(); - await db.settings.update(MASTER_SETTINGS_KEY, { + await databaseUtil.updateDefaultSettings({ apiServer: this.apiServerInput, }); + if (USE_DEXIE_DB) { + await db.open(); + await db.settings.update(MASTER_SETTINGS_KEY, { + apiServer: this.apiServerInput, + }); + } this.apiServer = this.apiServerInput; } async onClickSavePartnerServer() { - await db.open(); - await db.settings.update(MASTER_SETTINGS_KEY, { + await databaseUtil.updateDefaultSettings({ partnerApiServer: this.partnerApiServerInput, }); + if (USE_DEXIE_DB) { + await db.open(); + await db.settings.update(MASTER_SETTINGS_KEY, { + partnerApiServer: this.partnerApiServerInput, + }); + } this.partnerApiServer = this.partnerApiServerInput; } async onClickSavePushServer() { - await db.open(); - await db.settings.update(MASTER_SETTINGS_KEY, { + await databaseUtil.updateDefaultSettings({ webPushServer: this.webPushServerInput, }); + if (USE_DEXIE_DB) { + await db.open(); + await db.settings.update(MASTER_SETTINGS_KEY, { + webPushServer: this.webPushServerInput, + }); + } this.webPushServer = this.webPushServerInput; this.$notify( { @@ -1822,10 +1935,15 @@ export default class AccountViewView extends Vue { openImageDialog() { (this.$refs.imageMethodDialog as ImageMethodDialog).open( async (imgUrl) => { - await db.open(); - await db.settings.update(MASTER_SETTINGS_KEY, { + await databaseUtil.updateDefaultSettings({ profileImageUrl: imgUrl, }); + if (USE_DEXIE_DB) { + await db.open(); + await db.settings.update(MASTER_SETTINGS_KEY, { + profileImageUrl: imgUrl, + }); + } this.profileImageUrl = imgUrl; //console.log("Got image URL:", imgUrl); }, @@ -1886,10 +2004,15 @@ export default class AccountViewView extends Vue { // keep the imageUrl in localStorage so the user can try again if they want } - await db.open(); - await db.settings.update(MASTER_SETTINGS_KEY, { + await databaseUtil.updateDefaultSettings({ profileImageUrl: undefined, }); + if (USE_DEXIE_DB) { + await db.open(); + await db.settings.update(MASTER_SETTINGS_KEY, { + profileImageUrl: undefined, + }); + } this.profileImageUrl = undefined; } catch (error) { @@ -1978,7 +2101,10 @@ export default class AccountViewView extends Vue { throw Error("Profile not saved"); } } catch (error) { - logConsoleAndDb("Error saving profile: " + errorStringForLog(error)); + databaseUtil.logConsoleAndDb("Error saving profile: " + errorStringForLog(error)); + if (USE_DEXIE_DB) { + logConsoleAndDb("Error saving profile: " + errorStringForLog(error)); + } const errorMessage: string = error.response?.data?.error?.message || error.response?.data?.error || @@ -2068,7 +2194,10 @@ export default class AccountViewView extends Vue { throw Error("Profile not deleted"); } } catch (error) { - logConsoleAndDb("Error deleting profile: " + errorStringForLog(error)); + databaseUtil.logConsoleAndDb("Error deleting profile: " + errorStringForLog(error)); + if (USE_DEXIE_DB) { + logConsoleAndDb("Error deleting profile: " + errorStringForLog(error)); + } const errorMessage: string = error.response?.data?.error?.message || error.response?.data?.error ||