Browse Source

feat(backup): implement platform-specific database backup service

- Add Capacitor-specific DatabaseBackupService implementation
- Update PlatformServiceFactory to correctly load platform services
- Fix Filesystem API usage in Capacitor backup service
- Add detailed logging throughout backup process
- Improve error handling and cleanup of temporary files
- Update web platform backup implementation
- Add proper TypeScript types and documentation

This commit implements a robust platform-specific backup service that:
- Uses Capacitor's Filesystem and Share APIs for mobile platforms
- Properly handles file paths and URIs
- Includes comprehensive logging for debugging
- Cleans up temporary files after sharing
- Maintains consistent interface across platforms
db-backup-cross-platform
Matthew Raymer 2 months ago
parent
commit
a0cf9ea721
  1. 6
      .env.mobile
  2. 6
      .eslintrc.js
  3. BIN
      android/.gradle/buildOutputCleanup/buildOutputCleanup.lock
  4. 4
      android/.gradle/buildOutputCleanup/cache.properties
  5. BIN
      android/.gradle/file-system.probe
  6. 2
      android/app/capacitor.build.gradle
  7. 8
      android/app/src/main/assets/capacitor.plugins.json
  8. 2
      android/app/src/main/assets/public/index.html
  9. 2
      android/build.gradle
  10. 6
      android/capacitor.settings.gradle
  11. 2
      android/gradle/wrapper/gradle-wrapper.properties
  12. 42
      package-lock.json
  13. 2
      package.json
  14. 12
      src/components/EntityIcon.vue
  15. 20
      src/db/index.ts
  16. 14
      src/interfaces/identifier.ts
  17. 288
      src/interfaces/service.ts
  18. 69
      src/platforms/capacitor/DatabaseBackupService.ts
  19. 81
      src/services/DatabaseBackupService.ts
  20. 150
      src/services/PlatformServiceFactory.ts
  21. 42
      src/services/RateLimitsService.ts
  22. 6
      src/services/platforms/mobile/DatabaseBackupService.ts
  23. 31
      src/services/platforms/web/DatabaseBackupService.ts
  24. 262
      src/types/interfaces.ts
  25. 53
      src/utils/logger.ts
  26. 77
      src/views/AccountViewView.vue
  27. 2
      src/views/ContactGiftingView.vue
  28. 4
      src/views/HelpView.vue
  29. 2
      src/views/HomeView.vue
  30. 2
      src/views/ProjectViewView.vue
  31. 12
      vite.config.common.mts
  32. 76
      vite.config.web.mts
  33. 27
      vite.config.web.ts

6
.env.mobile

@ -1 +1,5 @@
PLATFORM=mobile PLATFORM=mobile
VITE_ENDORSER_API_URL=https://test-api.endorser.ch/api/v2/claim
VITE_PARTNER_API_URL=https://test-api.partner.ch/api/v2
VITE_IMAGE_API_URL=https://test-api.images.ch/api/v2
VITE_PUSH_SERVER_URL=https://test-api.push.ch/api/v2

6
.eslintrc.js

@ -26,6 +26,10 @@ module.exports = {
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn", "no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
"@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-unnecessary-type-constraint": "off" "@typescript-eslint/no-unnecessary-type-constraint": "off",
"@typescript-eslint/no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}]
}, },
}; };

BIN
android/.gradle/buildOutputCleanup/buildOutputCleanup.lock

Binary file not shown.

4
android/.gradle/buildOutputCleanup/cache.properties

@ -1,2 +1,2 @@
#Fri Mar 21 07:27:50 UTC 2025 #Thu Apr 03 08:01:00 UTC 2025
gradle.version=8.2.1 gradle.version=8.11.1

BIN
android/.gradle/file-system.probe

Binary file not shown.

2
android/app/capacitor.build.gradle

@ -10,6 +10,8 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies { dependencies {
implementation project(':capacitor-app') implementation project(':capacitor-app')
implementation project(':capacitor-filesystem')
implementation project(':capacitor-share')
} }

8
android/app/src/main/assets/capacitor.plugins.json

@ -2,5 +2,13 @@
{ {
"pkg": "@capacitor/app", "pkg": "@capacitor/app",
"classpath": "com.capacitorjs.plugins.app.AppPlugin" "classpath": "com.capacitorjs.plugins.app.AppPlugin"
},
{
"pkg": "@capacitor/filesystem",
"classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin"
},
{
"pkg": "@capacitor/share",
"classpath": "com.capacitorjs.plugins.share.SharePlugin"
} }
] ]

2
android/app/src/main/assets/public/index.html

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<title>TimeSafari</title> <title>TimeSafari</title>
<script type="module" crossorigin src="/assets/index-CZMUlUNO.js"></script> <script type="module" crossorigin src="/assets/index-CxCVZqQa.js"></script>
</head> </head>
<body> <body>
<noscript> <noscript>

2
android/build.gradle

@ -7,7 +7,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.2.1' classpath 'com.android.tools.build:gradle:8.9.0'
classpath 'com.google.gms:google-services:4.4.0' classpath 'com.google.gms:google-services:4.4.0'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong

6
android/capacitor.settings.gradle

@ -4,3 +4,9 @@ project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/
include ':capacitor-app' include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android') project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
include ':capacitor-filesystem'
project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android')
include ':capacitor-share'
project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android')

2
android/gradle/wrapper/gradle-wrapper.properties

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

42
package-lock.json

