Browse Source

start adding the SQL approach to files, also using the Dexie approach if desired

pull/137/head
Trent Larson 2 weeks ago
parent
commit
11f2527b04
  1. 15
      doc/secure-storage-implementation.md
  2. 3
      src/constants/app.ts
  3. 65
      src/db/databaseUtil.ts
  4. 9
      src/libs/endorserServer.ts
  5. 137
      src/views/AccountViewView.vue

15
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<void> {
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

3
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.

65
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<Settings> {
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<Settings> {
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<Settings> {
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<Settings> {
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<void> {
const platform = PlatformServiceFactory.getInstance();
const todayKey = new Date().toDateString();

9
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,8 +1365,15 @@ export async function setVisibilityUtil(
if (resp.status === 200) {
const success = resp.data.success;
if (success) {
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 {
logger.error(

137
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<Blob>();
@ -1145,9 +1148,14 @@ export default class AccountViewView extends Vue {
if (error.status === 404) {
// this is ok: the profile is not yet created
} else {
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() {
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
await db.open();
const settings = await retrieveSettingsForActiveAccount();
settings = await retrieveSettingsForActiveAccount();
}
console.log("activeDid", settings.activeDid, "settings", settings);
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
@ -1268,43 +1280,68 @@ export default class AccountViewView extends Vue {
async toggleShowContactAmounts() {
this.showContactGives = !this.showContactGives;
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 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 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 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 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) {
return timeStr ? timeStr.substring(0, timeStr.indexOf("T")) : "?";
@ -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(
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 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 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 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 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 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 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 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,8 +1603,11 @@ export default class AccountViewView extends Vue {
* @returns {Promise<Blob>} The generated blob object.
*/
private async generateDatabaseBlob(): Promise<Blob> {
if (USE_DEXIE_DB) {
return await db.export({ prettyJson: true });
}
throw new Error("Not implemented");
}
/**
* Creates a temporary URL for a blob object.
@ -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,6 +1708,7 @@ export default class AccountViewView extends Vue {
*/
async submitImportFile() {
if (inputImportFileNameRef.value != null) {
if (USE_DEXIE_DB) {
await db
.delete()
.then(async () => {
@ -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 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 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 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 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 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) {
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) {
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 ||

Loading…
Cancel
Save