Browse Source

Merge branch 'master' into remove-image-cache

pull/164/head
Matthew Raymer 3 days ago
parent
commit
a3b69fc0d4
  1. 4
      BUILDING.md
  2. 36
      README.md
  3. 4
      package-lock.json
  4. 2
      package.json
  5. 2
      src/components/ContactInputForm.vue
  6. 10
      src/libs/endorserServer.ts
  7. 18
      src/router/index.ts
  8. 2
      src/services/AbsurdSqlDatabaseService.ts
  9. 2
      src/services/deepLinks.ts
  10. 3
      src/services/platforms/WebPlatformService.ts
  11. 2
      src/test/PlatformServiceMixinTest.vue
  12. 32
      src/test/index.ts
  13. 5
      src/utils/PlatformServiceMixin.ts
  14. 3
      src/views/AccountViewView.vue
  15. 3
      src/views/DIDView.vue
  16. 67
      src/views/TestView.vue

4
BUILDING.md

@ -8,7 +8,9 @@ This guide explains how to build TimeSafari for different platforms using the co
```bash ```bash
# 🖥️ Web Development # 🖥️ Web Development
npm run build:web:dev # Start development server with hot reload npm install # setup -- and pkgx.dev `dev` command before this will set environment with npm, etc
npm run build:web:serve -- --test # Start with test endorser server
npm run build:web:dev # Start development server with hot reload with local endorser server
npm run build:web:prod # Production build npm run build:web:prod # Production build
# 📱 Mobile Development # 📱 Mobile Development

36
README.md