@ -11,7 +11,7 @@
"@capacitor/android": "^6.2.0", "@capacitor/android": "^6.2.0",
"@capacitor/app": "^6.0.0", "@capacitor/app": "^6.0.0",
"@capacitor/cli": "^6.2.0", "@capacitor/cli": "^6.2.0",
"@capacitor/core": "^6.2.0", "@capacitor/core": "^6.2.1",
"@capacitor/filesystem": "^6.0.3", "@capacitor/filesystem": "^6.0.3",
"@capacitor/ios": "^6.2.0", "@capacitor/ios": "^6.2.0",
"@capacitor/share": "^6.0.3", "@capacitor/share": "^6.0.3",
@ -13208,9 +13208,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001707", "version": "1.0.30001709",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001709.tgz",
"integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", "integrity": "sha512-NgL3vUTnDrPCZ3zTahp4fsugQ4dc7EKTSzwQDPEel6DMoMnfH2jhry9n2Zm8onbSR+f/QtKHFOA+iAQu4kbtWA==",
"devOptional": true, "devOptional": true,
"funding": [ "funding": [
{ {
@ -15612,9 +15612,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.129", "version": "1.5.130",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.129.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.130.tgz",
"integrity": "sha512-JlXUemX4s0+9f8mLqib/bHH8gOHf5elKS6KeWG3sk3xozb/JTq/RLXIv8OKUWiK4Ah00Wm88EFj5PYkFr4RUPA==", "integrity": "sha512-Ou2u7L9j2XLZbhqzyX0jWDj6gA8D3jIfVzt4rikLf3cGBa0VdReuFimBKS9tQJA4+XpeCxj1NoWlfBXzbMa9IA==",
"devOptional": true, "devOptional": true,
"license": "ISC" "license": "ISC"
}, },
@ -16037,14 +16037,14 @@
} }
}, },
"node_modules/eslint-plugin-prettier": { "node_modules/eslint-plugin-prettier": {
"version": "5.2.5", "version": "5.2.6",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.5.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz",
"integrity": "sha512-IKKP8R87pJyMl7WWamLgPkloB16dagPIdd2FjBDbyRYPKo93wS/NbCOPh6gH+ieNLC+XZrhJt/kWj0PS/DFdmg==", "integrity": "sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"prettier-linter-helpers": "^1.0.0", "prettier-linter-helpers": "^1.0.0",
"synckit": "^0.10.2" "synckit": "^0.11.0"
}, },
"engines": { "engines": {
"node": "^14.18.0 || >=16.0.0" "node": "^14.18.0 || >=16.0.0"
@ -18568,9 +18568,9 @@
} }
}, },
"node_modules/image-size": { "node_modules/image-size": {
"version": "1.2.0", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.0.tgz", "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
"integrity": "sha512-4S8fwbO6w3GeCVN6OPtA9I5IGKkcDMPcKndtUlpJuCwu7JLjtj7JZpwqLuyY2nrmQT3AWsCJLSKPsc2mPBSl3w==", "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true, "peer": true,
@ -23520,9 +23520,9 @@
} }
}, },
"node_modules/nostr-tools": { "node_modules/nostr-tools": {
"version": "2.11.1", "version": "2.12.0",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.11.1.tgz", "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.12.0.tgz",
"integrity": "sha512-+Oj5t+behIkU9kh3go5wg8Aa5oR7euBU9gOItUNapJe5Gaa+KPzMuTIN+rMRK3DaZ4Zt6RM4kR/ddwstzGKf7g==", "integrity": "sha512-pUWEb020gTvt1XZvTa8AKNIHWFapjsv2NKyk43Ez2nnvz6WSXsrTFE0XtkNLSRBjPn6EpxumKeNiVzLz74jNSA==",
"license": "Unlicense", "license": "Unlicense",
"dependencies": { "dependencies": {
"@noble/ciphers": "^0.5.1", "@noble/ciphers": "^0.5.1",
@ -28778,9 +28778,9 @@
} }
}, },
"node_modules/synckit": { "node_modules/synckit": {
"version": "0.10.3", "version": "0.11.1",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.10.3.tgz", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.1.tgz",
"integrity": "sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ==", "integrity": "sha512-fWZqNBZNNFp/7mTUy1fSsydhKsAKJ+u90Nk7kOK5Gcq9vObaqLBLjWFDBkyVU9Vvc6Y71VbOevMuGhqv02bT+Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -28791,7 +28791,7 @@
"node": "^14.18.0 || >=16.0.0" "node": "^14.18.0 || >=16.0.0"
}, },
"funding": { "funding": {
"url": "https://opencollective.com/unts" "url": "https://opencollective.com/synckit"
} }
}, },
"node_modules/synckit/node_modules/tslib": { "node_modules/synckit/node_modules/tslib": {

2
package.json

@ -45,7 +45,7 @@
"@capacitor/android": "^6.2.0", "@capacitor/android": "^6.2.0",
"@capacitor/app": "^6.0.0", "@capacitor/app": "^6.0.0",
"@capacitor/cli": "^6.2.0", "@capacitor/cli": "^6.2.0",
"@capacitor/core": "^6.2.0", "@capacitor/core": "^6.2.1",
"@capacitor/filesystem": "^6.0.3", "@capacitor/filesystem": "^6.0.3",
"@capacitor/ios": "^6.2.0", "@capacitor/ios": "^6.2.0",
"@capacitor/share": "^6.0.3", "@capacitor/share": "^6.0.3",

12
src/components/EntityIcon.vue

@ -7,10 +7,11 @@ import { createAvatar, StyleOptions } from "@dicebear/core";
import { avataaars } from "@dicebear/collection"; import { avataaars } from "@dicebear/collection";
import { Vue, Component, Prop } from "vue-facing-decorator"; import { Vue, Component, Prop } from "vue-facing-decorator";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import { logger } from "../utils/logger";
@Component @Component
export default class EntityIcon extends Vue { export default class EntityIcon extends Vue {
@Prop contact: Contact; @Prop({ required: false }) contact?: Contact;
@Prop entityId = ""; // overridden by contact.did or profileImageUrl @Prop entityId = ""; // overridden by contact.did or profileImageUrl
@Prop iconSize = 0; @Prop iconSize = 0;
@Prop profileImageUrl = ""; // overridden by contact.profileImageUrl @Prop profileImageUrl = ""; // overridden by contact.profileImageUrl
@ -22,7 +23,8 @@ export default class EntityIcon extends Vue {
} else { } else {
const identifier = this.contact?.did || this.entityId; const identifier = this.contact?.did || this.entityId;
if (!identifier) { if (!identifier) {
return `<img src="../src/assets/blank-square.svg" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`; const baseUrl = import.meta.env.VITE_BASE_URL || '/';
return `<img src="${baseUrl}assets/blank-square.svg" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`;
} }
// https://api.dicebear.com/8.x/avataaars/svg?seed= // https://api.dicebear.com/8.x/avataaars/svg?seed=
// ... does not render things with the same seed as this library. // ... does not render things with the same seed as this library.
@ -37,6 +39,12 @@ export default class EntityIcon extends Vue {
return svgString; return svgString;
} }
} }
mounted() {
logger.log('EntityIcon mounted, profileImageUrl:', this.profileImageUrl);
logger.log('EntityIcon mounted, entityId:', this.entityId);
logger.log('EntityIcon mounted, iconSize:', this.iconSize);
}
} }
</script> </script>
<style scoped></style> <style scoped></style>

20
src/db/index.ts

@ -1,5 +1,6 @@
import BaseDexie, { Table } from "dexie"; import BaseDexie, { Table } from "dexie";
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon"; import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
import { exportDB } from "dexie-export-import";
import * as R from "ramda"; import * as R from "ramda";
import { Account, AccountsSchema } from "./tables/accounts"; import { Account, AccountsSchema } from "./tables/accounts";
@ -26,19 +27,21 @@ type NonsensitiveTables = {
}; };
// Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings // Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings
export type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T; export type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T & { export: (options?: any) => Promise<Blob> };
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T; export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T & { export: (options?: any) => Promise<Blob> };
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> = export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> = BaseDexie & T & { export: (options?: any) => Promise<Blob> };
BaseDexie & T;
//// Initialize the DBs, starting with the sensitive ones. //// Initialize the DBs, starting with the sensitive ones.
// Initialize Dexie database for secret, which is then used to encrypt accountsDB // Initialize Dexie database for secret, which is then used to encrypt accountsDB
export const secretDB = new BaseDexie("TimeSafariSecret") as SecretDexie; export const secretDB = new BaseDexie("TimeSafariSecret") as SecretDexie;
secretDB.version(1).stores(SecretSchema); secretDB.version(1).stores(SecretSchema);
secretDB.export = (options) => exportDB(secretDB, options);
// Initialize Dexie database for accounts // Initialize Dexie database for accounts
const accountsDexie = new BaseDexie("TimeSafariAccounts") as SensitiveDexie; const accountsDexie = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
accountsDexie.version(1).stores(AccountsSchema);
accountsDexie.export = (options) => exportDB(accountsDexie, options);
// Instead of accountsDBPromise, use libs/util retrieveAccountMetadata or retrieveFullyDecryptedAccount // Instead of accountsDBPromise, use libs/util retrieveAccountMetadata or retrieveFullyDecryptedAccount
// so that it's clear whether the usage needs the private key inside. // so that it's clear whether the usage needs the private key inside.
@ -54,8 +57,15 @@ export const accountsDBPromise = useSecretAndInitializeAccountsDB(
//// Now initialize the other DB. //// Now initialize the other DB.
// Initialize Dexie databases for non-sensitive data // Initialize Dexie database for non-sensitive data
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie; export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
db.version(1).stores({
contacts: ContactSchema.contacts,
logs: LogSchema.logs,
settings: SettingsSchema.settings,
temp: TempSchema.temp,
});
db.export = (options) => exportDB(db, options);
// Only the tables with index modifications need listing. https://dexie.org/docs/Tutorial/Design#database-versioning // Only the tables with index modifications need listing. https://dexie.org/docs/Tutorial/Design#database-versioning

14
src/interfaces/identifier.ts

@ -15,8 +15,20 @@ export interface IKey {
meta?: KeyMeta; meta?: KeyMeta;
} }
export interface IService {
id: string;
type: string;
serviceEndpoint: string;
description?: string;
metadata?: {
version?: string;
capabilities?: string[];
config?: Record<string, unknown>;
};
}
export interface IIdentifier { export interface IIdentifier {
did: string; did: string;
keys: IKey[]; keys: IKey[];
services: any[]; services: IService[];
} }

