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
|
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",
|
"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": "^_"
|
||||||
|
}]
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Binary file not shown.
@@ -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
|
||||||
|
|||||||
Binary file not shown.
@@ -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')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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
generated
42
package-lock.json
generated
@@ -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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
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
|
* @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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
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>
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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