@ -3,36 +3,9 @@
[Time Safari](https://timesafari.org/) allows people to ease into collaboration: start with expressions of gratitude [Time Safari](https://timesafari.org/) allows people to ease into collaboration: start with expressions of gratitude
and expand to crowd-fund with time & money, then record and see the impact of contributions. and expand to crowd-fund with time & money, then record and see the impact of contributions.
## Database Migration Status
**Current Status**: The application is undergoing a migration from Dexie (IndexedDB) to SQLite using absurd-sql. This migration is in **Phase 2** with a well-defined migration fence in place.
### Migration Progress
- ✅ **SQLite Database Service**: Fully implemented with absurd-sql
- ✅ **Platform Service Layer**: Unified database interface across platforms
- ✅ **Settings Migration**: Core user settings transferred
- ✅ **Account Migration**: Identity and key management
- 🔄 **Contact Migration**: User contact data (via import interface)
- 📋 **Code Cleanup**: Remove unused Dexie imports
### Migration Fence
The migration is controlled by a **migration fence** that separates legacy Dexie code from the new SQLite implementation. See [Migration Fence Definition](doc/migration-fence-definition.md) for complete details.
**Key Points**:
- Legacy Dexie database is disabled by default
- All database operations go through `PlatformServiceMixin`
- Migration tools provide controlled access to both databases
- Clear separation between legacy and new code
### Migration Documentation
- [Migration Guide](doc/migration-to-wa-sqlite.md) - Complete migration process
- [Migration Fence Definition](doc/migration-fence-definition.md) - Fence boundaries and rules
- [Database Migration Guide](doc/database-migration-guide.md) - User-facing migration tools
## Roadmap ## Roadmap
See [project.task.yaml](project.task.yaml) for current priorities. See [ClickUp](https://sharing.clickup.com/9014278710/l/h/8cmnyhp-174/10573fec74e2ba0) for current priorities.
(Numbers at the beginning of lines are estimated hours. See [taskyaml.org](https://taskyaml.org/) for details.)
## Setup & Building ## Setup & Building
@ -42,14 +15,16 @@ Quick start:
```bash ```bash
npm install npm install
npm run dev npm run build:web:serve -- --test
``` ```
To be able to make submissions: go to "profile" (bottom left), go to the bottom and expand "Show Advanced Settings", go to the bottom and to the "Test Page", and finally "Become User 0" to see all the functionality.
See [BUILDING.md](BUILDING.md) for comprehensive build instructions for all platforms (Web, Electron, iOS, Android, Docker). See [BUILDING.md](BUILDING.md) for comprehensive build instructions for all platforms (Web, Electron, iOS, Android, Docker).
## Development Database Clearing ## Development Database Clearing
TimeSafari provides a simple script-based approach to clear the database for development purposes. TimeSafari provides a simple script-based approach to clear the local database (not the claim server) for development purposes.
### Quick Usage ### Quick Usage
```bash ```bash
@ -126,7 +101,6 @@ const apiUrl = `${APP_SERVER}/api/claim/123`;
### Documentation ### Documentation
- [Domain Configuration System](docs/domain-configuration.md) - Complete guide
- [Constants and Configuration](src/constants/app.ts) - Core constants - [Constants and Configuration](src/constants/app.ts) - Core constants
## Tests ## Tests

4
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "1.0.6", "version": "1.0.7-beta",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "timesafari", "name": "timesafari",
"version": "1.0.6", "version": "1.0.7-beta",
"dependencies": { "dependencies": {
"@capacitor-community/electron": "^5.0.1", "@capacitor-community/electron": "^5.0.1",
"@capacitor-community/sqlite": "6.0.2", "@capacitor-community/sqlite": "6.0.2",

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "1.0.6", "version": "1.0.7-beta",
"description": "Time Safari Application", "description": "Time Safari Application",
"author": { "author": {
"name": "Time Safari Team" "name": "Time Safari Team"

2
src/components/ContactInputForm.vue

@ -167,7 +167,7 @@ export default class ContactInputForm extends Vue {
*/ */
@Emit("qr-scan") @Emit("qr-scan")
private handleQRScan(): void { private handleQRScan(): void {
console.log("[ContactInputForm] QR scan button clicked"); // QR scan button clicked - event emitted for parent handling
} }
} }
</script> </script>

10
src/libs/endorserServer.ts

@ -1348,12 +1348,12 @@ export async function createEndorserJwtVcFromClaim(
} }
/** /**
* Create a JWT for a RegisterAction claim. * Create a JWT for a RegisterAction claim, used for registrations & invites.
* *
* @param activeDid - The DID of the user creating the invite * @param activeDid - The DID of the user creating the invite
* @param contact - The contact to register, with a 'did' field (all optional for invites) * @param contact - Optional - The contact to register, with a 'did' field (all optional for invites)
* @param identifier - The identifier for the invite, usually random * @param identifier - Optional - The identifier for the invite, usually random
* @param expiresIn - The number of seconds until the invite expires * @param expiresIn - Optional - The number of seconds until the invite expires
* @returns The JWT for the RegisterAction claim * @returns The JWT for the RegisterAction claim
*/ */
export async function createInviteJwt( export async function createInviteJwt(
@ -1367,7 +1367,7 @@ export async function createInviteJwt(
"@type": "RegisterAction", "@type": "RegisterAction",
agent: { identifier: activeDid }, agent: { identifier: activeDid },
object: SERVICE_ID, object: SERVICE_ID,
identifier: identifier, identifier: identifier, // not sent if undefined
}; };
if (contact?.did) { if (contact?.did) {
vcClaim.participant = { identifier: contact.did }; vcClaim.participant = { identifier: contact.did };

18
src/router/index.ts

@ -82,6 +82,15 @@ const routes: Array<RouteRecordRaw> = [
name: "database-migration", name: "database-migration",
component: () => import("../views/DatabaseMigration.vue"), component: () => import("../views/DatabaseMigration.vue"),
}, },
{
path: "/deep-link-error",
name: "deep-link-error",
component: () => import("../views/DeepLinkErrorView.vue"),
meta: {
title: "Invalid Deep Link",
requiresAuth: false,
},
},
{ {
path: "/did/:did?", path: "/did/:did?",
name: "did", name: "did",
@ -276,15 +285,6 @@ const routes: Array<RouteRecordRaw> = [
name: "user-profile", name: "user-profile",
component: () => import("../views/UserProfileView.vue"), component: () => import("../views/UserProfileView.vue"),
}, },
{
path: "/deep-link-error",
name: "deep-link-error",
component: () => import("../views/DeepLinkErrorView.vue"),
meta: {
title: "Invalid Deep Link",
requiresAuth: false,
},
},
]; ];
const isElectron = window.location.protocol === "file:"; const isElectron = window.location.protocol === "file:";

2
src/services/AbsurdSqlDatabaseService.ts

@ -1,7 +1,9 @@
// **WORKER-COMPATIBLE CRYPTO POLYFILL**: Must be at the very top // **WORKER-COMPATIBLE CRYPTO POLYFILL**: Must be at the very top
// This prevents "crypto is not defined" errors when running in worker context // This prevents "crypto is not defined" errors when running in worker context
if (typeof window === "undefined" && typeof crypto === "undefined") { if (typeof window === "undefined" && typeof crypto === "undefined") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).crypto = { (globalThis as any).crypto = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getRandomValues: (array: any) => { getRandomValues: (array: any) => {
// Simple fallback for worker context // Simple fallback for worker context
for (let i = 0; i < array.length; i++) { for (let i = 0; i < array.length; i++) {

2
src/services/deepLinks.ts

@ -179,7 +179,7 @@ export class DeepLinkHandler {
const validRoute = routeSchema.parse(path) as DeepLinkRoute; const validRoute = routeSchema.parse(path) as DeepLinkRoute;
routeName = ROUTE_MAP[validRoute].name; routeName = ROUTE_MAP[validRoute].name;
} catch (error) { } catch (error) {
console.error(`[DeepLink] Invalid route path: ${path}`); logger.error(`[DeepLink] Invalid route path: ${path}`);
// Redirect to error page with information about the invalid link // Redirect to error page with information about the invalid link
await this.router.replace({ await this.router.replace({

3
src/services/platforms/WebPlatformService.ts

@ -693,7 +693,8 @@ export class WebPlatformService implements PlatformService {
const setClause = keys.map((key) => `${key} = ?`).join(", "); const setClause = keys.map((key) => `${key} = ?`).join(", ");
const sql = `UPDATE settings SET ${setClause} WHERE accountDid = ?`; const sql = `UPDATE settings SET ${setClause} WHERE accountDid = ?`;
const params = [...keys.map((key) => settings[key]), did]; const params = [...keys.map((key) => settings[key]), did];
console.log( // Log update operation for debugging
logger.debug(
"[WebPlatformService] updateDidSpecificSettings", "[WebPlatformService] updateDidSpecificSettings",
sql, sql,
JSON.stringify(params, null, 2), JSON.stringify(params, null, 2),

2
src/test/PlatformServiceMixinTest.vue

@ -92,6 +92,7 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
}) })
export default class PlatformServiceMixinTest extends Vue { export default class PlatformServiceMixinTest extends Vue {
result: string = ""; result: string = "";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
userZeroTestResult: any = null; userZeroTestResult: any = null;
activeTest: string = ""; // Track which test is currently active activeTest: string = ""; // Track which test is currently active
@ -267,6 +268,7 @@ This tests the complete save → retrieve cycle with actual database interaction
this.result = `User #0 settings test completed. isRegistered: ${accountSettings.isRegistered}`; this.result = `User #0 settings test completed. isRegistered: ${accountSettings.isRegistered}`;
} catch (error) { } catch (error) {
this.result = `Error testing User #0 settings: ${error}`; this.result = `Error testing User #0 settings: ${error}`;
// eslint-disable-next-line no-console
console.error("Error testing User #0 settings:", error); console.error("Error testing User #0 settings:", error);
} }
} }

32
src/test/index.ts

@ -1,9 +1,29 @@
import axios from "axios"; import axios from "axios";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import { SERVICE_ID } from "../libs/endorserServer"; import { SERVICE_ID } from "../libs/endorserServer";
import { deriveAddress, newIdentifier } from "../libs/crypto"; import {
DEFAULT_ROOT_DERIVATION_PATH,
deriveAddress,
newIdentifier,
} from "../libs/crypto";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { AppString } from "../constants/app"; import { AppString } from "../constants/app";
import { saveNewIdentity } from "@/libs/util";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
const TEST_USER_0_MNEMONIC =
"rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage";
export async function testBecomeUser0() {
const [addr, privateHex, publicHex, deriPath] = deriveAddress(TEST_USER_0_MNEMONIC);
const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath);
await saveNewIdentity(identity0, TEST_USER_0_MNEMONIC, DEFAULT_ROOT_DERIVATION_PATH);
const platformService = await PlatformServiceFactory.getInstance();
await platformService.updateDidSpecificSettings(identity0.did, {
isRegistered: true,
});
}
/** /**
* Get User #0 to sign & submit a RegisterAction for the user's activeDid. * Get User #0 to sign & submit a RegisterAction for the user's activeDid.
@ -15,10 +35,7 @@ import { AppString } from "../constants/app";
* @throws Error if registration fails or database access fails * @throws Error if registration fails or database access fails
*/ */
export async function testServerRegisterUser() { export async function testServerRegisterUser() {
const testUser0Mnem = const [addr, privateHex, publicHex, deriPath] = deriveAddress(TEST_USER_0_MNEMONIC);
"seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control";
const [addr, privateHex, publicHex, deriPath] = deriveAddress(testUser0Mnem);
const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath); const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath);
@ -32,9 +49,9 @@ export async function testServerRegisterUser() {
const vcClaim = { const vcClaim = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "RegisterAction", "@type": "RegisterAction",
agent: { did: identity0.did }, agent: { identifier: identity0.did },
object: SERVICE_ID, object: SERVICE_ID,
participant: { did: settings.activeDid }, participant: { identifier: settings.activeDid },
}; };
// Make a payload for the claim // Make a payload for the claim
@ -71,4 +88,5 @@ export async function testServerRegisterUser() {
const resp = await axios.post(url, payload, { headers }); const resp = await axios.post(url, payload, { headers });
logger.log("User registration result:", resp); logger.log("User registration result:", resp);
return resp;
} }

5
src/utils/PlatformServiceMixin.ts

@ -133,6 +133,7 @@ export const PlatformServiceMixin = {
* Used for change detection and component updates * Used for change detection and component updates
*/ */
currentActiveDid(): string | null { currentActiveDid(): string | null {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (this as any)._currentActiveDid; return (this as any)._currentActiveDid;
}, },
@ -200,7 +201,9 @@ export const PlatformServiceMixin = {
* This method should be called when the user switches identities * This method should be called when the user switches identities
*/ */
async $updateActiveDid(newDid: string | null): Promise<void> { async $updateActiveDid(newDid: string | null): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const oldDid = (this as any)._currentActiveDid; const oldDid = (this as any)._currentActiveDid;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this as any)._currentActiveDid = newDid; (this as any)._currentActiveDid = newDid;
if (newDid !== oldDid) { if (newDid !== oldDid) {
@ -291,6 +294,7 @@ export const PlatformServiceMixin = {
// Convert searchBoxes array to JSON string if present // Convert searchBoxes array to JSON string if present
if (settings.searchBoxes !== undefined) { if (settings.searchBoxes !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(converted as any).searchBoxes = Array.isArray(settings.searchBoxes) (converted as any).searchBoxes = Array.isArray(settings.searchBoxes)
? JSON.stringify(settings.searchBoxes) ? JSON.stringify(settings.searchBoxes)
: String(settings.searchBoxes); : String(settings.searchBoxes);
@ -692,6 +696,7 @@ export const PlatformServiceMixin = {
typeof method.value === "string"; typeof method.value === "string";
if (!isValid && method !== undefined) { if (!isValid && method !== undefined) {
// eslint-disable-next-line no-console
console.warn( console.warn(
"[ContactNormalization] Invalid contact method:", "[ContactNormalization] Invalid contact method:",
method, method,

3
src/views/AccountViewView.vue

@ -61,7 +61,8 @@
/> />
<!-- Notifications --> <!-- Notifications -->
<!-- Currently disabled because it doesn't work, even on Chrome. If restored, make sure it works or doesn't show on mobile/electron. --> <!-- Currently disabled because it doesn't work, even on Chrome.
If restored, make sure it works or doesn't show on mobile/electron. -->
<section <section
v-if="false" v-if="false"
id="sectionNotifications" id="sectionNotifications"

3
src/views/DIDView.vue

@ -7,7 +7,8 @@
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 id="ViewHeading" class="text-lg text-center font-light relative px-7"> <h1 id="ViewHeading" class="text-lg text-center font-light relative px-7">
<!-- Go to 'contacts' instead of just 'back' because they could get here from an edit page (and going back there is annoying). --> <!-- Go to 'contacts' instead of just 'back' because they could get here from an edit page
(and going back there is annoying). -->
<router-link <router-link
:to="{ name: 'contacts' }" :to="{ name: 'contacts' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"

67
src/views/TestView.vue

@ -21,7 +21,17 @@
</h1> </h1>
</div> </div>
<div> <div v-if="isNotProdServer">
<h2 class="text-xl font-bold mb-4">User Registration</h2>
<button :class="primaryButtonClasses" @click="registerMe()">
Register Yourself
</button>
<button :class="primaryButtonClasses" @click="becomeUser0()">
Become User 0 (who can register others)
</button>
</div>
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">Notiwind Alerts</h2> <h2 class="text-xl font-bold mb-4">Notiwind Alerts</h2>
<!-- Notification test buttons using computed configuration --> <!-- Notification test buttons using computed configuration -->
@ -99,7 +109,7 @@
<div> <div>
Register Passkey Register Passkey
<button :class="primaryButtonClasses" @click="register()"> <button :class="primaryButtonClasses" @click="registerPasskey()">
Simplewebauthn Simplewebauthn
</button> </button>
</div> </div>
@ -235,6 +245,7 @@ import {
registerAndSavePasskey, registerAndSavePasskey,
SHARED_PHOTO_BASE64_KEY, SHARED_PHOTO_BASE64_KEY,
} from "../libs/util"; } from "../libs/util";
import { testBecomeUser0, testServerRegisterUser } from "@/test";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { Account } from "../db/tables/accounts"; import { Account } from "../db/tables/accounts";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
@ -300,6 +311,7 @@ export default class Help extends Vue {
// for passkeys // for passkeys
credIdHex?: string; credIdHex?: string;
activeDid?: string; activeDid?: string;
apiServer?: string;
jwt?: string; jwt?: string;
peerSetup?: PeerSetup; peerSetup?: PeerSetup;
userName?: string; userName?: string;
@ -521,17 +533,6 @@ export default class Help extends Vue {
]; ];
} }
/**
* Method to trigger notification test
* Centralizes notification testing logic
*/
triggerTestNotification(config: {
notification: NotificationIface;
timeout?: number;
}) {
this.$notify(config.notification, config.timeout);
}
/** /**
* Component initialization * Component initialization
* *
@ -541,6 +542,7 @@ export default class Help extends Vue {
async mounted() { async mounted() {
const settings = await this.$accountSettings(); const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.userName = settings.firstName; this.userName = settings.firstName;
const account = await retrieveAccountMetadata(this.activeDid); const account = await retrieveAccountMetadata(this.activeDid);
@ -553,6 +555,43 @@ export default class Help extends Vue {
} }
} }
/**
* Checks if running on production server
*
* @returns True if not on production server (enables test utilities)
*/
public isNotProdServer() {
return this.apiServer !== AppString.PROD_ENDORSER_API_SERVER;
}
async registerMe() {
const response = await testServerRegisterUser();
if (response.status === 201) {
alert("Registration successful.");
this.$router.push({ name: "home" }); // because this page checks for registered status and sets things if it detects a change
} else {
logger.error("Registration failure response:", response);
alert("Registration failed: " + (response.data.error || response.data));
}
}
async becomeUser0() {
await testBecomeUser0();
alert("You are now User 0.");
this.$router.push({ name: "home" }); // because this page checks for registered status and sets things if it detects a change
}
/**
* Method to trigger notification test
* Centralizes notification testing logic
*/
triggerTestNotification(config: {
notification: NotificationIface;
timeout?: number;
}) {
this.$notify(config.notification, config.timeout);
}
/** /**
* Handles file upload for image sharing tests * Handles file upload for image sharing tests
* *
@ -609,7 +648,7 @@ export default class Help extends Vue {
* Includes validation and user confirmation workflow * Includes validation and user confirmation workflow
* Uses notification helpers for consistent messaging * Uses notification helpers for consistent messaging
*/ */
public async register() { public async registerPasskey() {
const DEFAULT_USERNAME = AppString.APP_NAME + " Tester"; const DEFAULT_USERNAME = AppString.APP_NAME + " Tester";
if (!this.userName) { if (!this.userName) {
const modalConfig = createPasskeyNameModal( const modalConfig = createPasskeyNameModal(

Loading…
Cancel
Save