288
src/interfaces/service.ts

@ -0,0 +1,288 @@
/**
* @file service.ts
* @description Service interfaces for Decentralized Identifiers (DIDs)
*
* This module defines the service interfaces used in the TimeSafari application.
* Services are associated with DIDs to provide additional functionality and endpoints.
*
* Architecture:
* 1. Base IService interface defines common service properties
* 2. Specialized interfaces extend IService for specific service types
* 3. Services are stored in IIdentifier.services array
* 4. Services are loaded and managed by PlatformServiceFactory
*
* Service Types:
* - EndorserService: Handles claims and endorsements
* - PushNotificationService: Manages web push notifications
* - ProfileService: Handles user profiles and settings
* - BackupService: Manages data backup and restore
*
* @see IIdentifier
* @see PlatformServiceFactory
* @see DatabaseBackupService
*/
/**
* Base interface for all DID services
*
* This interface defines the core properties that all services must implement.
* It follows the W3C DID specification for service endpoints.
*
* @example
* const service: IService = {
* id: 'endorser-service',
* type: 'EndorserService',
* serviceEndpoint: 'https://api.endorser.ch',
* description: 'Endorser service for claims and endorsements',
* metadata: {
* version: '1.0.0',
* capabilities: ['claims', 'endorsements'],
* config: { apiServer: 'https://api.endorser.ch' }
* }
* };
*/
export interface IService {
/**
* Unique identifier for the service
* @example 'endorser-service'
* @example 'push-notification-service'
*/
id: string;
/**
* Type of service
* @example 'EndorserService'
* @example 'PushNotificationService'
*/
type: string;
/**
* Endpoint URL for the service
* @example 'https://api.endorser.ch'
* @example 'https://push.timesafari.app'
*/
serviceEndpoint: string;
/**
* Optional human-readable description of the service
* @example 'Service for handling claims and endorsements'
*/
description?: string;
/**
* Optional metadata for service configuration
*/
metadata?: {
/**
* Service version in semantic versioning format
* @example '1.0.0'
*/
version?: string;
/**
* Array of service capabilities
* @example ['claims', 'endorsements']
* @example ['notifications', 'alerts']
*/
capabilities?: string[];
/**
* Service-specific configuration
* @example { apiServer: 'https://api.endorser.ch' }
*/
config?: Record<string, unknown>;
};
}
/**
* Service for handling claims and endorsements
*
* This service provides endpoints for:
* - Submitting claims
* - Managing endorsements
* - Checking rate limits
*
* @example
* const endorserService: IEndorserService = {
* id: 'endorser-service',
* type: 'EndorserService',
* serviceEndpoint: 'https://api.endorser.ch',
* metadata: {
* version: '1.0.0',
* capabilities: ['claims', 'endorsements'],
* config: {
* apiServer: 'https://api.endorser.ch',
* rateLimits: {
* claimsPerDay: 100,
* endorsementsPerDay: 1000
* }
* }
* }
* };
*/
export interface IEndorserService extends IService {
/** @override */
type: "EndorserService";
/** @override */
metadata: {
version: string;
capabilities: ["claims", "endorsements"];
config: {
/**
* API server URL
* @example 'https://api.endorser.ch'
*/
apiServer: string;
/**
* Optional rate limits
*/
rateLimits?: {
/**
* Maximum claims per day
* @default 100
*/
claimsPerDay: number;
/**
* Maximum endorsements per day
* @default 1000
*/
endorsementsPerDay: number;
};
};
};
}
/**
* Service for managing web push notifications
*
* This service provides endpoints for:
* - Registering push subscriptions
* - Sending push notifications
* - Managing notification preferences
*
* @example
* const pushService: IPushNotificationService = {
* id: 'push-service',
* type: 'PushNotificationService',
* serviceEndpoint: 'https://push.timesafari.app',
* metadata: {
* version: '1.0.0',
* capabilities: ['notifications'],
* config: {
* pushServer: 'https://push.timesafari.app',
* vapidPublicKey: '...'
* }
* }
* };
*/
export interface IPushNotificationService extends IService {
/** @override */
type: "PushNotificationService";
/** @override */
metadata: {
version: string;
capabilities: ["notifications"];
config: {
/**
* Push server URL
* @example 'https://push.timesafari.app'
*/
pushServer: string;
/**
* Optional VAPID public key for push notifications
*/
vapidPublicKey?: string;
};
};
}
/**
* Service for managing user profiles and settings
*
* This service provides endpoints for:
* - Managing user profiles
* - Updating user settings
* - Retrieving user preferences
*
* @example
* const profileService: IProfileService = {
* id: 'profile-service',
* type: 'ProfileService',
* serviceEndpoint: 'https://partner-api.endorser.ch',
* metadata: {
* version: '1.0.0',
* capabilities: ['profile', 'settings'],
* config: {
* partnerApiServer: 'https://partner-api.endorser.ch'
* }
* }
* };
*/
export interface IProfileService extends IService {
/** @override */
type: "ProfileService";
/** @override */
metadata: {
version: string;
capabilities: ["profile", "settings"];
config: {
/**
* Partner API server URL
* @example 'https://partner-api.endorser.ch'
*/
partnerApiServer: string;
};
};
}
/**
* Service for managing data backup and restore operations
*
* This service provides endpoints for:
* - Creating backups
* - Restoring from backups
* - Managing backup storage
*
* @example
* const backupService: IBackupService = {
* id: 'backup-service',
* type: 'BackupService',
* serviceEndpoint: 'https://backup.timesafari.app',
* metadata: {
* version: '1.0.0',
* capabilities: ['backup', 'restore'],
* config: {
* storageType: 'cloud',
* encryptionKey: '...'
* }
* }
* };
*/
export interface IBackupService extends IService {
/** @override */
type: "BackupService";
/** @override */
metadata: {
version: string;
capabilities: ["backup", "restore"];
config: {
/**
* Storage type for backups
* @default 'local'
*/
storageType: "local" | "cloud";
/**
* Optional encryption key for backups
*/
encryptionKey?: string;
};
};
}

