Compare commits
6 Commits
master
...
db-backup-
Author | SHA1 | Date |
---|---|---|
|
5a6e5289ff | 2 months ago |
|
6620311b7d | 2 months ago |
|
6e2bdc69e9 | 2 months ago |
|
b8c3517072 | 2 months ago |
|
a0cf9ea721 | 2 months ago |
|
42d706b1fb | 2 months ago |
58 changed files with 5698 additions and 1973 deletions
@ -0,0 +1 @@ |
|||||
|
PLATFORM=electron |
@ -0,0 +1,5 @@ |
|||||
|
PLATFORM=capacitor |
||||
|
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 |
@ -0,0 +1 @@ |
|||||
|
PLATFORM=web |
Binary file not shown.
@ -1,2 +1,2 @@ |
|||||
#Fri Mar 21 07:27:50 UTC 2025 |
#Thu Apr 03 10:21:42 UTC 2025 |
||||
gradle.version=8.2.1 |
gradle.version=8.11.1 |
||||
|
Binary file not shown.
File diff suppressed because it is too large
@ -0,0 +1,257 @@ |
|||||
|
/** * @file ProfileSection.vue * @description Component for managing user |
||||
|
profile information * @author Matthew Raymer * @version 1.0.0 */ |
||||
|
|
||||
|
<template> |
||||
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"> |
||||
|
<div v-if="loading" class="text-center mb-2"> |
||||
|
<font-awesome |
||||
|
icon="spinner" |
||||
|
class="fa-spin text-slate-400" |
||||
|
></font-awesome> |
||||
|
Loading profile... |
||||
|
</div> |
||||
|
<div v-else class="flex items-center mb-2"> |
||||
|
<span class="font-bold">Public Profile</span> |
||||
|
<font-awesome |
||||
|
icon="circle-info" |
||||
|
class="text-slate-400 fa-fw ml-2 cursor-pointer" |
||||
|
@click="showProfileInfo" |
||||
|
/> |
||||
|
</div> |
||||
|
<textarea |
||||
|
v-model="profileDesc" |
||||
|
class="w-full h-32 p-2 border border-slate-300 rounded-md" |
||||
|
placeholder="Write something about yourself for the public..." |
||||
|
:readonly="loading || saving" |
||||
|
:class="{ 'bg-slate-100': loading || saving }" |
||||
|
></textarea> |
||||
|
|
||||
|
<div class="flex items-center mb-4" @click="toggleLocation"> |
||||
|
<input v-model="includeLocation" type="checkbox" class="mr-2" /> |
||||
|
<label for="includeLocation">Include Location</label> |
||||
|
</div> |
||||
|
<div v-if="includeLocation" class="mb-4 aspect-video"> |
||||
|
<p class="text-sm mb-2 text-slate-500"> |
||||
|
For your security, choose a location nearby but not exactly at your |
||||
|
place. |
||||
|
</p> |
||||
|
|
||||
|
<l-map |
||||
|
ref="profileMap" |
||||
|
class="!z-40 rounded-md" |
||||
|
@click="handleMapClick" |
||||
|
@ready="onMapReady" |
||||
|
> |
||||
|
<l-tile-layer |
||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" |
||||
|
layer-type="base" |
||||
|
name="OpenStreetMap" |
||||
|
/> |
||||
|
<l-marker |
||||
|
v-if="latitude && longitude" |
||||
|
:lat-lng="[latitude, longitude]" |
||||
|
@click="confirmEraseLocation" |
||||
|
/> |
||||
|
</l-map> |
||||
|
</div> |
||||
|
<div v-if="!loading && !saving"> |
||||
|
<div class="flex justify-between items-center"> |
||||
|
<button |
||||
|
class="mt-2 px-4 py-2 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md" |
||||
|
:disabled="loading || saving" |
||||
|
:class="{ |
||||
|
'opacity-50 cursor-not-allowed': loading || saving, |
||||
|
}" |
||||
|
@click="saveProfile" |
||||
|
> |
||||
|
Save Profile |
||||
|
</button> |
||||
|
<button |
||||
|
class="mt-2 px-4 py-2 bg-gradient-to-b from-red-400 to-red-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md" |
||||
|
:disabled="loading || saving" |
||||
|
:class="{ |
||||
|
'opacity-50 cursor-not-allowed': |
||||
|
loading || saving || (!profileDesc && !includeLocation), |
||||
|
}" |
||||
|
@click="confirmDeleteProfile" |
||||
|
> |
||||
|
Delete Profile |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div v-else-if="loading">Loading...</div> |
||||
|
<div v-else>Saving...</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue, Prop, Emit } from "vue-facing-decorator"; |
||||
|
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet"; |
||||
|
import { ProfileService } from "../services/ProfileService"; |
||||
|
import { logger } from "../utils/logger"; |
||||
|
|
||||
|
@Component({ |
||||
|
components: { |
||||
|
LMap, |
||||
|
LMarker, |
||||
|
LTileLayer, |
||||
|
}, |
||||
|
}) |
||||
|
export default class ProfileSection extends Vue { |
||||
|
@Prop({ required: true }) activeDid!: string; |
||||
|
@Prop({ required: true }) partnerApiServer!: string; |
||||
|
|
||||
|
@Emit("profile-updated") profileUpdated() {} |
||||
|
|
||||
|
loading = true; |
||||
|
saving = false; |
||||
|
profileDesc = ""; |
||||
|
latitude = 0; |
||||
|
longitude = 0; |
||||
|
includeLocation = false; |
||||
|
zoom = 2; |
||||
|
|
||||
|
async mounted() { |
||||
|
await this.loadProfile(); |
||||
|
} |
||||
|
|
||||
|
async loadProfile() { |
||||
|
try { |
||||
|
const profile = await ProfileService.loadProfile( |
||||
|
this.activeDid, |
||||
|
this.partnerApiServer, |
||||
|
); |
||||
|
if (profile) { |
||||
|
this.profileDesc = profile.description || ""; |
||||
|
this.latitude = profile.location?.lat || 0; |
||||
|
this.longitude = profile.location?.lng || 0; |
||||
|
this.includeLocation = !!(this.latitude && this.longitude); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
logger.error("Error loading profile:", error); |
||||
|
this.$notify({ |
||||
|
group: "alert", |
||||
|
type: "danger", |
||||
|
title: "Error Loading Profile", |
||||
|
text: "Your server profile is not available.", |
||||
|
}); |
||||
|
} finally { |
||||
|
this.loading = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async saveProfile() { |
||||
|
this.saving = true; |
||||
|
try { |
||||
|
await ProfileService.saveProfile(this.activeDid, this.partnerApiServer, { |
||||
|
description: this.profileDesc, |
||||
|
location: this.includeLocation |
||||
|
? { |
||||
|
lat: this.latitude, |
||||
|
lng: this.longitude, |
||||
|
} |
||||
|
: undefined, |
||||
|
}); |
||||
|
|
||||
|
this.$notify({ |
||||
|
group: "alert", |
||||
|
type: "success", |
||||
|
title: "Profile Saved", |
||||
|
text: "Your profile has been updated successfully.", |
||||
|
}); |
||||
|
this.profileUpdated(); |
||||
|
} catch (error) { |
||||
|
logger.error("Error saving profile:", error); |
||||
|
this.$notify({ |
||||
|
group: "alert", |
||||
|
type: "danger", |
||||
|
title: "Error Saving Profile", |
||||
|
text: "There was an error saving your profile.", |
||||
|
}); |
||||
|
} finally { |
||||
|
this.saving = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
toggleLocation() { |
||||
|
this.includeLocation = !this.includeLocation; |
||||
|
if (!this.includeLocation) { |
||||
|
this.latitude = 0; |
||||
|
this.longitude = 0; |
||||
|
this.zoom = 2; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
handleMapClick(event: { latlng: { lat: number; lng: number } }) { |
||||
|
this.latitude = event.latlng.lat; |
||||
|
this.longitude = event.latlng.lng; |
||||
|
} |
||||
|
|
||||
|
onMapReady(map: L.Map) { |
||||
|
const zoom = this.latitude && this.longitude ? 12 : 2; |
||||
|
map.setView([this.latitude, this.longitude], zoom); |
||||
|
} |
||||
|
|
||||
|
confirmEraseLocation() { |
||||
|
this.$notify({ |
||||
|
group: "modal", |
||||
|
type: "confirm", |
||||
|
title: "Erase Marker", |
||||
|
text: "Are you sure you don't want to mark a location? This will erase the current location.", |
||||
|
onYes: () => { |
||||
|
this.latitude = 0; |
||||
|
this.longitude = 0; |
||||
|
this.zoom = 2; |
||||
|
this.includeLocation = false; |
||||
|
}, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
async confirmDeleteProfile() { |
||||
|
this.$notify({ |
||||
|
group: "modal", |
||||
|
type: "confirm", |
||||
|
title: "Delete Profile", |
||||
|
text: "Are you sure you want to delete your public profile? This will remove your description and location from the server, and it cannot be undone.", |
||||
|
onYes: this.deleteProfile, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
async deleteProfile() { |
||||
|
this.saving = true; |
||||
|
try { |
||||
|
await ProfileService.deleteProfile(this.activeDid, this.partnerApiServer); |
||||
|
this.profileDesc = ""; |
||||
|
this.latitude = 0; |
||||
|
this.longitude = 0; |
||||
|
this.includeLocation = false; |
||||
|
this.$notify({ |
||||
|
group: "alert", |
||||
|
type: "success", |
||||
|
title: "Profile Deleted", |
||||
|
text: "Your profile has been deleted successfully.", |
||||
|
}); |
||||
|
this.profileUpdated(); |
||||
|
} catch (error) { |
||||
|
logger.error("Error deleting profile:", error); |
||||
|
this.$notify({ |
||||
|
group: "alert", |
||||
|
type: "danger", |
||||
|
title: "Error Deleting Profile", |
||||
|
text: "There was an error deleting your profile.", |
||||
|
}); |
||||
|
} finally { |
||||
|
this.saving = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
showProfileInfo() { |
||||
|
this.$notify({ |
||||
|
group: "alert", |
||||
|
type: "info", |
||||
|
title: "Public Profile Information", |
||||
|
text: "This data will be published for all to see, so be careful what your write. Your ID will only be shared with people who you allow to see your activity.", |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
</script> |
@ -0,0 +1,34 @@ |
|||||
|
/** |
||||
|
* Interface for a Decentralized Identifier (DID) |
||||
|
* @author Matthew Raymer |
||||
|
*/ |
||||
|
|
||||
|
import { KeyMeta } from "../libs/crypto/vc"; |
||||
|
|
||||
|
export interface IKey { |
||||
|
id: string; |
||||
|
type: string; |
||||
|
controller: string; |
||||
|
ethereumAddress: string; |
||||
|
publicKeyHex: string; |
||||
|
privateKeyHex: string; |
||||
|
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: IService[]; |
||||
|
} |
@ -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; |
||||
|
}; |
||||
|
}; |
||||
|
} |
@ -0,0 +1,82 @@ |
|||||
|
/** |
||||
|
* @file DatabaseBackupService.ts |
||||
|
* @description Capacitor-specific implementation of database backup service |
||||
|
* |
||||
|
* This service handles database backup operations on Capacitor platforms (Android/iOS) |
||||
|
* using the Filesystem and Share plugins. It creates a temporary backup file, |
||||
|
* writes the backup data to it, and shares the file using the platform's share sheet. |
||||
|
*/ |
||||
|
|
||||
|
import { Filesystem, Directory } from "@capacitor/filesystem"; |
||||
|
import { Share } from "@capacitor/share"; |
||||
|
import { DatabaseBackupService as BaseDatabaseBackupService } from "../../services/DatabaseBackupService"; |
||||
|
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 backup process for Capacitor platform"); |
||||
|
|
||||
|
// Create a timestamped backup file name
|
||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); |
||||
|
const backupFileName = `timesafari-backup-${timestamp}.json`; |
||||
|
const backupFilePath = `backups/${backupFileName}`; |
||||
|
|
||||
|
log("Creating backup file:", { |
||||
|
fileName: backupFileName, |
||||
|
path: backupFilePath, |
||||
|
}); |
||||
|
|
||||
|
// Write the backup file
|
||||
|
const writeResult = (await Filesystem.writeFile({ |
||||
|
path: backupFilePath, |
||||
|
data: base64Data, |
||||
|
directory: Directory.Cache, |
||||
|
recursive: true, |
||||
|
})) as unknown as { uri: string }; |
||||
|
|
||||
|
if (!writeResult.uri) { |
||||
|
throw new Error("Failed to write backup file: No URI returned"); |
||||
|
} |
||||
|
|
||||
|
log("Backup file written successfully:", { uri: writeResult.uri }); |
||||
|
|
||||
|
// Share the backup file
|
||||
|
log("Sharing backup file"); |
||||
|
await Share.share({ |
||||
|
title: "TimeSafari Backup", |
||||
|
text: "Your TimeSafari backup file", |
||||
|
url: writeResult.uri, |
||||
|
dialogTitle: "Share TimeSafari Backup", |
||||
|
}); |
||||
|
|
||||
|
log("Backup shared successfully"); |
||||
|
|
||||
|
// Clean up the temporary file
|
||||
|
try { |
||||
|
await Filesystem.deleteFile({ |
||||
|
path: backupFilePath, |
||||
|
directory: Directory.Cache, |
||||
|
}); |
||||
|
log("Temporary backup file cleaned up"); |
||||
|
} catch (cleanupError) { |
||||
|
error("Failed to clean up temporary backup file:", cleanupError); |
||||
|
// Don't throw here as the backup was successful
|
||||
|
} |
||||
|
} catch (err) { |
||||
|
error("Error during backup process:", err); |
||||
|
throw err; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,95 @@ |
|||||
|
/** |
||||
|
* @file DatabaseBackupService.ts |
||||
|
* @description Base service class for handling database backup operations |
||||
|
* |
||||
|
* 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 { |
||||
|
/** |
||||
|
* 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, |
||||
|
): 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(); |
||||
|
return await factory.createDatabaseBackupService(); |
||||
|
} |
||||
|
} |
@ -0,0 +1,195 @@ |
|||||
|
/** |
||||
|
* @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 { DatabaseBackupService as StubDatabaseBackupService } from "./platforms/empty"; |
||||
|
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(); |
||||
|
} |
||||
|
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.ts 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> { |
||||
|
// List of supported platforms for web builds
|
||||
|
const webSupportedPlatforms = ["web", "capacitor", "electron"]; |
||||
|
|
||||
|
// Return stub implementation for unsupported platforms
|
||||
|
if (!webSupportedPlatforms.includes(this.platform)) { |
||||
|
logger.log( |
||||
|
`Using stub implementation for unsupported platform: ${this.platform}`, |
||||
|
); |
||||
|
return new StubDatabaseBackupService(); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
logger.log(`Loading platform-specific service for ${this.platform}`); |
||||
|
// Use dynamic import with platform-specific path
|
||||
|
const module = await import( |
||||
|
/* @vite-ignore */ |
||||
|
`./platforms/${this.platform}/DatabaseBackupService.ts` |
||||
|
); |
||||
|
logger.log("Platform service loaded successfully"); |
||||
|
return new module.DatabaseBackupService(); |
||||
|
} catch (error) { |
||||
|
logger.error( |
||||
|
`[TimeSafari] Failed to load platform-specific service for ${this.platform}:`, |
||||
|
error, |
||||
|
); |
||||
|
// Fallback to stub implementation on error
|
||||
|
logger.log("Falling back to stub implementation"); |
||||
|
return new StubDatabaseBackupService(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 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; |
||||
|
} |
||||
|
} |
@ -0,0 +1,105 @@ |
|||||
|
/** |
||||
|
* @file ProfileService.ts |
||||
|
* @description Service class for handling user profile operations |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
|
||||
|
import { logger } from "../utils/logger"; |
||||
|
import { getHeaders } from "../libs/endorserServer"; |
||||
|
import type { UserProfile } from "@/types/interfaces"; |
||||
|
|
||||
|
export class ProfileService { |
||||
|
/** |
||||
|
* Saves a user profile to the server |
||||
|
* @param activeDid - The user's active DID |
||||
|
* @param partnerApiServer - The partner API server URL |
||||
|
* @param profile - The profile data to save |
||||
|
* @returns Promise<void> |
||||
|
*/ |
||||
|
static async saveProfile( |
||||
|
activeDid: string, |
||||
|
partnerApiServer: string, |
||||
|
profile: Partial<UserProfile>, |
||||
|
): Promise<void> { |
||||
|
try { |
||||
|
const headers = await getHeaders(activeDid); |
||||
|
const response = await fetch( |
||||
|
`${partnerApiServer}/api/partner/userProfile`, |
||||
|
{ |
||||
|
method: "POST", |
||||
|
headers, |
||||
|
body: JSON.stringify(profile), |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
if (!response.ok) { |
||||
|
throw new Error(`Failed to save profile: ${response.statusText}`); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
logger.error("Error saving profile:", error); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Deletes a user profile from the server |
||||
|
* @param activeDid - The user's active DID |
||||
|
* @param partnerApiServer - The partner API server URL |
||||
|
* @returns Promise<void> |
||||
|
*/ |
||||
|
static async deleteProfile( |
||||
|
activeDid: string, |
||||
|
partnerApiServer: string, |
||||
|
): Promise<void> { |
||||
|
try { |
||||
|
const headers = await getHeaders(activeDid); |
||||
|
const response = await fetch( |
||||
|
`${partnerApiServer}/api/partner/userProfile`, |
||||
|
{ |
||||
|
method: "DELETE", |
||||
|
headers, |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
if (!response.ok) { |
||||
|
throw new Error(`Failed to delete profile: ${response.statusText}`); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
logger.error("Error deleting profile:", error); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Loads a user profile from the server |
||||
|
* @param activeDid - The user's active DID |
||||
|
* @param partnerApiServer - The partner API server URL |
||||
|
* @returns Promise<UserProfile | null> |
||||
|
*/ |
||||
|
static async loadProfile( |
||||
|
activeDid: string, |
||||
|
partnerApiServer: string, |
||||
|
): Promise<UserProfile | null> { |
||||
|
try { |
||||
|
const headers = await getHeaders(activeDid); |
||||
|
const response = await fetch( |
||||
|
`${partnerApiServer}/api/partner/userProfileForIssuer/${activeDid}`, |
||||
|
{ headers }, |
||||
|
); |
||||
|
|
||||
|
if (response.status === 404) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
if (!response.ok) { |
||||
|
throw new Error(`Failed to load profile: ${response.statusText}`); |
||||
|
} |
||||
|
|
||||
|
return await response.json(); |
||||
|
} catch (error) { |
||||
|
logger.error("Error loading profile:", error); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,110 @@ |
|||||
|
/** |
||||
|
* @file RateLimitsService.ts |
||||
|
* @description Service class for handling rate limit operations |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
|
||||
|
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 did - The user's DID |
||||
|
* @returns 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(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) { |
||||
|
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; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Fetches image rate limits for a given DID |
||||
|
* @param apiServer - The API server URL |
||||
|
* @param activeDid - The user's active DID |
||||
|
* @returns Promise<ImageRateLimits> |
||||
|
*/ |
||||
|
static async fetchImageRateLimits( |
||||
|
apiServer: string, |
||||
|
activeDid: string, |
||||
|
): Promise<ImageRateLimits> { |
||||
|
try { |
||||
|
const headers = await getHeaders(activeDid); |
||||
|
const response = await fetch( |
||||
|
`${apiServer}/api/endorser/imageRateLimits/${activeDid}`, |
||||
|
{ headers }, |
||||
|
); |
||||
|
|
||||
|
if (!response.ok) { |
||||
|
throw new Error( |
||||
|
`Failed to fetch image rate limits: ${response.statusText}`, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return await response.json(); |
||||
|
} catch (error) { |
||||
|
logger.error("Error fetching image rate limits:", error); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Formats rate limit error messages |
||||
|
* @param error - The error object |
||||
|
* @returns string |
||||
|
*/ |
||||
|
static formatRateLimitError(error: unknown): string { |
||||
|
if (error instanceof Error) { |
||||
|
return error.message; |
||||
|
} |
||||
|
if (typeof error === "object" && error !== null) { |
||||
|
const err = error as { |
||||
|
response?: { data?: { error?: { message?: string } } }; |
||||
|
}; |
||||
|
return err.response?.data?.error?.message || "An unknown error occurred"; |
||||
|
} |
||||
|
return "An unknown error occurred"; |
||||
|
} |
||||
|
} |
@ -0,0 +1,35 @@ |
|||||
|
/** |
||||
|
* @file DatabaseBackupService.ts |
||||
|
* @description Capacitor-specific implementation of the DatabaseBackupService |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
|
||||
|
import { DatabaseBackupService } from "../../DatabaseBackupService"; |
||||
|
import { Filesystem } from "@capacitor/filesystem"; |
||||
|
import { Share } from "@capacitor/share"; |
||||
|
|
||||
|
export default class CapacitorDatabaseBackupService extends DatabaseBackupService { |
||||
|
protected async handleBackup( |
||||
|
base64Data: string, |
||||
|
_arrayBuffer: ArrayBuffer, |
||||
|
_blob: Blob, |
||||
|
): Promise<void> { |
||||
|
// Capacitor platform handling
|
||||
|
const fileName = `database-backup-${new Date().toISOString()}.json`; |
||||
|
const path = `backups/${fileName}`; |
||||
|
|
||||
|
await Filesystem.writeFile({ |
||||
|
path, |
||||
|
data: base64Data, |
||||
|
directory: "CACHE", |
||||
|
recursive: true, |
||||
|
}); |
||||
|
|
||||
|
await Share.share({ |
||||
|
title: "Database Backup", |
||||
|
text: "Here's your database backup", |
||||
|
url: path, |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -0,0 +1,18 @@ |
|||||
|
import { DatabaseBackupService } from "../../DatabaseBackupService"; |
||||
|
import { dialog } from "electron"; |
||||
|
import * as fs from "fs"; |
||||
|
import * as path from "path"; |
||||
|
|
||||
|
export default class ElectronDatabaseBackupService extends DatabaseBackupService { |
||||
|
protected async handleBackup(base64Data: string): Promise<void> { |
||||
|
const { filePath } = await dialog.showSaveDialog({ |
||||
|
title: "Save Database Backup", |
||||
|
defaultPath: path.join(process.env.HOME || "", "database-backup.json"), |
||||
|
filters: [{ name: "JSON", extensions: ["json"] }], |
||||
|
}); |
||||
|
|
||||
|
if (filePath) { |
||||
|
fs.writeFileSync(filePath, base64Data, "base64"); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,20 @@ |
|||||
|
/** |
||||
|
* @file empty.ts |
||||
|
* @description Stub implementation for excluding platform-specific code |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
|
||||
|
import { DatabaseBackupService } from "../DatabaseBackupService"; |
||||
|
|
||||
|
export default class StubDatabaseBackupService extends DatabaseBackupService { |
||||
|
protected async handleBackup( |
||||
|
_base64Data: string, |
||||
|
_arrayBuffer: ArrayBuffer, |
||||
|
_blob: Blob, |
||||
|
): Promise<void> { |
||||
|
throw new Error("This platform does not support database backups"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export { StubDatabaseBackupService as DatabaseBackupService }; |
@ -0,0 +1,33 @@ |
|||||
|
/** |
||||
|
* @file DatabaseBackupService.ts |
||||
|
* @description Web-specific implementation of the DatabaseBackupService |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
|
||||
|
import { DatabaseBackupService } from "../../DatabaseBackupService"; |
||||
|
import { log, error } from "../../../utils/logger"; |
||||
|
|
||||
|
export default class WebDatabaseBackupService extends DatabaseBackupService { |
||||
|
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; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,64 @@ |
|||||
|
/** |
||||
|
* Type declarations for Capacitor modules used in the application. |
||||
|
* @author Matthew Raymer |
||||
|
*/ |
||||
|
|
||||
|
declare module "@capacitor/filesystem" { |
||||
|
export interface FileWriteOptions { |
||||
|
path: string; |
||||
|
data: string; |
||||
|
directory?: string; |
||||
|
encoding?: string; |
||||
|
recursive?: boolean; |
||||
|
} |
||||
|
|
||||
|
export interface FileReadResult { |
||||
|
data: string; |
||||
|
} |
||||
|
|
||||
|
export interface FileDeleteOptions { |
||||
|
path: string; |
||||
|
directory?: string; |
||||
|
} |
||||
|
|
||||
|
export interface FilesystemDirectory { |
||||
|
Cache: "CACHE"; |
||||
|
Documents: "DOCUMENTS"; |
||||
|
Data: "DATA"; |
||||
|
External: "EXTERNAL"; |
||||
|
ExternalStorage: "EXTERNAL_STORAGE"; |
||||
|
} |
||||
|
|
||||
|
export interface Filesystem { |
||||
|
writeFile(options: FileWriteOptions): Promise<void>; |
||||
|
readFile(options: { |
||||
|
path: string; |
||||
|
directory?: string; |
||||
|
}): Promise<FileReadResult>; |
||||
|
deleteFile(options: FileDeleteOptions): Promise<void>; |
||||
|
} |
||||
|
|
||||
|
export const Filesystem: Filesystem; |
||||
|
export const Directory: FilesystemDirectory; |
||||
|
export const Encoding: { |
||||
|
UTF8: "utf8"; |
||||
|
ASCII: "ascii"; |
||||
|
UTF16: "utf16"; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
declare module "@capacitor/share" { |
||||
|
export interface ShareOptions { |
||||
|
title?: string; |
||||
|
text?: string; |
||||
|
url?: string; |
||||
|
dialogTitle?: string; |
||||
|
files?: string[]; |
||||
|
} |
||||
|
|
||||
|
export interface Share { |
||||
|
share(options: ShareOptions): Promise<void>; |
||||
|
} |
||||
|
|
||||
|
export const Share: Share; |
||||
|
} |
@ -0,0 +1,430 @@ |
|||||
|
/** |
||||
|
* @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?: { |
||||
|
/** |
||||
|
* HD wallet derivation path |
||||
|
* @example "m/44'/60'/0'/0/0" |
||||
|
*/ |
||||
|
derivationPath?: string; |
||||
|
|
||||
|
/** |
||||
|
* Key usage or purpose |
||||
|
* @example "signing", "encryption" |
||||
|
*/ |
||||
|
usage?: string; |
||||
|
|
||||
|
/** |
||||
|
* Key creation timestamp |
||||
|
*/ |
||||
|
createdAt?: number; |
||||
|
|
||||
|
/** |
||||
|
* Additional key metadata |
||||
|
*/ |
||||
|
[key: string]: unknown; |
||||
|
}; |
||||
|
}>; |
||||
|
|
||||
|
/** |
||||
|
* 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; |
||||
|
}>; |
||||
|
|
||||
|
/** |
||||
|
* Optional metadata about the identifier |
||||
|
*/ |
||||
|
meta?: { |
||||
|
/** |
||||
|
* DID method-specific metadata |
||||
|
* @example { network: "mainnet", chainId: 1 } for ethr |
||||
|
*/ |
||||
|
method?: Record<string, unknown>; |
||||
|
|
||||
|
/** |
||||
|
* Identifier creation timestamp |
||||
|
*/ |
||||
|
createdAt?: number; |
||||
|
|
||||
|
/** |
||||
|
* Last update timestamp |
||||
|
*/ |
||||
|
updatedAt?: number; |
||||
|
|
||||
|
/** |
||||
|
* Additional identifier metadata |
||||
|
*/ |
||||
|
[key: string]: unknown; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 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; |
||||
|
|
||||
|
/** |
||||
|
* Key usage or purpose |
||||
|
* @example "signing", "encryption" |
||||
|
*/ |
||||
|
usage?: string; |
||||
|
|
||||
|
/** |
||||
|
* Key creation timestamp |
||||
|
*/ |
||||
|
createdAt?: number; |
||||
|
|
||||
|
/** |
||||
|
* 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; |
||||
|
error?: Error; |
||||
|
} |
||||
|
|
||||
|
export interface UserProfile { |
||||
|
/** User's profile description */ |
||||
|
description: string; |
||||
|
/** User's location information */ |
||||
|
location?: { |
||||
|
/** Latitude coordinate */ |
||||
|
lat: number; |
||||
|
/** Longitude coordinate */ |
||||
|
lng: number; |
||||
|
}; |
||||
|
/** User's given name */ |
||||
|
givenName?: string; |
||||
|
/** User's family name */ |
||||
|
familyName?: string; |
||||
|
} |
||||
|
|
||||
|
export interface ErrorResponse { |
||||
|
error: string; |
||||
|
message: string; |
||||
|
statusCode: number; |
||||
|
} |
||||
|
|
||||
|
export interface LeafletMouseEvent { |
||||
|
latlng: { |
||||
|
lat: number; |
||||
|
lng: number; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export interface GiveRecordWithContactInfo { |
||||
|
type?: string; |
||||
|
agentDid: string; |
||||
|
amount: number; |
||||
|
amountConfirmed: number; |
||||
|
description: string; |
||||
|
fullClaim: GiveVerifiableCredential; |
||||
|
fulfillsHandleId: string; |
||||
|
fulfillsPlanHandleId?: string; |
||||
|
fulfillsType?: string; |
||||
|
handleId: string; |
||||
|
issuedAt: string; |
||||
|
issuerDid: string; |
||||
|
jwtId: string; |
||||
|
providerPlanHandleId?: string; |
||||
|
recipientDid: string; |
||||
|
unit: string; |
||||
|
giver: { |
||||
|
known: boolean; |
||||
|
displayName: string; |
||||
|
profileImageUrl?: string; |
||||
|
}; |
||||
|
issuer: { |
||||
|
known: boolean; |
||||
|
displayName: string; |
||||
|
profileImageUrl?: string; |
||||
|
}; |
||||
|
receiver: { |
||||
|
known: boolean; |
||||
|
displayName: string; |
||||
|
profileImageUrl?: string; |
||||
|
}; |
||||
|
providerPlanName?: string; |
||||
|
recipientProjectName?: string; |
||||
|
image?: string; |
||||
|
} |
||||
|
|
||||
|
export interface TimeSafariError extends Error { |
||||
|
/** |
||||
|
* User-friendly error message |
||||
|
*/ |
||||
|
userMessage?: string; |
||||
|
|
||||
|
/** |
||||
|
* Error code for programmatic handling |
||||
|
*/ |
||||
|
code?: string; |
||||
|
|
||||
|
/** |
||||
|
* Additional error context |
||||
|
*/ |
||||
|
context?: Record<string, unknown>; |
||||
|
} |
File diff suppressed because it is too large
@ -0,0 +1,46 @@ |
|||||
|
import { defineConfig } from "vite"; |
||||
|
import vue from "@vitejs/plugin-vue"; |
||||
|
import path from "path"; |
||||
|
|
||||
|
export default defineConfig({ |
||||
|
plugins: [vue()], |
||||
|
resolve: { |
||||
|
alias: { |
||||
|
'@': path.resolve(__dirname, './src'), |
||||
|
'nostr-tools': path.resolve(__dirname, 'node_modules/nostr-tools'), |
||||
|
'nostr-tools/nip06': path.resolve(__dirname, 'node_modules/nostr-tools/nip06'), |
||||
|
'nostr-tools/core': path.resolve(__dirname, 'node_modules/nostr-tools/core'), |
||||
|
stream: 'stream-browserify', |
||||
|
util: 'util', |
||||
|
crypto: 'crypto-browserify' |
||||
|
}, |
||||
|
mainFields: ['module', 'jsnext:main', 'jsnext', 'main'], |
||||
|
}, |
||||
|
optimizeDeps: { |
||||
|
include: ['nostr-tools', 'nostr-tools/nip06', 'nostr-tools/core'], |
||||
|
esbuildOptions: { |
||||
|
define: { |
||||
|
global: 'globalThis' |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
build: { |
||||
|
sourcemap: true, |
||||
|
target: 'esnext', |
||||
|
chunkSizeWarningLimit: 1000, |
||||
|
commonjsOptions: { |
||||
|
include: [/node_modules/], |
||||
|
transformMixedEsModules: true |
||||
|
}, |
||||
|
rollupOptions: { |
||||
|
external: ['stream', 'util', 'crypto'], |
||||
|
output: { |
||||
|
globals: { |
||||
|
stream: 'stream', |
||||
|
util: 'util', |
||||
|
crypto: 'crypto' |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}); |
File diff suppressed because one or more lines are too long
@ -0,0 +1,28 @@ |
|||||
|
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('mobile'), |
||||
|
}, |
||||
|
build: { |
||||
|
...baseConfig.build, |
||||
|
outDir: 'dist/mobile', |
||||
|
rollupOptions: { |
||||
|
...baseConfig.build.rollupOptions, |
||||
|
output: { |
||||
|
...baseConfig.build.rollupOptions.output, |
||||
|
manualChunks: { |
||||
|
// Mobile-specific chunk splitting
|
||||
|
vendor: ['vue', 'vue-router', 'pinia'], |
||||
|
capacitor: ['@capacitor/core', '@capacitor/filesystem', '@capacitor/share'], |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
}); |
@ -1,46 +1,18 @@ |
|||||
import { defineConfig } from "vite"; |
import { defineConfig, loadEnv } from "vite"; |
||||
import vue from "@vitejs/plugin-vue"; |
import baseConfig from "./vite.config.base"; |
||||
import path from "path"; |
|
||||
|
|
||||
export default defineConfig({ |
// https://vitejs.dev/config/
|
||||
plugins: [vue()], |
export default defineConfig(({ mode }) => { |
||||
resolve: { |
// Load env file based on `mode` in the current working directory.
|
||||
alias: { |
// Set the third parameter to '' to load all env regardless of the `VITE_` prefix.
|
||||
'@': path.resolve(__dirname, './src'), |
const env = loadEnv(mode, process.cwd(), ''); |
||||
'nostr-tools': path.resolve(__dirname, 'node_modules/nostr-tools'), |
const platform = env.PLATFORM || 'web'; |
||||
'nostr-tools/nip06': path.resolve(__dirname, 'node_modules/nostr-tools/nip06'), |
|
||||
'nostr-tools/core': path.resolve(__dirname, 'node_modules/nostr-tools/core'), |
// Load platform-specific config
|
||||
stream: 'stream-browserify', |
const platformConfig = require(`./vite.config.${platform}`).default; |
||||
util: 'util', |
|
||||
crypto: 'crypto-browserify' |
return { |
||||
}, |
...baseConfig, |
||||
mainFields: ['module', 'jsnext:main', 'jsnext', 'main'], |
...platformConfig, |
||||
}, |
}; |
||||
optimizeDeps: { |
|
||||
include: ['nostr-tools', 'nostr-tools/nip06', 'nostr-tools/core'], |
|
||||
esbuildOptions: { |
|
||||
define: { |
|
||||
global: 'globalThis' |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
build: { |
|
||||
sourcemap: true, |
|
||||
target: 'esnext', |
|
||||
chunkSizeWarningLimit: 1000, |
|
||||
commonjsOptions: { |
|
||||
include: [/node_modules/], |
|
||||
transformMixedEsModules: true |
|
||||
}, |
|
||||
rollupOptions: { |
|
||||
external: ['stream', 'util', 'crypto'], |
|
||||
output: { |
|
||||
globals: { |
|
||||
stream: 'stream', |
|
||||
util: 'util', |
|
||||
crypto: 'crypto' |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
}); |
}); |
@ -1,27 +1,92 @@ |
|||||
import { defineConfig, mergeConfig } from "vite"; |
/** |
||||
import { VitePWA } from "vite-plugin-pwa"; |
* @file vite.config.web.mts |
||||
import { createBuildConfig } from "./vite.config.common.mts"; |
* @description Vite configuration for web platform builds |
||||
import { loadAppConfig } from "./vite.config.utils.mts"; |
* |
||||
|
* 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 |
||||
|
*/ |
||||
|
|
||||
export default defineConfig(async () => { |
import { defineConfig } from "vite"; |
||||
const baseConfig = await createBuildConfig('web'); |
import vue from "@vitejs/plugin-vue"; |
||||
const appConfig = await loadAppConfig(); |
import baseConfig from "./vite.config.base"; |
||||
|
|
||||
return mergeConfig(baseConfig, { |
// Define Node.js built-in modules that need browser compatibility |
||||
plugins: [ |
const nodeBuiltins = { |
||||
VitePWA({ |
stream: 'stream-browserify', |
||||
registerType: 'autoUpdate', |
util: 'util', |
||||
manifest: appConfig.pwaConfig?.manifest, |
crypto: 'crypto-browserify', |
||||
devOptions: { |
http: 'stream-http', |
||||
enabled: false |
https: 'https-browserify', |
||||
|
zlib: 'browserify-zlib', |
||||
|
url: 'url', |
||||
|
assert: 'assert', |
||||
|
path: 'path-browserify', |
||||
|
fs: 'browserify-fs', |
||||
|
tty: 'tty-browserify' |
||||
|
}; |
||||
|
|
||||
|
export default defineConfig({ |
||||
|
...baseConfig, |
||||
|
plugins: [vue()], |
||||
|
optimizeDeps: { |
||||
|
...baseConfig.optimizeDeps, |
||||
|
include: [...(baseConfig.optimizeDeps?.include || []), 'qrcode.vue'], |
||||
|
exclude: Object.keys(nodeBuiltins), |
||||
|
esbuildOptions: { |
||||
|
define: { |
||||
|
global: 'globalThis' |
||||
|
} |
||||
|
} |
||||
}, |
}, |
||||
workbox: { |
resolve: { |
||||
cleanupOutdatedCaches: true, |
...baseConfig.resolve, |
||||
skipWaiting: true, |
alias: { |
||||
clientsClaim: true, |
...baseConfig.resolve?.alias, |
||||
sourcemap: true |
...nodeBuiltins |
||||
|
} |
||||
|
}, |
||||
|
build: { |
||||
|
...baseConfig.build, |
||||
|
commonjsOptions: { |
||||
|
...baseConfig.build?.commonjsOptions, |
||||
|
include: [/node_modules/], |
||||
|
exclude: [/src\/services\/platforms\/electron/], |
||||
|
transformMixedEsModules: true |
||||
|
}, |
||||
|
rollupOptions: { |
||||
|
...baseConfig.build?.rollupOptions, |
||||
|
external: Object.keys(nodeBuiltins), |
||||
|
output: { |
||||
|
...baseConfig.build?.rollupOptions?.output, |
||||
|
globals: nodeBuiltins |
||||
|
} |
||||
|
} |
||||
} |
} |
||||
}) |
|
||||
] |
|
||||
}); |
|
||||
}); |
}); |
||||
|
Loading…
Reference in new issue