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
This commit is contained in:
@@ -1 +1,5 @@
|
||||
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
|
||||
@@ -26,6 +26,10 @@ module.exports = {
|
||||
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@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": "^_"
|
||||
}]
|
||||
},
|
||||
};
|
||||
|
||||
Binary file not shown.
@@ -1,2 +1,2 @@
|
||||
#Fri Mar 21 07:27:50 UTC 2025
|
||||
gradle.version=8.2.1
|
||||
#Thu Apr 03 08:01:00 UTC 2025
|
||||
gradle.version=8.11.1
|
||||
|
||||
Binary file not shown.
@@ -10,6 +10,8 @@ android {
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
implementation project(':capacitor-app')
|
||||
implementation project(':capacitor-filesystem')
|
||||
implementation project(':capacitor-share')
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -2,5 +2,13 @@
|
||||
{
|
||||
"pkg": "@capacitor/app",
|
||||
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/filesystem",
|
||||
"classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/share",
|
||||
"classpath": "com.capacitorjs.plugins.share.SharePlugin"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<title>TimeSafari</title>
|
||||
<script type="module" crossorigin src="/assets/index-CZMUlUNO.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CxCVZqQa.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
|
||||
@@ -7,7 +7,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
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'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
|
||||
@@ -4,3 +4,9 @@ project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/
|
||||
|
||||
include ':capacitor-app'
|
||||
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')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
42
package-lock.json
generated
42
package-lock.json
generated
@@ -11,7 +11,7 @@
|
||||
"@capacitor/android": "^6.2.0",
|
||||
"@capacitor/app": "^6.0.0",
|
||||
"@capacitor/cli": "^6.2.0",
|
||||
"@capacitor/core": "^6.2.0",
|
||||
"@capacitor/core": "^6.2.1",
|
||||
"@capacitor/filesystem": "^6.0.3",
|
||||
"@capacitor/ios": "^6.2.0",
|
||||
"@capacitor/share": "^6.0.3",
|
||||
@@ -13208,9 +13208,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001707",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz",
|
||||
"integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==",
|
||||
"version": "1.0.30001709",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001709.tgz",
|
||||
"integrity": "sha512-NgL3vUTnDrPCZ3zTahp4fsugQ4dc7EKTSzwQDPEel6DMoMnfH2jhry9n2Zm8onbSR+f/QtKHFOA+iAQu4kbtWA==",
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -15612,9 +15612,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.129",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.129.tgz",
|
||||
"integrity": "sha512-JlXUemX4s0+9f8mLqib/bHH8gOHf5elKS6KeWG3sk3xozb/JTq/RLXIv8OKUWiK4Ah00Wm88EFj5PYkFr4RUPA==",
|
||||
"version": "1.5.130",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.130.tgz",
|
||||
"integrity": "sha512-Ou2u7L9j2XLZbhqzyX0jWDj6gA8D3jIfVzt4rikLf3cGBa0VdReuFimBKS9tQJA4+XpeCxj1NoWlfBXzbMa9IA==",
|
||||
"devOptional": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -16037,14 +16037,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier": {
|
||||
"version": "5.2.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.5.tgz",
|
||||
"integrity": "sha512-IKKP8R87pJyMl7WWamLgPkloB16dagPIdd2FjBDbyRYPKo93wS/NbCOPh6gH+ieNLC+XZrhJt/kWj0PS/DFdmg==",
|
||||
"version": "5.2.6",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz",
|
||||
"integrity": "sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prettier-linter-helpers": "^1.0.0",
|
||||
"synckit": "^0.10.2"
|
||||
"synckit": "^0.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
@@ -18568,9 +18568,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/image-size": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.0.tgz",
|
||||
"integrity": "sha512-4S8fwbO6w3GeCVN6OPtA9I5IGKkcDMPcKndtUlpJuCwu7JLjtj7JZpwqLuyY2nrmQT3AWsCJLSKPsc2mPBSl3w==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
|
||||
"integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
@@ -23520,9 +23520,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nostr-tools": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.11.1.tgz",
|
||||
"integrity": "sha512-+Oj5t+behIkU9kh3go5wg8Aa5oR7euBU9gOItUNapJe5Gaa+KPzMuTIN+rMRK3DaZ4Zt6RM4kR/ddwstzGKf7g==",
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.12.0.tgz",
|
||||
"integrity": "sha512-pUWEb020gTvt1XZvTa8AKNIHWFapjsv2NKyk43Ez2nnvz6WSXsrTFE0XtkNLSRBjPn6EpxumKeNiVzLz74jNSA==",
|
||||
"license": "Unlicense",
|
||||
"dependencies": {
|
||||
"@noble/ciphers": "^0.5.1",
|
||||
@@ -28778,9 +28778,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/synckit": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.10.3.tgz",
|
||||
"integrity": "sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ==",
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.1.tgz",
|
||||
"integrity": "sha512-fWZqNBZNNFp/7mTUy1fSsydhKsAKJ+u90Nk7kOK5Gcq9vObaqLBLjWFDBkyVU9Vvc6Y71VbOevMuGhqv02bT+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -28791,7 +28791,7 @@
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unts"
|
||||
"url": "https://opencollective.com/synckit"
|
||||
}
|
||||
},
|
||||
"node_modules/synckit/node_modules/tslib": {
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"@capacitor/android": "^6.2.0",
|
||||
"@capacitor/app": "^6.0.0",
|
||||
"@capacitor/cli": "^6.2.0",
|
||||
"@capacitor/core": "^6.2.0",
|
||||
"@capacitor/core": "^6.2.1",
|
||||
"@capacitor/filesystem": "^6.0.3",
|
||||
"@capacitor/ios": "^6.2.0",
|
||||
"@capacitor/share": "^6.0.3",
|
||||
|
||||
@@ -7,10 +7,11 @@ import { createAvatar, StyleOptions } from "@dicebear/core";
|
||||
import { avataaars } from "@dicebear/collection";
|
||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
@Component
|
||||
export default class EntityIcon extends Vue {
|
||||
@Prop contact: Contact;
|
||||
@Prop({ required: false }) contact?: Contact;
|
||||
@Prop entityId = ""; // overridden by contact.did or profileImageUrl
|
||||
@Prop iconSize = 0;
|
||||
@Prop profileImageUrl = ""; // overridden by contact.profileImageUrl
|
||||
@@ -22,7 +23,8 @@ export default class EntityIcon extends Vue {
|
||||
} else {
|
||||
const identifier = this.contact?.did || this.entityId;
|
||||
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=
|
||||
// ... does not render things with the same seed as this library.
|
||||
@@ -37,6 +39,12 @@ export default class EntityIcon extends Vue {
|
||||
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>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import BaseDexie, { Table } from "dexie";
|
||||
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
|
||||
import { exportDB } from "dexie-export-import";
|
||||
import * as R from "ramda";
|
||||
|
||||
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
|
||||
export type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T;
|
||||
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
||||
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
||||
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: (options?: any) => Promise<Blob> };
|
||||
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> = BaseDexie & T & { export: (options?: any) => Promise<Blob> };
|
||||
|
||||
//// Initialize the DBs, starting with the sensitive ones.
|
||||
|
||||
// Initialize Dexie database for secret, which is then used to encrypt accountsDB
|
||||
export const secretDB = new BaseDexie("TimeSafariSecret") as SecretDexie;
|
||||
secretDB.version(1).stores(SecretSchema);
|
||||
secretDB.export = (options) => exportDB(secretDB, options);
|
||||
|
||||
// Initialize Dexie database for accounts
|
||||
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
|
||||
// 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.
|
||||
|
||||
// Initialize Dexie databases for non-sensitive data
|
||||
// Initialize Dexie database for non-sensitive data
|
||||
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
|
||||
|
||||
|
||||
@@ -15,8 +15,20 @@ export interface IKey {
|
||||
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 {
|
||||
did: string;
|
||||
keys: IKey[];
|
||||
services: any[];
|
||||
services: IService[];
|
||||
}
|
||||
|
||||
288
src/interfaces/service.ts
Normal file
288
src/interfaces/service.ts
Normal file
@@ -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
Normal file
69
src/platforms/capacitor/DatabaseBackupService.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,95 @@
|
||||
/**
|
||||
* @file DatabaseBackupService.ts
|
||||
* @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 { log, error } from "../utils/logger";
|
||||
|
||||
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(
|
||||
"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(
|
||||
base64Data: string,
|
||||
arrayBuffer: ArrayBuffer,
|
||||
blob: Blob,
|
||||
blob: Blob
|
||||
): 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 service = await factory.createDatabaseBackupService();
|
||||
await service.handleBackup(base64Data, arrayBuffer, blob);
|
||||
return await factory.createDatabaseBackupService();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,102 @@
|
||||
/**
|
||||
* @file PlatformServiceFactory.ts
|
||||
* @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 { 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 {
|
||||
/**
|
||||
* Singleton instance of the factory
|
||||
* @private
|
||||
*/
|
||||
private static instance: PlatformServiceFactory;
|
||||
|
||||
/**
|
||||
* Current platform identifier
|
||||
* @private
|
||||
*/
|
||||
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() {
|
||||
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 {
|
||||
if (!PlatformServiceFactory.instance) {
|
||||
PlatformServiceFactory.instance = new PlatformServiceFactory();
|
||||
@@ -22,19 +104,75 @@ export class PlatformServiceFactory {
|
||||
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> {
|
||||
try {
|
||||
// Use Vite's dynamic import for platform-specific implementation
|
||||
const { default: PlatformService } = await import(
|
||||
`./platforms/${this.platform}/DatabaseBackupService`
|
||||
logger.log(`Loading platform-specific service for ${this.platform}`);
|
||||
// Update the import path to point to the correct location
|
||||
const { DatabaseBackupService: PlatformService } = await import(
|
||||
`../platforms/${this.platform}/DatabaseBackupService`
|
||||
);
|
||||
logger.log('Platform service loaded successfully');
|
||||
return new PlatformService();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
logger.error(
|
||||
`Failed to load platform-specific service for ${this.platform}:`,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,32 +8,40 @@
|
||||
import { logger } from "../utils/logger";
|
||||
import { getHeaders } from "../libs/endorserServer";
|
||||
import type { EndorserRateLimits, ImageRateLimits } from "../interfaces/limits";
|
||||
import axios from "axios";
|
||||
|
||||
export class RateLimitsService {
|
||||
/**
|
||||
* Fetches rate limits for a given DID
|
||||
* @param apiServer - The API server URL
|
||||
* @param activeDid - The user's active DID
|
||||
* @param did - The user's DID
|
||||
* @returns Promise<EndorserRateLimits>
|
||||
*/
|
||||
static async fetchRateLimits(
|
||||
apiServer: string,
|
||||
activeDid: string,
|
||||
): Promise<EndorserRateLimits> {
|
||||
static async fetchRateLimits(apiServer: string, did: string): Promise<EndorserRateLimits> {
|
||||
logger.log('Fetching rate limits for DID:', did);
|
||||
logger.log('Using API server:', apiServer);
|
||||
|
||||
try {
|
||||
const headers = await getHeaders(activeDid);
|
||||
const response = await fetch(
|
||||
`${apiServer}/api/endorser/rateLimits/${activeDid}`,
|
||||
{ headers },
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch rate limits: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
const headers = await getHeaders(did);
|
||||
const response = await axios.get(`${apiServer}/api/v2/rate-limits/${did}`, { headers });
|
||||
logger.log('Rate limits response:', response.data);
|
||||
return response.data;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,11 @@ import { Filesystem } from "@capacitor/filesystem";
|
||||
import { Share } from "@capacitor/share";
|
||||
|
||||
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
|
||||
const fileName = `database-backup-${new Date().toISOString()}.json`;
|
||||
const path = `backups/${fileName}`;
|
||||
|
||||
@@ -6,17 +6,28 @@
|
||||
*/
|
||||
|
||||
import { DatabaseBackupService } from "../../DatabaseBackupService";
|
||||
import { log, error } from "../../../utils/logger";
|
||||
|
||||
export default class WebDatabaseBackupService extends DatabaseBackupService {
|
||||
protected async handleBackup(blob: Blob): Promise<void> {
|
||||
// Web platform handling
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `database-backup-${new Date().toISOString()}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
protected async handleBackup(
|
||||
_base64Data: string,
|
||||
_arrayBuffer: ArrayBuffer,
|
||||
blob: Blob
|
||||
): Promise<void> {
|
||||
try {
|
||||
log('Starting web platform backup');
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,286 @@
|
||||
/**
|
||||
* Type declarations for custom interfaces used in the application.
|
||||
* @author Matthew Raymer
|
||||
* @file interfaces.ts
|
||||
* @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";
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
/**
|
||||
* The DID string in the format 'did:method:identifier'
|
||||
* @example 'did:ethr:0x123...'
|
||||
*/
|
||||
did: string;
|
||||
|
||||
/**
|
||||
* The DID method provider
|
||||
* @example 'ethr'
|
||||
*/
|
||||
provider: string;
|
||||
|
||||
/**
|
||||
* Array of cryptographic keys associated with the DID
|
||||
*/
|
||||
keys: Array<{
|
||||
/**
|
||||
* Key identifier
|
||||
* @example 'keys-1'
|
||||
*/
|
||||
kid: string;
|
||||
|
||||
/**
|
||||
* Key management system
|
||||
* @example 'local'
|
||||
*/
|
||||
kms: string;
|
||||
|
||||
/**
|
||||
* Key type
|
||||
* @example 'Secp256k1'
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Public key in hexadecimal format
|
||||
* @example '0x...'
|
||||
*/
|
||||
publicKeyHex: string;
|
||||
|
||||
/**
|
||||
* Optional metadata about the key
|
||||
*/
|
||||
meta?: any;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Array of service endpoints associated with the DID
|
||||
*/
|
||||
services: Array<{
|
||||
/**
|
||||
* Service identifier
|
||||
* @example 'endorser-service'
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Service type
|
||||
* @example 'EndorserService'
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Service endpoint URL
|
||||
* @example 'https://api.endorser.ch'
|
||||
*/
|
||||
serviceEndpoint: string;
|
||||
|
||||
/**
|
||||
* Optional service description
|
||||
*/
|
||||
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 {
|
||||
status: "preparing" | "exporting" | "complete" | "error";
|
||||
message?: string;
|
||||
|
||||
@@ -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 = {
|
||||
log: (message: string, ...args: unknown[]) => {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(message, ...args);
|
||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
||||
logToDb(message + argsString);
|
||||
const formattedMessage = formatMessage(message, ...args);
|
||||
console.log(formattedMessage);
|
||||
logToDb(message + (args.length > 0 ? " - " + safeStringify(args) : ""));
|
||||
}
|
||||
},
|
||||
warn: (message: string, ...args: unknown[]) => {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(message, ...args);
|
||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
||||
logToDb(message + argsString);
|
||||
const formattedMessage = formatMessage(message, ...args);
|
||||
console.warn(formattedMessage);
|
||||
logToDb(message + (args.length > 0 ? " - " + safeStringify(args) : ""));
|
||||
}
|
||||
},
|
||||
error: (message: string, ...args: unknown[]) => {
|
||||
// Errors will always be logged
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(message, ...args);
|
||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
||||
logToDb(message + argsString);
|
||||
const formattedMessage = formatMessage(message, ...args);
|
||||
console.error(formattedMessage);
|
||||
logToDb(message + (args.length > 0 ? " - " + safeStringify(args) : ""));
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -836,7 +836,7 @@ import "leaflet/dist/leaflet.css";
|
||||
import { AxiosError } from "axios";
|
||||
import { Buffer } from "buffer/";
|
||||
import Dexie from "dexie";
|
||||
import "dexie-export-import";
|
||||
import { exportDB } from "dexie-export-import";
|
||||
import * as R from "ramda";
|
||||
import type { IIdentifier, UserProfile } from "@/types/interfaces";
|
||||
import { ref } from "vue";
|
||||
@@ -889,6 +889,7 @@ import { DatabaseBackupService } from "../services/DatabaseBackupService";
|
||||
import { ProfileService } from "../services/ProfileService";
|
||||
import { RateLimitsService } from "../services/RateLimitsService";
|
||||
import ProfileSection from "../components/ProfileSection.vue";
|
||||
import { log, error } from '../utils/logger';
|
||||
|
||||
const inputImportFileNameRef = ref<Blob>();
|
||||
|
||||
@@ -943,6 +944,8 @@ export default class AccountViewView extends Vue {
|
||||
DEFAULT_IMAGE_API_SERVER = DEFAULT_IMAGE_API_SERVER;
|
||||
DEFAULT_PARTNER_API_SERVER = DEFAULT_PARTNER_API_SERVER;
|
||||
|
||||
private db = db;
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
apiServerInput = "";
|
||||
@@ -1400,42 +1403,52 @@ export default class AccountViewView extends Vue {
|
||||
*/
|
||||
async exportDatabase() {
|
||||
try {
|
||||
// Generate database blob
|
||||
const blob = await (Dexie as any).export(db, {
|
||||
prettyJson: true,
|
||||
logger.log("Starting database export process");
|
||||
|
||||
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
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
const base64Data = Buffer.from(arrayBuffer).toString("base64");
|
||||
|
||||
await DatabaseBackupService.createAndShareBackup(
|
||||
base64Data,
|
||||
arrayBuffer,
|
||||
blob,
|
||||
);
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Export Complete",
|
||||
text: "Your database has been exported successfully.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
const errorMessage = error.message;
|
||||
this.limitsMessage = errorMessage || "Bad server response.";
|
||||
logger.error("Got bad response retrieving limits:", error);
|
||||
} else {
|
||||
this.limitsMessage = "Got an error retrieving limits.";
|
||||
logger.error("Got some error retrieving limits:", error);
|
||||
if (!db.export) {
|
||||
logger.error("Database export method not available");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log("Creating export blob");
|
||||
const blob = await db.export();
|
||||
logger.log("Blob created:", {
|
||||
type: blob.type,
|
||||
size: blob.size
|
||||
});
|
||||
|
||||
logger.log("Converting blob to base64");
|
||||
const base64Data = await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
logger.log("Base64 data length:", base64Data.length);
|
||||
|
||||
logger.log("Converting blob to ArrayBuffer");
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
logger.log("ArrayBuffer size:", arrayBuffer.byteLength);
|
||||
|
||||
logger.log("Creating and sharing backup");
|
||||
await this.createAndShareBackup(base64Data, arrayBuffer, blob);
|
||||
logger.log("Backup creation and sharing completed");
|
||||
} 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) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
if (target.files) {
|
||||
@@ -1942,5 +1955,9 @@ export default class AccountViewView extends Vue {
|
||||
this.userProfileLongitude = updatedProfile.location?.lng || 0;
|
||||
this.includeUserProfileLocation = !!updatedProfile.location;
|
||||
}
|
||||
|
||||
async getDatabase() {
|
||||
return await db;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<h2 class="text-base flex gap-4 items-center">
|
||||
<span class="grow">
|
||||
<img
|
||||
src="../assets/blank-square.svg"
|
||||
src="@/assets/blank-square.svg"
|
||||
width="32"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
||||
/>
|
||||
|
||||
@@ -484,13 +484,13 @@
|
||||
<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>
|
||||
<img
|
||||
src="../assets/help/creative-commons-circle.svg"
|
||||
src="@/assets/help/creative-commons-circle.svg"
|
||||
alt="CC circle"
|
||||
width="20"
|
||||
class="display: inline"
|
||||
/>
|
||||
<img
|
||||
src="../assets/help/creative-commons-zero.svg"
|
||||
src="@/assets/help/creative-commons-zero.svg"
|
||||
alt="CC zero"
|
||||
width="20"
|
||||
style="display: inline"
|
||||
|
||||
@@ -135,7 +135,7 @@ Raymer * @version 1.0.0 */
|
||||
>
|
||||
<li @click="openDialog()">
|
||||
<img
|
||||
src="../assets/blank-square.svg"
|
||||
src="@/assets/blank-square.svg"
|
||||
class="mx-auto border border-blue-500 rounded-md mb-1 cursor-pointer"
|
||||
/>
|
||||
<h3
|
||||
|
||||
@@ -220,7 +220,7 @@
|
||||
</li>
|
||||
<li @click="openGiftDialogToProject()">
|
||||
<img
|
||||
src="../assets/blank-square.svg"
|
||||
src="@/assets/blank-square.svg"
|
||||
class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer"
|
||||
/>
|
||||
<h3
|
||||
|
||||
@@ -36,7 +36,15 @@ export async function createBuildConfig(mode: string) {
|
||||
assetsDir: 'assets',
|
||||
chunkSizeWarningLimit: 1000,
|
||||
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: {
|
||||
@@ -44,6 +52,8 @@ export async function createBuildConfig(mode: string) {
|
||||
'process.env.VITE_PLATFORM': JSON.stringify(mode),
|
||||
'process.env.VITE_PWA_ENABLED': JSON.stringify(!(isElectron || isPyWebView || isCapacitor)),
|
||||
__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: {
|
||||
alias: {
|
||||
|
||||
@@ -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 { createBuildConfig } from "./vite.config.common.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');
|
||||
|
||||
// Load application-specific configuration
|
||||
const appConfig = await loadAppConfig();
|
||||
|
||||
// Merge configurations with web-specific settings
|
||||
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: [
|
||||
// Progressive Web App configuration
|
||||
VitePWA({
|
||||
// Auto-update service worker
|
||||
registerType: 'autoUpdate',
|
||||
|
||||
// PWA manifest configuration
|
||||
manifest: appConfig.pwaConfig?.manifest,
|
||||
|
||||
// Development options
|
||||
devOptions: {
|
||||
enabled: false
|
||||
},
|
||||
|
||||
// Workbox configuration for service worker
|
||||
workbox: {
|
||||
cleanupOutdatedCaches: true,
|
||||
skipWaiting: true,
|
||||
|
||||
@@ -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'],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user