69
src/platforms/capacitor/DatabaseBackupService.ts

@ -0,0 +1,69 @@
/**
* @file DatabaseBackupService.ts
* @description Capacitor-specific implementation of DatabaseBackupService
*
* This implementation handles database backup operations specifically for Capacitor
* platforms (Android/iOS). It uses the Filesystem and Share plugins to save and
* share the backup file.
*/
import { DatabaseBackupService as BaseDatabaseBackupService } from "../../services/DatabaseBackupService";
import { Filesystem, Directory } from "@capacitor/filesystem";
import { Share } from "@capacitor/share";
import { log, error } from "../../utils/logger";
export class DatabaseBackupService extends BaseDatabaseBackupService {
/**
* Handles the backup process for Capacitor platforms
*
* @param base64Data - Backup data in base64 format
* @param arrayBuffer - Backup data as ArrayBuffer
* @param blob - Backup data as Blob
*/
protected async handleBackup(
base64Data: string,
arrayBuffer: ArrayBuffer,
blob: Blob
): Promise<void> {
try {
log("Starting Capacitor backup process");
// Create a temporary file
const fileName = `timesafari-backup-${new Date().toISOString()}.json`;
const filePath = `backups/${fileName}`;
log("Writing backup file");
const result = await Filesystem.writeFile({
path: filePath,
data: base64Data,
directory: Directory.Cache,
recursive: true
});
log("Getting file path");
const fileInfo = await Filesystem.stat({
path: filePath,
directory: Directory.Cache
});
log("Sharing backup file");
await Share.share({
title: "TimeSafari Backup",
text: "Your TimeSafari backup file",
url: fileInfo.uri,
dialogTitle: "Share TimeSafari Backup"
});
log("Backup shared successfully");
// Clean up the temporary file
await Filesystem.deleteFile({
path: filePath,
directory: Directory.Cache
});
} catch (err) {
error("Error during Capacitor backup:", err);
throw err;
}
}
}

81
src/services/DatabaseBackupService.ts

@ -1,26 +1,95 @@
/** /**
* @file DatabaseBackupService.ts * @file DatabaseBackupService.ts
* @description Base service class for handling database backup operations * @description Base service class for handling database backup operations
* @author Matthew Raymer *
* @version 1.0.0 * This service implements the Template Method pattern to provide a common interface
* for database backup operations across different platforms. It defines the structure
* of backup operations while delegating platform-specific implementations to subclasses.
*
* Build Process Integration:
* 1. Platform-Specific Implementation:
* - Each platform (web, electron, capacitor) has its own implementation
* - Implementations are loaded dynamically via PlatformServiceFactory
* - Located in ./platforms/{platform}/DatabaseBackupService.ts
*
* 2. Build Configuration:
* - Vite config files (vite.config.*.mts) set VITE_PLATFORM
* - PlatformServiceFactory uses this to load correct implementation
* - Build process creates separate chunks for each platform
*
* 3. Data Handling:
* - Supports multiple data formats (base64, ArrayBuffer, Blob)
* - Platform implementations handle format conversion
* - Ensures consistent backup format across platforms
*
* Usage:
* - Create backup: DatabaseBackupService.createAndShareBackup(data)
* - Platform-specific: new WebDatabaseBackupService().handleBackup()
*
* @see PlatformServiceFactory.ts
* @see vite.config.web.mts
* @see vite.config.electron.mts
* @see vite.config.capacitor.mts
*/ */
import { PlatformServiceFactory } from "./PlatformServiceFactory"; import { PlatformServiceFactory } from "./PlatformServiceFactory";
import { log, error } from "../utils/logger";
export class DatabaseBackupService { export class DatabaseBackupService {
protected async handleBackup(): Promise<void> { /**
* Template method that must be implemented by platform-specific services
* @param base64Data - Backup data in base64 format
* @param arrayBuffer - Backup data as ArrayBuffer
* @param blob - Backup data as Blob
* @throws Error if not implemented by subclass
*/
protected async handleBackup(
_base64Data: string,
_arrayBuffer: ArrayBuffer,
_blob: Blob,
): Promise<void> {
throw new Error( throw new Error(
"handleBackup must be implemented by platform-specific service", "handleBackup must be implemented by platform-specific service",
); );
} }
/**
* Factory method to create and share a backup
* Uses PlatformServiceFactory to get platform-specific implementation
*
* @param base64Data - Backup data in base64 format
* @param arrayBuffer - Backup data as ArrayBuffer
* @param blob - Backup data as Blob
* @returns Promise that resolves when backup is complete
*/
public static async createAndShareBackup( public static async createAndShareBackup(
base64Data: string, base64Data: string,
arrayBuffer: ArrayBuffer, arrayBuffer: ArrayBuffer,
blob: Blob, blob: Blob
): Promise<void> { ): Promise<void> {
try {
log('Creating platform-specific backup service');
const backupService = await this.getPlatformSpecificBackupService();
log('Backup service created successfully');
log('Executing platform-specific backup');
await backupService.handleBackup(base64Data, arrayBuffer, blob);
log('Backup completed successfully');
} catch (err) {
error('Error during backup creation:', err);
if (err instanceof Error) {
error('Error details:', {
name: err.name,
message: err.message,
stack: err.stack
});
}
throw err;
}
}
private static async getPlatformSpecificBackupService(): Promise<DatabaseBackupService> {
const factory = PlatformServiceFactory.getInstance(); const factory = PlatformServiceFactory.getInstance();
const service = await factory.createDatabaseBackupService(); return await factory.createDatabaseBackupService();
await service.handleBackup(base64Data, arrayBuffer, blob);
} }
} }

150
src/services/PlatformServiceFactory.ts

@ -1,20 +1,102 @@
/** /**
* @file PlatformServiceFactory.ts * @file PlatformServiceFactory.ts
* @description Factory for creating platform-specific service implementations * @description Factory for creating platform-specific service implementations
* @author Matthew Raymer *
* @version 1.0.0 * This factory implements the Abstract Factory pattern to create platform-specific
* implementations of services. It uses Vite's dynamic import feature to load the
* appropriate implementation based on the current platform (web, electron, etc.).
*
* Architecture:
* 1. Singleton Pattern:
* - Ensures only one factory instance exists
* - Manages platform-specific service instances
* - Maintains consistent state across the application
*
* 2. Dynamic Loading:
* - Uses Vite's dynamic import for platform-specific code
* - Loads services on-demand based on platform
* - Handles platform detection and service instantiation
*
* 3. Platform Detection:
* - Uses VITE_PLATFORM environment variable
* - Supports web, electron, and capacitor platforms
* - Falls back to 'web' if platform is not specified
*
* Usage:
* ```typescript
* // Get factory instance
* const factory = PlatformServiceFactory.getInstance();
*
* // Create platform-specific service
* const backupService = await factory.createDatabaseBackupService();
* ```
*
* @see vite.config.web.mts
* @see vite.config.electron.mts
* @see vite.config.capacitor.mts
* @see DatabaseBackupService
*/ */
import { DatabaseBackupService } from "./DatabaseBackupService"; import { DatabaseBackupService } from "./DatabaseBackupService";
import { logger } from "../utils/logger";
/**
* Factory class for creating platform-specific service implementations
*
* This class manages the creation and instantiation of platform-specific
* service implementations. It uses the Abstract Factory pattern to provide
* a consistent interface for creating services across different platforms.
*
* @example
* ```typescript
* // Get factory instance
* const factory = PlatformServiceFactory.getInstance();
*
* // Create platform-specific service
* const backupService = await factory.createDatabaseBackupService();
*
* // Use the service
* await backupService.handleBackup(data);
* ```
*/
export class PlatformServiceFactory { export class PlatformServiceFactory {
/**
* Singleton instance of the factory
* @private
*/
private static instance: PlatformServiceFactory; private static instance: PlatformServiceFactory;
/**
* Current platform identifier
* @private
*/
private platform: string; private platform: string;
/**
* Private constructor to enforce singleton pattern
*
* Initializes the factory with the current platform from environment variables.
* Falls back to 'web' if no platform is specified.
*
* @private
*/
private constructor() { private constructor() {
this.platform = import.meta.env.VITE_PLATFORM || "web"; this.platform = import.meta.env.VITE_PLATFORM || "web";
} }
/**
* Gets the singleton instance of the factory
*
* Creates a new instance if one doesn't exist, otherwise returns
* the existing instance.
*
* @returns {PlatformServiceFactory} The singleton factory instance
*
* @example
* ```typescript
* const factory = PlatformServiceFactory.getInstance();
* ```
*/
public static getInstance(): PlatformServiceFactory { public static getInstance(): PlatformServiceFactory {
if (!PlatformServiceFactory.instance) { if (!PlatformServiceFactory.instance) {
PlatformServiceFactory.instance = new PlatformServiceFactory(); PlatformServiceFactory.instance = new PlatformServiceFactory();
@ -22,19 +104,75 @@ export class PlatformServiceFactory {
return PlatformServiceFactory.instance; return PlatformServiceFactory.instance;
} }
/**
* Creates a platform-specific database backup service
*
* Dynamically loads and instantiates the appropriate implementation
* based on the current platform. The implementation is loaded from
* the platforms/{platform}/DatabaseBackupService.js file.
*
* @returns {Promise<DatabaseBackupService>} A promise that resolves to a platform-specific backup service
* @throws {Error} If the service fails to load or instantiate
*
* @example
* ```typescript
* const factory = PlatformServiceFactory.getInstance();
* try {
* const backupService = await factory.createDatabaseBackupService();
* await backupService.handleBackup(data);
* } catch (error) {
* logger.error('Failed to create backup service:', error);
* }
* ```
*/
public async createDatabaseBackupService(): Promise<DatabaseBackupService> { public async createDatabaseBackupService(): Promise<DatabaseBackupService> {
try { try {
// Use Vite's dynamic import for platform-specific implementation logger.log(`Loading platform-specific service for ${this.platform}`);
const { default: PlatformService } = await import( // Update the import path to point to the correct location
`./platforms/${this.platform}/DatabaseBackupService` const { DatabaseBackupService: PlatformService } = await import(
`../platforms/${this.platform}/DatabaseBackupService`
); );
logger.log('Platform service loaded successfully');
return new PlatformService(); return new PlatformService();
} catch (error) { } catch (error) {
console.error( logger.error(
`Failed to load platform-specific service for ${this.platform}:`, `Failed to load platform-specific service for ${this.platform}:`,
error, error,
); );
throw error; throw error;
} }
} }
/**
* Gets the current platform identifier
*
* @returns {string} The current platform identifier
*
* @example
* ```typescript
* const factory = PlatformServiceFactory.getInstance();
* logger.log(factory.getPlatform()); // 'web', 'electron', or 'capacitor'
* ```
*/
public getPlatform(): string {
return this.platform;
}
/**
* Sets the current platform identifier
*
* This method is primarily used for testing purposes to override
* the platform detection. Use with caution in production code.
*
* @param {string} platform - The platform identifier to set
*
* @example
* ```typescript
* const factory = PlatformServiceFactory.getInstance();
* factory.setPlatform('electron'); // For testing purposes only
* ```
*/
public setPlatform(platform: string): void {
this.platform = platform;
}
} }

42
src/services/RateLimitsService.ts

@ -8,32 +8,40 @@
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { getHeaders } from "../libs/endorserServer"; import { getHeaders } from "../libs/endorserServer";
import type { EndorserRateLimits, ImageRateLimits } from "../interfaces/limits"; import type { EndorserRateLimits, ImageRateLimits } from "../interfaces/limits";
import axios from "axios";
export class RateLimitsService { export class RateLimitsService {
/** /**
* Fetches rate limits for a given DID * Fetches rate limits for a given DID
* @param apiServer - The API server URL * @param apiServer - The API server URL
* @param activeDid - The user's active DID * @param did - The user's DID
* @returns Promise<EndorserRateLimits> * @returns Promise<EndorserRateLimits>
*/ */
static async fetchRateLimits( static async fetchRateLimits(apiServer: string, did: string): Promise<EndorserRateLimits> {
apiServer: string, logger.log('Fetching rate limits for DID:', did);
activeDid: string, logger.log('Using API server:', apiServer);
): Promise<EndorserRateLimits> {
try { try {
const headers = await getHeaders(activeDid); const headers = await getHeaders(did);
const response = await fetch( const response = await axios.get(`${apiServer}/api/v2/rate-limits/${did}`, { headers });
`${apiServer}/api/endorser/rateLimits/${activeDid}`, logger.log('Rate limits response:', response.data);
{ headers }, return response.data;
);
if (!response.ok) {
throw new Error(`Failed to fetch rate limits: ${response.statusText}`);
}
return await response.json();
} catch (error) { } catch (error) {
logger.error("Error fetching rate limits:", error); if (axios.isAxiosError(error) && (error.response?.status === 400 || error.response?.status === 404)) {
const errorData = error.response.data as { error?: { message?: string, code?: string } };
if (errorData.error?.code === 'UNREGISTERED_USER' || error.response?.status === 404) {
logger.log('User is not registered, returning default limits');
return {
doneClaimsThisWeek: "0",
maxClaimsPerWeek: "0",
nextWeekBeginDateTime: new Date().toISOString(),
doneRegistrationsThisMonth: "0",
maxRegistrationsPerMonth: "0",
nextMonthBeginDateTime: new Date().toISOString()
};
}
}
logger.error('Error fetching rate limits:', error);
throw error; throw error;
} }
} }

6
src/services/platforms/mobile/DatabaseBackupService.ts

@ -10,7 +10,11 @@ import { Filesystem } from "@capacitor/filesystem";
import { Share } from "@capacitor/share"; import { Share } from "@capacitor/share";
export default class MobileDatabaseBackupService extends DatabaseBackupService { export default class MobileDatabaseBackupService extends DatabaseBackupService {
protected async handleBackup(base64Data: string): Promise<void> { protected async handleBackup(
base64Data: string,
_arrayBuffer: ArrayBuffer,
_blob: Blob,
): Promise<void> {
// Mobile platform handling // Mobile platform handling
const fileName = `database-backup-${new Date().toISOString()}.json`; const fileName = `database-backup-${new Date().toISOString()}.json`;
const path = `backups/${fileName}`; const path = `backups/${fileName}`;

31
src/services/platforms/web/DatabaseBackupService.ts

@ -6,17 +6,28 @@
*/ */
import { DatabaseBackupService } from "../../DatabaseBackupService"; import { DatabaseBackupService } from "../../DatabaseBackupService";
import { log, error } from "../../../utils/logger";
export default class WebDatabaseBackupService extends DatabaseBackupService { export default class WebDatabaseBackupService extends DatabaseBackupService {
protected async handleBackup(blob: Blob): Promise<void> { protected async handleBackup(
// Web platform handling _base64Data: string,
const url = URL.createObjectURL(blob); _arrayBuffer: ArrayBuffer,
const a = document.createElement("a"); blob: Blob
a.href = url; ): Promise<void> {
a.download = `database-backup-${new Date().toISOString()}.json`; try {
document.body.appendChild(a); log('Starting web platform backup');
a.click(); const url = URL.createObjectURL(blob);
document.body.removeChild(a); const a = document.createElement("a");
URL.revokeObjectURL(url); a.href = url;
a.download = `database-backup-${new Date().toISOString()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
log('Web platform backup completed');
} catch (err) {
error('Error during web platform backup:', err);
throw err;
}
} }
} }

262
src/types/interfaces.ts

@ -1,28 +1,286 @@
/** /**
* Type declarations for custom interfaces used in the application. * @file interfaces.ts
* @author Matthew Raymer * @description Core type declarations for the TimeSafari application
*
* This module defines the core interfaces and types used throughout the application.
* It serves as the central location for type definitions that are shared across
* multiple components and services.
*
* Architecture:
* 1. DID (Decentralized Identifier) Types:
* - IIdentifier: Core DID structure
* - IKey: Cryptographic key information
* - IService: Service endpoints and capabilities
*
* 2. Verifiable Credential Types:
* - GenericCredWrapper: Base wrapper for all credentials
* - GiveVerifiableCredential: Gift-related credentials
* - OfferVerifiableCredential: Offer-related credentials
* - RegisterVerifiableCredential: Registration credentials
*
* 3. Service Types:
* - EndorserService: Claims and endorsements
* - PushNotificationService: Web push notifications
* - ProfileService: User profiles
* - BackupService: Data backup
*
* @see src/interfaces/identifier.ts
* @see src/interfaces/claims.ts
* @see src/interfaces/limits.ts
*/ */
import { GiveVerifiableCredential } from "../interfaces"; import { GiveVerifiableCredential } from "../interfaces";
/**
* Interface for a Decentralized Identifier (DID)
*
* This interface defines the structure of a DID, which is a unique identifier
* that can be used to look up a DID document containing information associated
* with the DID, such as public keys and service endpoints.
*
* @example
* ```typescript
* const identifier: IIdentifier = {
* did: 'did:ethr:0x123...',
* provider: 'ethr',
* keys: [{
* kid: 'keys-1',
* kms: 'local',
* type: 'Secp256k1',
* publicKeyHex: '0x...',
* meta: { derivationPath: "m/44'/60'/0'/0/0" }
* }],
* services: [{
* id: 'endorser-service',
* type: 'EndorserService',
* serviceEndpoint: 'https://api.endorser.ch'
* }]
* };
* ```
*/
export interface IIdentifier { export interface IIdentifier {
/**
* The DID string in the format 'did:method:identifier'
* @example 'did:ethr:0x123...'
*/
did: string; did: string;
/**
* The DID method provider
* @example 'ethr'
*/
provider: string; provider: string;
/**
* Array of cryptographic keys associated with the DID
*/
keys: Array<{ keys: Array<{
/**
* Key identifier
* @example 'keys-1'
*/
kid: string; kid: string;
/**
* Key management system
* @example 'local'
*/
kms: string; kms: string;
/**
* Key type
* @example 'Secp256k1'
*/
type: string; type: string;
/**
* Public key in hexadecimal format
* @example '0x...'
*/
publicKeyHex: string; publicKeyHex: string;
/**
* Optional metadata about the key
*/
meta?: any; meta?: any;
}>; }>;
/**
* Array of service endpoints associated with the DID
*/
services: Array<{ services: Array<{
/**
* Service identifier
* @example 'endorser-service'
*/
id: string; id: string;
/**
* Service type
* @example 'EndorserService'
*/
type: string; type: string;
/**
* Service endpoint URL
* @example 'https://api.endorser.ch'
*/
serviceEndpoint: string; serviceEndpoint: string;
/**
* Optional service description
*/
description?: string; description?: string;
}>; }>;
} }
/**
* Interface for a cryptographic key
*
* This interface defines the structure of a cryptographic key used in the
* DID system. It includes both public and private key information, along
* with metadata about the key's purpose and derivation.
*
* @example
* ```typescript
* const key: IKey = {
* id: 'did:ethr:0x123...#keys-1',
* type: 'Secp256k1VerificationKey2018',
* controller: 'did:ethr:0x123...',
* ethereumAddress: '0x123...',
* publicKeyHex: '0x...',
* privateKeyHex: '0x...',
* meta: {
* derivationPath: "m/44'/60'/0'/0/0"
* }
* };
* ```
*/
export interface IKey {
/**
* Unique identifier for the key
* @example 'did:ethr:0x123...#keys-1'
*/
id: string;
/**
* Key type specification
* @example 'Secp256k1VerificationKey2018'
*/
type: string;
/**
* DID that controls this key
* @example 'did:ethr:0x123...'
*/
controller: string;
/**
* Associated Ethereum address
* @example '0x123...'
*/
ethereumAddress: string;
/**
* Public key in hexadecimal format
* @example '0x...'
*/
publicKeyHex: string;
/**
* Private key in hexadecimal format
* @example '0x...'
*/
privateKeyHex: string;
/**
* Optional metadata about the key
*/
meta?: {
/**
* HD wallet derivation path
* @example "m/44'/60'/0'/0/0"
*/
derivationPath?: string;
/**
* Additional key metadata
*/
[key: string]: unknown;
};
}
/**
* Interface for a service endpoint
*
* This interface defines the structure of a service endpoint that can be
* associated with a DID. Services provide additional functionality and
* endpoints for DID operations.
*
* @example
* ```typescript
* const service: IService = {
* id: 'endorser-service',
* type: 'EndorserService',
* serviceEndpoint: 'https://api.endorser.ch',
* description: 'Service for handling claims and endorsements',
* metadata: {
* version: '1.0.0',
* capabilities: ['claims', 'endorsements'],
* config: {
* apiServer: 'https://api.endorser.ch'
* }
* }
* };
* ```
*/
export interface IService {
/**
* Unique identifier for the service
* @example 'endorser-service'
*/
id: string;
/**
* Type of service
* @example 'EndorserService'
*/
type: string;
/**
* Service endpoint URL
* @example 'https://api.endorser.ch'
*/
serviceEndpoint: string;
/**
* Optional human-readable description
*/
description?: string;
/**
* Optional service metadata
*/
metadata?: {
/**
* Service version
* @example '1.0.0'
*/
version?: string;
/**
* Array of service capabilities
* @example ['claims', 'endorsements']
*/
capabilities?: string[];
/**
* Service-specific configuration
*/
config?: Record<string, unknown>;
};
}
export interface ExportProgress { export interface ExportProgress {
status: "preparing" | "exporting" | "complete" | "error"; status: "preparing" | "exporting" | "complete" | "error";
message?: string; message?: string;

53
src/utils/logger.ts

@ -19,28 +19,55 @@ function safeStringify(obj: unknown) {
}); });
} }
function formatMessage(message: string, ...args: unknown[]): string {
const prefix = '[TimeSafari]';
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
return `${prefix} ${message}${argsString}`;
}
export const logger = { export const logger = {
log: (message: string, ...args: unknown[]) => { log: (message: string, ...args: unknown[]) => {
if (process.env.NODE_ENV !== "production") { if (process.env.NODE_ENV !== "production") {
// eslint-disable-next-line no-console const formattedMessage = formatMessage(message, ...args);
console.log(message, ...args); console.log(formattedMessage);
const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; logToDb(message + (args.length > 0 ? " - " + safeStringify(args) : ""));
logToDb(message + argsString);
} }
}, },
warn: (message: string, ...args: unknown[]) => { warn: (message: string, ...args: unknown[]) => {
if (process.env.NODE_ENV !== "production") { if (process.env.NODE_ENV !== "production") {
// eslint-disable-next-line no-console const formattedMessage = formatMessage(message, ...args);
console.warn(message, ...args); console.warn(formattedMessage);
const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; logToDb(message + (args.length > 0 ? " - " + safeStringify(args) : ""));
logToDb(message + argsString);
} }
}, },
error: (message: string, ...args: unknown[]) => { error: (message: string, ...args: unknown[]) => {
// Errors will always be logged const formattedMessage = formatMessage(message, ...args);
// eslint-disable-next-line no-console console.error(formattedMessage);
console.error(message, ...args); logToDb(message + (args.length > 0 ? " - " + safeStringify(args) : ""));
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
logToDb(message + argsString);
}, },
}; };
export function log(...args: any[]) {
const message = formatMessage(args[0], ...args.slice(1));
console.log(message);
}
export function error(...args: any[]) {
const message = formatMessage(args[0], ...args.slice(1));
console.error(message);
}
export function warn(...args: any[]) {
const message = formatMessage(args[0], ...args.slice(1));
console.warn(message);
}
export function info(...args: any[]) {
const message = formatMessage(args[0], ...args.slice(1));
console.info(message);
}
export function debug(...args: any[]) {
const message = formatMessage(args[0], ...args.slice(1));
console.debug(message);
}

77
src/views/AccountViewView.vue

@ -836,7 +836,7 @@ import "leaflet/dist/leaflet.css";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { Buffer } from "buffer/"; import { Buffer } from "buffer/";
import Dexie from "dexie"; import Dexie from "dexie";
import "dexie-export-import"; import { exportDB } from "dexie-export-import";
import * as R from "ramda"; import * as R from "ramda";
import type { IIdentifier, UserProfile } from "@/types/interfaces"; import type { IIdentifier, UserProfile } from "@/types/interfaces";
import { ref } from "vue"; import { ref } from "vue";
@ -889,6 +889,7 @@ import { DatabaseBackupService } from "../services/DatabaseBackupService";
import { ProfileService } from "../services/ProfileService"; import { ProfileService } from "../services/ProfileService";
import { RateLimitsService } from "../services/RateLimitsService"; import { RateLimitsService } from "../services/RateLimitsService";
import ProfileSection from "../components/ProfileSection.vue"; import ProfileSection from "../components/ProfileSection.vue";
import { log, error } from '../utils/logger';
const inputImportFileNameRef = ref<Blob>(); const inputImportFileNameRef = ref<Blob>();
@ -943,6 +944,8 @@ export default class AccountViewView extends Vue {
DEFAULT_IMAGE_API_SERVER = DEFAULT_IMAGE_API_SERVER; DEFAULT_IMAGE_API_SERVER = DEFAULT_IMAGE_API_SERVER;
DEFAULT_PARTNER_API_SERVER = DEFAULT_PARTNER_API_SERVER; DEFAULT_PARTNER_API_SERVER = DEFAULT_PARTNER_API_SERVER;
private db = db;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
apiServerInput = ""; apiServerInput = "";
@ -1400,42 +1403,52 @@ export default class AccountViewView extends Vue {
*/ */
async exportDatabase() { async exportDatabase() {
try { try {
// Generate database blob logger.log("Starting database export process");
const blob = await (Dexie as any).export(db, {
prettyJson: true, const db = await this.getDatabase();
logger.log("Database instance:", {
name: db.name,
version: db.verno,
tables: db.tables.map((t: { name: string }) => t.name)
}); });
// Convert blob to base64 for mobile platforms if (!db.export) {
const arrayBuffer = await blob.arrayBuffer(); logger.error("Database export method not available");
const base64Data = Buffer.from(arrayBuffer).toString("base64"); return;
}
await DatabaseBackupService.createAndShareBackup( logger.log("Creating export blob");
base64Data, const blob = await db.export();
arrayBuffer, logger.log("Blob created:", {
blob, type: blob.type,
); size: blob.size
});
this.$notify( logger.log("Converting blob to base64");
{ const base64Data = await new Promise<string>((resolve, reject) => {
group: "alert", const reader = new FileReader();
type: "success", reader.onload = () => resolve(reader.result as string);
title: "Export Complete", reader.onerror = reject;
text: "Your database has been exported successfully.", reader.readAsDataURL(blob);
}, });
5000, logger.log("Base64 data length:", base64Data.length);
);
} catch (error: unknown) { logger.log("Converting blob to ArrayBuffer");
if (error instanceof Error) { const arrayBuffer = await blob.arrayBuffer();
const errorMessage = error.message; logger.log("ArrayBuffer size:", arrayBuffer.byteLength);
this.limitsMessage = errorMessage || "Bad server response.";
logger.error("Got bad response retrieving limits:", error); logger.log("Creating and sharing backup");
} else { await this.createAndShareBackup(base64Data, arrayBuffer, blob);
this.limitsMessage = "Got an error retrieving limits."; logger.log("Backup creation and sharing completed");
logger.error("Got some error retrieving limits:", error); } catch (error) {
} logger.error("Error during database export:", error);
} }
} }
async createAndShareBackup(base64Data: string, arrayBuffer: ArrayBuffer, blob: Blob) {
await DatabaseBackupService.createAndShareBackup(base64Data, arrayBuffer, blob);
}
async uploadImportFile(event: Event) { async uploadImportFile(event: Event) {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
if (target.files) { if (target.files) {
@ -1942,5 +1955,9 @@ export default class AccountViewView extends Vue {
this.userProfileLongitude = updatedProfile.location?.lng || 0; this.userProfileLongitude = updatedProfile.location?.lng || 0;
this.includeUserProfileLocation = !!updatedProfile.location; this.includeUserProfileLocation = !!updatedProfile.location;
} }
async getDatabase() {
return await db;
}
} }
</script> </script>

2
src/views/ContactGiftingView.vue

@ -21,7 +21,7 @@
<h2 class="text-base flex gap-4 items-center"> <h2 class="text-base flex gap-4 items-center">
<span class="grow"> <span class="grow">
<img <img
src="../assets/blank-square.svg" src="@/assets/blank-square.svg"
width="32" width="32"
class="inline-block align-middle border border-slate-300 rounded-md mr-1" class="inline-block align-middle border border-slate-300 rounded-md mr-1"
/> />

4
src/views/HelpView.vue

@ -484,13 +484,13 @@
<a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" target="_blank" rel="license noopener noreferrer"> <a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" target="_blank" rel="license noopener noreferrer">
<span class="text-blue-500 mr-1">CC0 1.0</span> <span class="text-blue-500 mr-1">CC0 1.0</span>
<img <img
src="../assets/help/creative-commons-circle.svg" src="@/assets/help/creative-commons-circle.svg"
alt="CC circle" alt="CC circle"
width="20" width="20"
class="display: inline" class="display: inline"
/> />
<img <img
src="../assets/help/creative-commons-zero.svg" src="@/assets/help/creative-commons-zero.svg"
alt="CC zero" alt="CC zero"
width="20" width="20"
style="display: inline" style="display: inline"

2
src/views/HomeView.vue

@ -135,7 +135,7 @@ Raymer * @version 1.0.0 */
> >
<li @click="openDialog()"> <li @click="openDialog()">
<img <img
src="../assets/blank-square.svg" src="@/assets/blank-square.svg"
class="mx-auto border border-blue-500 rounded-md mb-1 cursor-pointer" class="mx-auto border border-blue-500 rounded-md mb-1 cursor-pointer"
/> />
<h3 <h3

2
src/views/ProjectViewView.vue

@ -220,7 +220,7 @@
</li> </li>
<li @click="openGiftDialogToProject()"> <li @click="openGiftDialogToProject()">
<img <img
src="../assets/blank-square.svg" src="@/assets/blank-square.svg"
class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer" class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer"
/> />
<h3 <h3

12
vite.config.common.mts

@ -36,7 +36,15 @@ export async function createBuildConfig(mode: string) {
assetsDir: 'assets', assetsDir: 'assets',
chunkSizeWarningLimit: 1000, chunkSizeWarningLimit: 1000,
rollupOptions: { rollupOptions: {
external: isCapacitor ? ['@capacitor/app'] : [] external: isCapacitor ? ['@capacitor/app'] : [],
output: {
assetFileNames: (assetInfo) => {
if (assetInfo.name?.endsWith('.svg')) {
return 'assets/[name][extname]';
}
return 'assets/[name]-[hash][extname]';
}
}
} }
}, },
define: { define: {
@ -44,6 +52,8 @@ export async function createBuildConfig(mode: string) {
'process.env.VITE_PLATFORM': JSON.stringify(mode), 'process.env.VITE_PLATFORM': JSON.stringify(mode),
'process.env.VITE_PWA_ENABLED': JSON.stringify(!(isElectron || isPyWebView || isCapacitor)), 'process.env.VITE_PWA_ENABLED': JSON.stringify(!(isElectron || isPyWebView || isCapacitor)),
__dirname: isElectron ? JSON.stringify(process.cwd()) : '""', __dirname: isElectron ? JSON.stringify(process.cwd()) : '""',
'process.env.VITE_ASSET_URL': JSON.stringify(isCapacitor ? './assets/' : '/assets/'),
'process.env.VITE_BASE_URL': JSON.stringify(isCapacitor ? './' : '/')
}, },
resolve: { resolve: {
alias: { alias: {

76
vite.config.web.mts

@ -1,20 +1,92 @@
import { defineConfig, mergeConfig } from "vite"; /**
* @file vite.config.web.mts
* @description Vite configuration for web platform builds
*
* This configuration file defines how the application is built for web platforms.
* It extends the base configuration with web-specific settings and optimizations.
*
* Build Process Integration:
* 1. Configuration Loading:
* - Loads environment variables based on build mode
* - Merges base configuration from vite.config.common.mts
* - Loads application-specific configuration
*
* 2. Platform Definition:
* - Sets VITE_PLATFORM environment variable to 'web'
* - Used by PlatformServiceFactory to load web-specific implementations
*
* 3. Build Output:
* - Outputs to 'dist/web' directory
* - Creates vendor chunk for Vue-related dependencies
* - Enables PWA features with auto-update capability
*
* 4. Development vs Production:
* - Development: Enables source maps and development features
* - Production: Optimizes chunks and enables PWA features
*
* Usage:
* - Development: npm run dev
* - Production: npm run build:web
*
* @see vite.config.common.mts
* @see vite.config.utils.mts
* @see PlatformServiceFactory.ts
*/
import { defineConfig, mergeConfig, loadEnv } from "vite";
import { VitePWA } from "vite-plugin-pwa"; import { VitePWA } from "vite-plugin-pwa";
import { createBuildConfig } from "./vite.config.common.mts"; import { createBuildConfig } from "./vite.config.common.mts";
import { loadAppConfig } from "./vite.config.utils.mts"; import { loadAppConfig } from "./vite.config.utils.mts";
export default defineConfig(async () => { export default defineConfig(async ({ mode }) => {
// Load environment variables based on build mode
const env = loadEnv(mode, process.cwd(), '');
// Load base configuration for web platform
const baseConfig = await createBuildConfig('web'); const baseConfig = await createBuildConfig('web');
// Load application-specific configuration
const appConfig = await loadAppConfig(); const appConfig = await loadAppConfig();
// Merge configurations with web-specific settings
return mergeConfig(baseConfig, { return mergeConfig(baseConfig, {
// Define platform-specific environment variables
define: {
'import.meta.env.VITE_PLATFORM': JSON.stringify('web'),
},
// Build output configuration
build: {
// Output directory for web builds
outDir: 'dist/web',
// Rollup-specific options
rollupOptions: {
output: {
// Create separate vendor chunk for Vue-related dependencies
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia'],
}
}
}
},
// Vite plugins configuration
plugins: [ plugins: [
// Progressive Web App configuration
VitePWA({ VitePWA({
// Auto-update service worker
registerType: 'autoUpdate', registerType: 'autoUpdate',
// PWA manifest configuration
manifest: appConfig.pwaConfig?.manifest, manifest: appConfig.pwaConfig?.manifest,
// Development options
devOptions: { devOptions: {
enabled: false enabled: false
}, },
// Workbox configuration for service worker
workbox: { workbox: {
cleanupOutdatedCaches: true, cleanupOutdatedCaches: true,
skipWaiting: true, skipWaiting: true,

27
vite.config.web.ts

@ -1,27 +0,0 @@
import { defineConfig, loadEnv } from "vite";
import baseConfig from "./vite.config.base";
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
return {
...baseConfig,
define: {
'import.meta.env.VITE_PLATFORM': JSON.stringify('web'),
},
build: {
...baseConfig.build,
outDir: 'dist/web',
rollupOptions: {
...baseConfig.build.rollupOptions,
output: {
...baseConfig.build.rollupOptions.output,
manualChunks: {
// Web-specific chunk splitting
vendor: ['vue', 'vue-router', 'pinia'],
}
}
}
}
};
});
Loading…
Cancel
Save