From 51c8d8ac8b279f915ef8fabb3fc85c5885c1c687 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sat, 1 Feb 2025 20:32:14 -0700 Subject: [PATCH 01/16] change to three prompts for an onboarding-method choice (first one doesn't work yet) --- README.md | 2 +- src/components/ChoiceButtonDialog.vue | 152 ++++++++++++++++++++++++++ src/views/HomeView.vue | 36 +++--- 3 files changed, 172 insertions(+), 18 deletions(-) create mode 100644 src/components/ChoiceButtonDialog.vue diff --git a/README.md b/README.md index d5a0682..5836447 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ See [project.task.yaml](project.task.yaml) for current priorities. ## Setup -We like pkgx: `sh <(curl https://pkgx.sh) +npm sh` +We like pkgx: `sh <(curl https://pkgx.sh) +vite sh` ``` npm install diff --git a/src/components/ChoiceButtonDialog.vue b/src/components/ChoiceButtonDialog.vue new file mode 100644 index 0000000..0660a7c --- /dev/null +++ b/src/components/ChoiceButtonDialog.vue @@ -0,0 +1,152 @@ + + + \ No newline at end of file diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index ae2156f..3f5f46d 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -366,6 +366,8 @@ + + From 2a23587c3b23c1033c79e16c215f352ade542cf3 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sun, 2 Feb 2025 17:06:51 -0700 Subject: [PATCH 02/16] make screen where user can create a group onboarding meeting --- src/components/GiftedDialog.vue | 4 +- src/components/OfferDialog.vue | 6 +- src/libs/crypto/index.ts | 158 +++++++++- src/libs/endorserServer.ts | 85 +++--- src/main.ts | 2 + src/router/index.ts | 5 + src/views/ClaimReportCertificateView.vue | 190 ++++++++++++ src/views/ContactsView.vue | 55 +++- src/views/NewEditProjectView.vue | 6 +- src/views/OnboardMeetingView.vue | 355 +++++++++++++++++++++++ src/views/QuickActionBvcEndView.vue | 2 +- src/views/TestView.vue | 54 +++- 12 files changed, 851 insertions(+), 71 deletions(-) create mode 100644 src/views/ClaimReportCertificateView.vue create mode 100644 src/views/OnboardMeetingView.vue diff --git a/src/components/GiftedDialog.vue b/src/components/GiftedDialog.vue index 92113fd..832c456 100644 --- a/src/components/GiftedDialog.vue +++ b/src/components/GiftedDialog.vue @@ -90,7 +90,7 @@ import { Vue, Component, Prop } from "vue-facing-decorator"; import { NotificationIface } from "@/constants/app"; -import { createAndSubmitGive, didInfo } from "@/libs/endorserServer"; +import { createAndSubmitGive, didInfo, serverMessageForUser } from "@/libs/endorserServer"; import * as libsUtil from "@/libs/util"; import { db, retrieveSettingsForActiveAccount } from "@/db/index"; import { Contact } from "@/db/tables/contacts"; @@ -336,7 +336,7 @@ export default class GiftedDialog extends Vue { console.error("Error with give recordation caught:", error); const errorMessage = error.userMessage || - error.response?.data?.error?.message || + serverMessageForUser(error) || "There was an error recording the give."; this.$notify( { diff --git a/src/components/OfferDialog.vue b/src/components/OfferDialog.vue index 4d54d0c..20aee1f 100644 --- a/src/components/OfferDialog.vue +++ b/src/components/OfferDialog.vue @@ -83,7 +83,7 @@ import { Vue, Component, Prop } from "vue-facing-decorator"; import { NotificationIface } from "@/constants/app"; -import { createAndSubmitOffer } from "@/libs/endorserServer"; +import { createAndSubmitOffer, serverMessageForUser } from "@/libs/endorserServer"; import * as libsUtil from "@/libs/util"; import { retrieveSettingsForActiveAccount } from "@/db/index"; @@ -304,9 +304,9 @@ export default class OfferDialog extends Vue { // eslint-disable-next-line @typescript-eslint/no-explicit-any getOfferCreationErrorMessage(result: any) { return ( + serverMessageForUser(result) || result.error?.userMessage || - result.error?.error || - result.response?.data?.error?.message + result.error?.error ); } } diff --git a/src/libs/crypto/index.ts b/src/libs/crypto/index.ts index 8260c88..74409ab 100644 --- a/src/libs/crypto/index.ts +++ b/src/libs/crypto/index.ts @@ -52,7 +52,7 @@ export const newIdentifier = ( * * * @param {string} mnemonic - * @return {*} {[string, string, string, string]} + * @return {[string, string, string, string]} address, privateHex, publicHex, derivationPath */ export const deriveAddress = ( mnemonic: string, @@ -88,7 +88,8 @@ export const generateSeed = (): string => { /** * Retrieve an access token, or "" if no DID is provided. * - * @return {*} + * @param {string} did + * @return {string} JWT with basic payload */ export const accessToken = async (did?: string) => { if (did) { @@ -147,3 +148,156 @@ export const nextDerivationPath = (origDerivPath: string) => { .join("/"); return newDerivPath; }; + +// Base64 encoding/decoding utilities for browser +function base64ToArrayBuffer(base64: string): Uint8Array { + const binaryString = atob(base64); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; +} + +function arrayBufferToBase64(buffer: ArrayBuffer): string { + const binary = String.fromCharCode(...new Uint8Array(buffer)); + return btoa(binary); +} + +const SALT_LENGTH = 16; +const IV_LENGTH = 12; +const KEY_LENGTH = 256; +const ITERATIONS = 100000; + +// Encryption helper function +export async function encryptMessage(message: string, password: string) { + const encoder = new TextEncoder(); + const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH)); + const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)); + + // Derive key from password using PBKDF2 + const keyMaterial = await crypto.subtle.importKey( + 'raw', + encoder.encode(password), + 'PBKDF2', + false, + ['deriveBits', 'deriveKey'] + ); + + const key = await crypto.subtle.deriveKey( + { + name: 'PBKDF2', + salt, + iterations: ITERATIONS, + hash: 'SHA-256' + }, + keyMaterial, + { name: 'AES-GCM', length: KEY_LENGTH }, + false, + ['encrypt'] + ); + + // Encrypt the message + const encryptedContent = await crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv + }, + key, + encoder.encode(message) + ); + + // Return a JSON structure with base64-encoded components + const result = { + salt: arrayBufferToBase64(salt), + iv: arrayBufferToBase64(iv), + encrypted: arrayBufferToBase64(encryptedContent) + }; + + return btoa(JSON.stringify(result)); +} + +// Decryption helper function +export async function decryptMessage(encryptedJson: string, password: string) { + const decoder = new TextDecoder(); + const { salt, iv, encrypted } = JSON.parse(atob(encryptedJson)); + + // Convert base64 components back to Uint8Arrays + const saltArray = base64ToArrayBuffer(salt); + const ivArray = base64ToArrayBuffer(iv); + const encryptedContent = base64ToArrayBuffer(encrypted); + + // Derive the same key using PBKDF2 with the extracted salt + const keyMaterial = await crypto.subtle.importKey( + 'raw', + new TextEncoder().encode(password), + 'PBKDF2', + false, + ['deriveBits', 'deriveKey'] + ); + + const key = await crypto.subtle.deriveKey( + { + name: 'PBKDF2', + salt: saltArray, + iterations: ITERATIONS, + hash: 'SHA-256' + }, + keyMaterial, + { name: 'AES-GCM', length: KEY_LENGTH }, + false, + ['decrypt'] + ); + + // Decrypt the content + const decryptedContent = await crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv: ivArray + }, + key, + encryptedContent + ); + + // Convert the decrypted content back to a string + return decoder.decode(decryptedContent); +} + +// Test function to verify encryption/decryption +export async function testEncryptionDecryption() { + try { + const testMessage = "Hello, this is a test message! 🚀"; + const testPassword = "myTestPassword123"; + + console.log("Original message:", testMessage); + + // Test encryption + console.log("Encrypting..."); + const encrypted = await encryptMessage(testMessage, testPassword); + console.log("Encrypted result:", encrypted); + + // Test decryption + console.log("Decrypting..."); + const decrypted = await decryptMessage(encrypted, testPassword); + console.log("Decrypted result:", decrypted); + + // Verify + const success = testMessage === decrypted; + console.log("Test " + (success ? "PASSED ✅" : "FAILED ❌")); + console.log("Messages match:", success); + + // Test with wrong password + console.log("\nTesting with wrong password..."); + try { + const wrongDecrypted = await decryptMessage(encrypted, "wrongPassword"); + console.log("Should not reach here"); + } catch (error) { + console.log("Correctly failed with wrong password ✅"); + } + + return success; + } catch (error) { + console.error("Test failed with error:", error); + return false; + } +} diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index f116090..0b80972 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -621,41 +621,6 @@ const planCache: LRUCache = new LRUCache({ max: 500, }); -/** - * Helpful for server errors, to get all the info -- including stuff skipped by toString & JSON.stringify - * - * @param error - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function errorStringForLog(error: any) { - let stringifiedError = "" + error; - try { - stringifiedError = JSON.stringify(error); - } catch (e) { - // can happen with Dexie, eg: - // TypeError: Converting circular structure to JSON - // --> starting at object with constructor 'DexieError2' - // | property '_promise' -> object with constructor 'DexiePromise' - // --- property '_value' closes the circle - } - let fullError = "" + error + " - JSON: " + stringifiedError; - const errorResponseText = JSON.stringify(error.response); - // for some reason, error.response is not included in stringify result (eg. for 400 errors on invite redemptions) - if (!R.empty(errorResponseText) && !fullError.includes(errorResponseText)) { - // add error.response stuff - if (R.equals(error?.config, error?.response?.config)) { - // but exclude "config" because it's already in there - const newErrorResponseText = JSON.stringify( - R.omit(["config"] as never[], error.response), - ); - fullError += " - .response w/o same config JSON: " + newErrorResponseText; - } else { - fullError += " - .response JSON: " + errorResponseText; - } - } - return fullError; -} - /** * @param handleId nullable, in which case "undefined" will be returned * @param requesterDid optional, in which case no private info will be returned @@ -710,6 +675,54 @@ export async function setPlanInCache( planCache.set(handleId, planSummary); } +/** + * + * @param error that is thrown from an Endorser server call by Axios + * @returns user-friendly message, or undefined if none found + */ +export function serverMessageForUser(error: any) { + return ( + // this is how most user messages are returned + error?.response?.data?.error?.message + // some are returned as "error" with a string, but those are more for devs and are less helpful to the user + ); +} + +/** + * Helpful for server errors, to get all the info -- including stuff skipped by toString & JSON.stringify + * + * @param error + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function errorStringForLog(error: any) { + let stringifiedError = "" + error; + try { + stringifiedError = JSON.stringify(error); + } catch (e) { + // can happen with Dexie, eg: + // TypeError: Converting circular structure to JSON + // --> starting at object with constructor 'DexieError2' + // | property '_promise' -> object with constructor 'DexiePromise' + // --- property '_value' closes the circle + } + let fullError = "" + error + " - JSON: " + stringifiedError; + const errorResponseText = JSON.stringify(error.response); + // for some reason, error.response is not included in stringify result (eg. for 400 errors on invite redemptions) + if (!R.empty(errorResponseText) && !fullError.includes(errorResponseText)) { + // add error.response stuff + if (R.equals(error?.config, error?.response?.config)) { + // but exclude "config" because it's already in there + const newErrorResponseText = JSON.stringify( + R.omit(["config"] as never[], error.response), + ); + fullError += " - .response w/o same config JSON: " + newErrorResponseText; + } else { + fullError += " - .response JSON: " + errorResponseText; + } + } + return fullError; +} + /** * * @returns { data: Array, hitLimit: boolean true if maximum was hit and there may be more } @@ -1113,7 +1126,7 @@ export async function createAndSubmitClaim( } catch (error: any) { console.error("Error submitting claim:", error); const errorMessage: string = - error.response?.data?.error?.message || + serverMessageForUser(error) || error.message || "Got some error submitting the claim. Check your permissions, network, and error logs."; diff --git a/src/main.ts b/src/main.ts index 758f844..2aa9c6b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -23,6 +23,7 @@ import { faCalendar, faCamera, faCaretDown, + faChair, faCheck, faChevronDown, faChevronLeft, @@ -100,6 +101,7 @@ library.add( faCalendar, faCamera, faCaretDown, + faChair, faCheck, faChevronDown, faChevronLeft, diff --git a/src/router/index.ts b/src/router/index.ts index b8b1be7..4e6e354 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -179,6 +179,11 @@ const routes: Array = [ name: "offer-details", component: () => import("../views/OfferDetailsView.vue"), }, + { + path: '/onboard-meeting', + name: 'onboard-meeting', + component: () => import('../views/OnboardMeetingView.vue'), + }, { path: "/project/:id?", name: "project", diff --git a/src/views/ClaimReportCertificateView.vue b/src/views/ClaimReportCertificateView.vue new file mode 100644 index 0000000..0cfd08c --- /dev/null +++ b/src/views/ClaimReportCertificateView.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index 8d553e2..3dab9fe 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -23,27 +23,48 @@
- - - + + + + + + + + - + + + + + + + + +
+ +

+ Onboarding Meeting +

+ + +
+

Current Meeting

+
+

Name: {{ existingMeeting.name }}

+

Expires: {{ formatExpirationTime(existingMeeting.expiresAt) }}

+

Share the the password with the people you want to onboard.

+
+
+ +
+
+ + +
+
+

Delete Meeting?

+

This action cannot be undone. Are you sure you want to delete this meeting?

+
+ + +
+
+
+ + +
+

Create New Meeting

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+
+ +
+
+
+ + + \ No newline at end of file diff --git a/src/views/QuickActionBvcEndView.vue b/src/views/QuickActionBvcEndView.vue index e147a9d..1fbc56e 100644 --- a/src/views/QuickActionBvcEndView.vue +++ b/src/views/QuickActionBvcEndView.vue @@ -22,7 +22,7 @@

Confirm

- +
There are no claims yet today for you to confirm. diff --git a/src/views/TestView.vue b/src/views/TestView.vue index 4d25524..ceb8704 100644 --- a/src/views/TestView.vue +++ b/src/views/TestView.vue @@ -35,7 +35,7 @@ 5000, ) " - class="font-bold uppercase bg-slate-900 text-white px-3 py-2 rounded-md mr-2" + class="font-bold capitalize bg-slate-900 text-white px-3 py-2 rounded-md mr-2" > Toast @@ -52,7 +52,7 @@ 5000, ) " - class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" + class="font-bold capitalize bg-slate-600 text-white px-3 py-2 rounded-md mr-2" > Info @@ -69,7 +69,7 @@ 5000, ) " - class="font-bold uppercase bg-emerald-600 text-white px-3 py-2 rounded-md mr-2" + class="font-bold capitalize bg-emerald-600 text-white px-3 py-2 rounded-md mr-2" > Success @@ -86,7 +86,7 @@ 5000, ) " - class="font-bold uppercase bg-amber-600 text-white px-3 py-2 rounded-md mr-2" + class="font-bold capitalize bg-amber-600 text-white px-3 py-2 rounded-md mr-2" > Warning @@ -103,7 +103,7 @@ 5000, ) " - class="font-bold uppercase bg-rose-600 text-white px-3 py-2 rounded-md mr-2" + class="font-bold capitalize bg-rose-600 text-white px-3 py-2 rounded-md mr-2" > Danger @@ -118,7 +118,7 @@ -1, ) " - class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" + class="font-bold capitalize bg-slate-600 text-white px-3 py-2 rounded-md mr-2" > Notif ON @@ -133,7 +133,7 @@ -1, ) " - class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" + class="font-bold capitalize bg-slate-600 text-white px-3 py-2 rounded-md mr-2" > Notif MUTE @@ -148,7 +148,7 @@ -1, ) " - class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" + class="font-bold capitalize bg-slate-600 text-white px-3 py-2 rounded-md mr-2" > Notif OFF @@ -184,7 +184,7 @@ Register Passkey @@ -194,13 +194,13 @@ Create JWT @@ -210,19 +210,19 @@ Verify New JWT @@ -230,11 +230,25 @@
Verify New JWT -- requires creation first
+ +
+

Encryption & Decryption

+ See console for more output. +
+ + Result: {{ encryptionTestResult }} +
+
@@ -248,6 +262,7 @@ import { Router } from "vue-router"; import QuickNav from "@/components/QuickNav.vue"; import { AppString, NotificationIface } from "@/constants/app"; import { db, retrieveSettingsForActiveAccount } from "@/db/index"; +import * as cryptoLib from "@/libs/crypto"; import * as vcLib from "@/libs/crypto/vc"; import { PeerSetup, @@ -279,6 +294,9 @@ const TEST_PAYLOAD = { export default class Help extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; + // for encryption/decryption + encryptionTestResult?: boolean; + // for file import fileName?: string; @@ -289,6 +307,8 @@ export default class Help extends Vue { peerSetup?: PeerSetup; userName?: string; + cryptoLib = cryptoLib; + async mounted() { const settings = await retrieveSettingsForActiveAccount(); this.activeDid = settings.activeDid || ""; @@ -363,6 +383,10 @@ export default class Help extends Vue { this.credIdHex = account.passkeyCredIdHex; } + public async testEncryptionDecryption() { + this.encryptionTestResult = await cryptoLib.testEncryptionDecryption(); + } + public async createJwtSimplewebauthn() { const account: AccountKeyInfo | undefined = await retrieveAccountMetadata( this.activeDid || "", From 5ff91186e2c535ef15fd478e8f0ffb8ad2a7d0ae Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Mon, 3 Feb 2025 12:18:13 -0700 Subject: [PATCH 03/16] add onboarding pages for the list and members, and refine the setup --- src/libs/endorserServer.ts | 1 + src/router/index.ts | 16 +- src/views/ContactsView.vue | 2 +- src/views/HomeView.vue | 2 +- src/views/OnboardMeetingListView.vue | 197 ++++++++++++ src/views/OnboardMeetingMembersView.vue | 157 +++++++++ ...ngView.vue => OnboardMeetingSetupView.vue} | 301 ++++++++++++++---- 7 files changed, 616 insertions(+), 60 deletions(-) create mode 100644 src/views/OnboardMeetingListView.vue create mode 100644 src/views/OnboardMeetingMembersView.vue rename src/views/{OnboardMeetingView.vue => OnboardMeetingSetupView.vue} (51%) diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index 0b80972..ab9c4fc 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -690,6 +690,7 @@ export function serverMessageForUser(error: any) { /** * Helpful for server errors, to get all the info -- including stuff skipped by toString & JSON.stringify + * It works with AxiosError, eg handling an error.response intelligently. * * @param error */ diff --git a/src/router/index.ts b/src/router/index.ts index 4e6e354..3bb330e 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -180,9 +180,19 @@ const routes: Array = [ component: () => import("../views/OfferDetailsView.vue"), }, { - path: '/onboard-meeting', - name: 'onboard-meeting', - component: () => import('../views/OnboardMeetingView.vue'), + path: '/onboard-meeting-list', + name: 'onboard-meeting-list', + component: () => import('../views/OnboardMeetingListView.vue'), + }, + { + path: '/onboard-meeting-members/:groupId', + name: 'onboard-meeting-members', + component: () => import('../views/OnboardMeetingMembersView.vue'), + }, + { + path: '/onboard-meeting-setup', + name: 'onboard-meeting-setup', + component: () => import('../views/OnboardMeetingSetupView.vue'), }, { path: "/project/:id?", diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index 3dab9fe..9ce3679 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -31,7 +31,7 @@ diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 3f5f46d..2db6c18 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -960,7 +960,7 @@ export default class HomeView extends Vue { option2Text: "We are nearby with cameras", option3Text: "We will share some other way", onOption1: () => { - (this.$router as Router).push({ name: "onboarding-meeting" }); + (this.$router as Router).push({ name: "onboard-meeting-list" }); }, onOption2: () => { (this.$router as Router).push({ name: "contact-qr" }); diff --git a/src/views/OnboardMeetingListView.vue b/src/views/OnboardMeetingListView.vue new file mode 100644 index 0000000..78903ed --- /dev/null +++ b/src/views/OnboardMeetingListView.vue @@ -0,0 +1,197 @@ + + + \ No newline at end of file diff --git a/src/views/OnboardMeetingMembersView.vue b/src/views/OnboardMeetingMembersView.vue new file mode 100644 index 0000000..6296ed6 --- /dev/null +++ b/src/views/OnboardMeetingMembersView.vue @@ -0,0 +1,157 @@ + + + \ No newline at end of file diff --git a/src/views/OnboardMeetingView.vue b/src/views/OnboardMeetingSetupView.vue similarity index 51% rename from src/views/OnboardMeetingView.vue rename to src/views/OnboardMeetingSetupView.vue index ec9769a..b181b3d 100644 --- a/src/views/OnboardMeetingView.vue +++ b/src/views/OnboardMeetingSetupView.vue @@ -9,14 +9,33 @@ -
+

Current Meeting

-

Name: {{ existingMeeting.name }}

-

Expires: {{ formatExpirationTime(existingMeeting.expiresAt) }}

-

Share the the password with the people you want to onboard.

+

Name: {{ currentMeeting.name }}

+

Expires: {{ formatExpirationTime(currentMeeting.expiresAt) }}

+
+

Share the password with the people you want to onboard.

+ + Go to Meeting Members + +
+ + The meeting password has been lost. Edit it, or delete and create a new meeting. +
-
+
+
- -
-

Create New Meeting

-
+ +
+

{{ isEditing ? 'Edit Meeting' : 'Create New Meeting' }}

+ +
Meeting Expiration Time Meeting Password Your Name - {{ isLoading ? 'Creating...' : 'Create Meeting' }} + {{ isLoading ? (isEditing ? 'Updating...' : 'Creating...') : (isEditing ? 'Update Meeting' : 'Create Meeting') }} + +
+
@@ -125,18 +154,23 @@ import { Component, Vue } from 'vue-facing-decorator'; import QuickNav from '@/components/QuickNav.vue'; import TopMessage from '@/components/TopMessage.vue'; -import { retrieveSettingsForActiveAccount } from '@/db/index'; -import { getHeaders, serverMessageForUser } from '@/libs/endorserServer'; +import { logConsoleAndDb, retrieveSettingsForActiveAccount } from '@/db/index'; +import { errorStringForLog, getHeaders, serverMessageForUser } from '@/libs/endorserServer'; import { encryptMessage } from '@/libs/crypto'; -interface Meeting { - groupId?: string; - name: string; - expiresAt: string; +interface ServerMeeting { + groupId: string; // from the server + name: string; // from the server + expiresAt: string; // from the server + userFullName?: string; // from the user's session + password?: string; // from the user's session } -interface NewMeeting extends Meeting { +interface MeetingSetupInfo { + name: string; + expiresAt: string; userFullName: string; + password: string; } @Component({ @@ -148,19 +182,15 @@ interface NewMeeting extends Meeting { export default class OnboardMeetingView extends Vue { $notify!: (notification: { group: string; type: string; title: string; text: string }, timeout?: number) => void; - existingMeeting: Meeting | null = null; - currentMeeting: NewMeeting = { - name: '', - expiresAt: this.getDefaultExpirationTime(), - userFullName: '', - }; - password = ''; - isLoading = false; + currentMeeting: ServerMeeting | null = null; + newOrUpdatedMeeting: MeetingSetupInfo | null = null; activeDid = ''; apiServer = ''; isDeleting = false; + isEditing = false; + isLoading = true; showDeleteConfirm = false; - + fullName = ''; get minDateTime() { const now = new Date(); now.setMinutes(now.getMinutes() + 5); // Set minimum 5 minutes in the future @@ -171,9 +201,10 @@ export default class OnboardMeetingView extends Vue { const settings = await retrieveSettingsForActiveAccount(); this.activeDid = settings.activeDid || ''; this.apiServer = settings.apiServer || ''; - this.currentMeeting.userFullName = settings.firstName || ''; + this.fullName = settings.firstName || ''; - await this.fetchExistingMeeting(); + await this.fetchCurrentMeeting(); + this.isLoading = false; } getDefaultExpirationTime(): string { @@ -198,7 +229,17 @@ export default class OnboardMeetingView extends Vue { return `${year}-${month}-${day}T${hours}:${minutes}`; } - async fetchExistingMeeting() { + blankMeeting(): MeetingSetupInfo { + return { + // no groupId yet + name: '', + expiresAt: this.getDefaultExpirationTime(), + userFullName: this.fullName, + password: this.currentMeeting?.password || "", + }; + } + + async fetchCurrentMeeting() { try { const headers = await getHeaders(this.activeDid); const response = await this.axios.get( @@ -206,19 +247,21 @@ export default class OnboardMeetingView extends Vue { { headers } ); - if (response.data && response.data.data) { - this.existingMeeting = response.data.data; + if (response?.data?.data) { + console.log('Response data', response.data.data); + this.currentMeeting = { + ...response.data.data, + userFullName: this.fullName, + password: this.currentMeeting?.password || "", + }; + console.log('Current meeting', this.currentMeeting); + } else { + // no meeting found + this.newOrUpdatedMeeting = this.blankMeeting(); } } catch (error) { - console.log('Error fetching existing meeting:', error); - this.$notify( - { - group: 'alert', - type: 'danger', - title: 'Error', - text: serverMessageForUser(error) || 'Failed to fetch existing meeting.', - }, - ); + // no meeting found + this.newOrUpdatedMeeting = this.blankMeeting(); } } @@ -226,8 +269,12 @@ export default class OnboardMeetingView extends Vue { this.isLoading = true; try { + if (!this.newOrUpdatedMeeting) { + throw Error('There was no meeting data to create. We should never get here.'); + } + // Convert local time to UTC for comparison and server submission - const localExpiresAt = new Date(this.currentMeeting.expiresAt); + const localExpiresAt = new Date(this.newOrUpdatedMeeting.expiresAt); const now = new Date(); if (localExpiresAt <= now) { this.$notify( @@ -241,19 +288,44 @@ export default class OnboardMeetingView extends Vue { ); return; } + if (!this.newOrUpdatedMeeting.userFullName) { + this.$notify( + { + group: 'alert', + type: 'warning', + title: 'Invalid Name', + text: 'Please enter your name.', + }, + 5000 + ); + return; + } + if (!this.newOrUpdatedMeeting.password) { + this.$notify( + { + group: 'alert', + type: 'warning', + title: 'Invalid Password', + text: 'Please enter a password.', + }, + 5000 + ); + return; + } + // create content with user's name and DID encrypted with password const content = { - name: this.currentMeeting.userFullName, + name: this.newOrUpdatedMeeting.userFullName, did: this.activeDid, }; - const encryptedContent = await encryptMessage(JSON.stringify(content), this.password); + const encryptedContent = await encryptMessage(JSON.stringify(content), this.newOrUpdatedMeeting.password); const headers = await getHeaders(this.activeDid); const response = await this.axios.post( this.apiServer + '/api/partner/groupOnboard', { - name: this.currentMeeting.name, + name: this.newOrUpdatedMeeting.name, expiresAt: localExpiresAt.toISOString(), content: encryptedContent, }, @@ -261,11 +333,11 @@ export default class OnboardMeetingView extends Vue { ); if (response.data && response.data.success) { - this.existingMeeting = { - groupId: response.data.groupId, - name: this.currentMeeting.name, - expiresAt: localExpiresAt.toISOString(), + this.currentMeeting = { + ...this.newOrUpdatedMeeting, + groupId: response.data.success.groupId, }; + this.newOrUpdatedMeeting = null; this.$notify( { group: 'alert', @@ -276,10 +348,10 @@ export default class OnboardMeetingView extends Vue { 3000 ); } else { - throw new Error('Failed to create meeting due to unexpected response data: ' + JSON.stringify(response.data)); + throw { response: response }; } } catch (error) { - console.error('Error creating meeting:', error); + logConsoleAndDb('Error creating meeting: ' + errorStringForLog(error), true); const errorMessage = serverMessageForUser(error); this.$notify( { @@ -324,7 +396,8 @@ export default class OnboardMeetingView extends Vue { { headers } ); - this.existingMeeting = null; + this.currentMeeting = null; + this.newOrUpdatedMeeting = this.blankMeeting(); this.showDeleteConfirm = false; this.$notify( @@ -351,5 +424,123 @@ export default class OnboardMeetingView extends Vue { this.isDeleting = false; } } + + startEditing() { + this.isEditing = true; + // Populate form with existing meeting data + if (this.currentMeeting) { + console.log('Current meeting', this.currentMeeting); + const localExpiresAt = new Date(this.currentMeeting.expiresAt); + this.newOrUpdatedMeeting = { + name: this.currentMeeting.name, + expiresAt: this.formatDateForInput(localExpiresAt), + userFullName: this.currentMeeting.userFullName || '', + password: this.currentMeeting.password || '', + }; + } else { + console.error('There is no current meeting to edit. We should never get here.'); + } + } + + cancelEditing() { + this.isEditing = false; + // Reset form data + this.newOrUpdatedMeeting = null; + } + + async updateMeeting() { + this.isLoading = true; + if (!this.newOrUpdatedMeeting) { + throw Error('There was no meeting data to update.'); + } + + try { + // Convert local time to UTC for comparison and server submission + const localExpiresAt = new Date(this.newOrUpdatedMeeting.expiresAt); + const now = new Date(); + if (localExpiresAt <= now) { + this.$notify( + { + group: 'alert', + type: 'warning', + title: 'Invalid Time', + text: 'Select a future time for the meeting expiration.', + }, + 5000 + ); + return; + } + if (!this.newOrUpdatedMeeting.userFullName) { + this.$notify( + { + group: 'alert', + type: 'warning', + title: 'Invalid Name', + text: 'Please enter your name.', + }, + 5000 + ); + return; + } + if (!this.newOrUpdatedMeeting.password) { + this.$notify( + { + group: 'alert', + type: 'warning', + title: 'Invalid Password', + text: 'Please enter a password.', + }, + 5000 + ); + return; + } + // create content with user's name and DID encrypted with password + const content = { + name: this.newOrUpdatedMeeting.userFullName, + did: this.activeDid, + }; + const encryptedContent = await encryptMessage(JSON.stringify(content), this.newOrUpdatedMeeting.password); + + const headers = await getHeaders(this.activeDid); + const response = await this.axios.put( + this.apiServer + '/api/partner/groupOnboard', + { + // the groupId is in the currentMeeting but it's not necessary while users only have one meeting + name: this.newOrUpdatedMeeting.name, + expiresAt: localExpiresAt.toISOString(), + content: encryptedContent, + }, + { headers } + ); + + if (response.data && response.data.success) { + console.log('Updated meeting', response.data); + // Update the current meeting with only the necessary fields + this.currentMeeting = { + ...this.newOrUpdatedMeeting, + groupId: this.currentMeeting?.groupId || "", + }; + this.newOrUpdatedMeeting = null; + console.log('Updated meeting now', this.currentMeeting); + this.isEditing = false; + } else { + throw { response: response }; + } + } catch (error) { + logConsoleAndDb('Error updating meeting: ' + errorStringForLog(error), true); + const errorMessage = serverMessageForUser(error); + this.$notify( + { + group: 'alert', + type: 'danger', + title: 'Error', + text: errorMessage || 'Failed to update meeting. Try reloading or submitting again.', + }, + 5000 + ); + } finally { + this.isLoading = false; + } + } } \ No newline at end of file From 40765feea1c0b948973dbb955c514d170d68b2d7 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Mon, 3 Feb 2025 12:39:16 -0700 Subject: [PATCH 04/16] move edit & delete around & eliminate redundant boolean --- src/views/OnboardMeetingSetupView.vue | 75 ++++++++++++++------------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/src/views/OnboardMeetingSetupView.vue b/src/views/OnboardMeetingSetupView.vue index b181b3d..7d864a1 100644 --- a/src/views/OnboardMeetingSetupView.vue +++ b/src/views/OnboardMeetingSetupView.vue @@ -9,44 +9,49 @@ -
-

Current Meeting

-
-

Name: {{ currentMeeting.name }}

-

Expires: {{ formatExpirationTime(currentMeeting.expiresAt) }}

-
-

Share the password with the people you want to onboard.

- +
+
+

Current Meeting

+
- - The meeting password has been lost. Edit it, or delete and create a new meeting. - -
-
-
+
+

Name: {{ currentMeeting.name }}

+

Expires: {{ formatExpirationTime(currentMeeting.expiresAt) }}

+ +
+

Share the password with the people you want to onboard.

+
+
+ Your copy of the password is not saved. Edit the meeting, or delete it and create a new meeting. +
+ + + Open page that meeting members see + +
@@ -73,9 +78,9 @@
-

{{ isEditing ? 'Edit Meeting' : 'Create New Meeting' }}

+

{{ isEditingOrCreating() ? 'Edit Meeting' : 'Create New Meeting' }}

-
+
- {{ isLoading ? (isEditing ? 'Updating...' : 'Creating...') : (isEditing ? 'Update Meeting' : 'Create Meeting') }} + {{ isLoading ? (isEditingOrCreating() ? 'Updating...' : 'Creating...') : (isEditingOrCreating() ? 'Update Meeting' : 'Create Meeting') }} - -
- -
- -
+
{{ errorMessage }}
@@ -33,20 +28,12 @@
-
-
-

{{ member.name }}

-

DID: {{ member.did }}

-
- -

- No members found in this meeting -

-

- That password failed. You may be in the wrong meeting. Go back and try again. -

-
+ @@ -54,37 +41,17 @@ import { Component, Vue } from 'vue-facing-decorator'; import QuickNav from '@/components/QuickNav.vue'; import TopMessage from '@/components/TopMessage.vue'; -import { logConsoleAndDb, retrieveSettingsForActiveAccount } from '@/db/index'; -import { errorStringForLog, getHeaders, serverMessageForUser } from '@/libs/endorserServer'; -import { decryptMessage } from '@/libs/crypto'; - -interface Member { - memberId: number; - content: string; -} - -interface DecryptedMember { - memberId: number; - name: string; - did: string; -} +import MembersList from '@/components/MembersList.vue'; @Component({ components: { QuickNav, TopMessage, + MembersList, }, }) export default class OnboardMeetingMembersView extends Vue { - $notify!: (notification: { group: string; type: string; title: string; text: string }, timeout?: number) => void; - - activeDid = ''; - apiServer = ''; - decryptedMembers: DecryptedMember[] = []; - decryptFailure = false; errorMessage = ''; - isLoading = false; - members: Member[] = []; get groupId(): string { return this.$route.params.groupId as string; @@ -103,55 +70,10 @@ export default class OnboardMeetingMembersView extends Vue { this.errorMessage = 'The password is missing. Go back and try again.'; return; } - - const settings = await retrieveSettingsForActiveAccount(); - this.activeDid = settings.activeDid || ''; - this.apiServer = settings.apiServer || ''; - await this.fetchMembers(); } - async fetchMembers() { - this.isLoading = true; - this.errorMessage = ''; - - try { - const headers = await getHeaders(this.activeDid); - const response = await this.axios.get( - `${this.apiServer}/api/partner/groupOnboardMembers/`, - { headers } - ); - - if (response.data && response.data.data) { - this.members = response.data.data; - await this.decryptMemberContents(); - } else { - throw { response: response }; - } - } catch (error) { - logConsoleAndDb('Error fetching members: ' + errorStringForLog(error), true); - this.errorMessage = serverMessageForUser(error) || 'Failed to fetch members.'; - } finally { - this.isLoading = false; - } - } - - async decryptMemberContents() { - this.decryptedMembers = []; - - for (const member of this.members) { - try { - const decryptedContent = await decryptMessage(member.content, this.password); - const content = JSON.parse(decryptedContent); - - this.decryptedMembers.push({ - memberId: member.memberId, - name: content.name, - did: content.did, - }); - } catch (error) { - this.decryptFailure = true; - } - } + handleError(message: string) { + this.errorMessage = message; } } \ No newline at end of file diff --git a/src/views/OnboardMeetingSetupView.vue b/src/views/OnboardMeetingSetupView.vue index 7d864a1..d213978 100644 --- a/src/views/OnboardMeetingSetupView.vue +++ b/src/views/OnboardMeetingSetupView.vue @@ -9,7 +9,10 @@ -
+

Current Meeting

@@ -19,7 +22,7 @@ title="Edit Meeting" > - Edit Meeting + {{ isInCreateMode() ? 'Create Meeting' : 'Edit Meeting' }}
- - - Open page that meeting members see -
+
@@ -77,10 +73,13 @@
-
-

{{ isEditingOrCreating() ? 'Edit Meeting' : 'Create New Meeting' }}

+
+

{{ isInCreateMode() ? 'Create New Meeting' : 'Edit Meeting' }}

- +
- {{ isLoading ? (isEditingOrCreating() ? 'Updating...' : 'Creating...') : (isEditingOrCreating() ? 'Update Meeting' : 'Create Meeting') }} + {{ isLoading ? (isInCreateMode() ? 'Creating...' : 'Updating...' ) : (isInCreateMode() ? 'Create Meeting' : 'Update Meeting') }}
+ +
+
+

Meeting Members

+
+ + Open page that meeting members see + + + +
+
@@ -159,6 +179,7 @@ import { Component, Vue } from 'vue-facing-decorator'; import QuickNav from '@/components/QuickNav.vue'; import TopMessage from '@/components/TopMessage.vue'; +import MembersList from '@/components/MembersList.vue'; import { logConsoleAndDb, retrieveSettingsForActiveAccount } from '@/db/index'; import { errorStringForLog, getHeaders, serverMessageForUser } from '@/libs/endorserServer'; import { encryptMessage } from '@/libs/crypto'; @@ -182,6 +203,7 @@ interface MeetingSetupInfo { components: { QuickNav, TopMessage, + MembersList, }, }) export default class OnboardMeetingView extends Vue { @@ -211,7 +233,11 @@ export default class OnboardMeetingView extends Vue { this.isLoading = false; } - isEditingOrCreating(): boolean { + isInCreateMode(): boolean { + return this.newOrUpdatedMeeting != null && this.currentMeeting == null; + } + + isInEditOrCreateMode(): boolean { return this.newOrUpdatedMeeting != null; } @@ -256,13 +282,11 @@ export default class OnboardMeetingView extends Vue { ); if (response?.data?.data) { - console.log('Response data', response.data.data); this.currentMeeting = { ...response.data.data, userFullName: this.fullName, password: this.currentMeeting?.password || "", }; - console.log('Current meeting', this.currentMeeting); } else { // no meeting found this.newOrUpdatedMeeting = this.blankMeeting(); @@ -345,6 +369,7 @@ export default class OnboardMeetingView extends Vue { ...this.newOrUpdatedMeeting, groupId: response.data.success.groupId, }; + this.newOrUpdatedMeeting = null; this.$notify( { @@ -436,7 +461,6 @@ export default class OnboardMeetingView extends Vue { startEditing() { // Populate form with existing meeting data if (this.currentMeeting) { - console.log('Current meeting', this.currentMeeting); const localExpiresAt = new Date(this.currentMeeting.expiresAt); this.newOrUpdatedMeeting = { name: this.currentMeeting.name, @@ -520,14 +544,12 @@ export default class OnboardMeetingView extends Vue { ); if (response.data && response.data.success) { - console.log('Updated meeting', response.data); // Update the current meeting with only the necessary fields this.currentMeeting = { ...this.newOrUpdatedMeeting, groupId: this.currentMeeting?.groupId || "", }; this.newOrUpdatedMeeting = null; - console.log('Updated meeting now', this.currentMeeting); } else { throw { response: response }; } @@ -547,5 +569,17 @@ export default class OnboardMeetingView extends Vue { this.isLoading = false; } } + + handleMembersError(message: string) { + this.$notify( + { + group: 'alert', + type: 'danger', + title: 'Error', + text: message, + }, + 5000 + ); + } } \ No newline at end of file From dd281e78fdd3b215664ec274f2c00f0482cdba55 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Mon, 3 Feb 2025 15:31:00 -0700 Subject: [PATCH 06/16] show when an onboarding member is already in a meeting, and allow them to leave --- src/main.ts | 2 + src/views/OnboardMeetingListView.vue | 110 ++++++++++++++++++++++++--- 2 files changed, 101 insertions(+), 11 deletions(-) diff --git a/src/main.ts b/src/main.ts index 2aa9c6b..df2f5d3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -74,6 +74,7 @@ import { faPlus, faQuestion, faQrcode, + faRightFromBracket, faRotate, faShareNodes, faSpinner, @@ -153,6 +154,7 @@ library.add( faQrcode, faQuestion, faRotate, + faRightFromBracket, faShareNodes, faSpinner, faSquare, diff --git a/src/views/OnboardMeetingListView.vue b/src/views/OnboardMeetingListView.vue index ab6a6eb..4984f3a 100644 --- a/src/views/OnboardMeetingListView.vue +++ b/src/views/OnboardMeetingListView.vue @@ -13,13 +13,32 @@
+
+

You are in this meeting.

+
+

{{ attendingMeeting.name }}

+
+ +
+
+
+
+ class="p-4 bg-white rounded-lg shadow hover:shadow-md transition-shadow cursor-pointer" + @click="promptPassword(meeting)" + >

{{ meeting.name }}

-

Group ID: {{ meeting.groupId }}

@@ -32,6 +51,7 @@

Enter Meeting Password

void; - meetings: Meeting[] = []; - isLoading = false; - showPasswordDialog = false; - password = ''; - selectedMeeting: Meeting | null = null; activeDid = ''; apiServer = ''; + attendingMeeting: Meeting | null = null; firstName = ''; + isLoading = false; + meetings: Meeting[] = []; + password = ''; + selectedMeeting: Meeting | null = null; + showPasswordDialog = false; async created() { const settings = await retrieveSettingsForActiveAccount(); @@ -99,14 +121,40 @@ export default class OnboardMeetingListView extends Vue { async fetchMeetings() { this.isLoading = true; try { + // get the meeting that the user is attending const headers = await getHeaders(this.activeDid); const response = await this.axios.get( - this.apiServer + '/api/partner/groupsOnboarding', + this.apiServer + '/api/partner/groupOnboardMember', { headers } ); + + if (response.data?.data) { + // they're in a meeting already + const attendingMeetingId = response.data.data.groupId; + // retrieve the meeting details + const headers2 = await getHeaders(this.activeDid); + const response2 = await this.axios.get( + this.apiServer + '/api/partner/groupOnboard/' + attendingMeetingId, + { headers: headers2 } + ); + + if (response2.data?.data) { + this.attendingMeeting = response2.data.data; + return; + } else { + // this should never happen + logConsoleAndDb('Error fetching meeting for user after saying they are in one.', true); + } + } + + const headers2 = await getHeaders(this.activeDid); + const response2 = await this.axios.get( + this.apiServer + '/api/partner/groupsOnboarding', + { headers: headers2 } + ); - if (response.data && response.data.data) { - this.meetings = response.data.data; + if (response2.data?.data) { + this.meetings = response2.data.data; } } catch (error) { logConsoleAndDb('Error fetching meetings: ' + errorStringForLog(error), true); @@ -128,6 +176,12 @@ export default class OnboardMeetingListView extends Vue { this.password = ''; this.selectedMeeting = meeting; this.showPasswordDialog = true; + nextTick(() => { + const input = this.$refs.passwordInput as HTMLInputElement; + if (input) { + input.focus(); + } + }); } cancelPasswordDialog() { @@ -195,5 +249,39 @@ export default class OnboardMeetingListView extends Vue { ); } } + + async leaveMeeting() { + try { + const headers = await getHeaders(this.activeDid); + await this.axios.delete( + this.apiServer + '/api/partner/groupOnboardMember', + { headers } + ); + + this.attendingMeeting = null; + await this.fetchMeetings(); + + this.$notify( + { + group: 'alert', + type: 'success', + title: 'Success', + text: 'Successfully left the meeting.', + }, + 5000 + ); + } catch (error) { + logConsoleAndDb('Error leaving meeting: ' + errorStringForLog(error), true); + this.$notify( + { + group: 'alert', + type: 'danger', + title: 'Error', + text: serverMessageForUser(error) || 'Failed to leave meeting.', + }, + 5000 + ); + } + } } \ No newline at end of file From 64830eeb05f243ec99f2234a0789bbf35a226743 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Mon, 3 Feb 2025 16:36:13 -0700 Subject: [PATCH 07/16] fix linting (and change a little wording in onboarding page) --- src/components/ChoiceButtonDialog.vue | 4 +- src/components/GiftedDialog.vue | 6 +- src/components/MembersList.vue | 68 +++-- src/components/OfferDialog.vue | 5 +- src/libs/crypto/index.ts | 58 ++-- src/libs/endorserServer.ts | 1 + src/router/index.ts | 18 +- src/views/ContactsView.vue | 31 ++- src/views/OnboardMeetingListView.vue | 166 ++++++----- src/views/OnboardMeetingMembersView.vue | 22 +- src/views/OnboardMeetingSetupView.vue | 353 +++++++++++++++--------- 11 files changed, 442 insertions(+), 290 deletions(-) diff --git a/src/components/ChoiceButtonDialog.vue b/src/components/ChoiceButtonDialog.vue index 0660a7c..2e8118a 100644 --- a/src/components/ChoiceButtonDialog.vue +++ b/src/components/ChoiceButtonDialog.vue @@ -117,7 +117,7 @@ export default class PromptDialog extends Vue { onOption3: this.onOption3, onCancel: this.onCancel, } as NotificationIface, - -1 + -1, ); } @@ -149,4 +149,4 @@ export default class PromptDialog extends Vue { close("string that does not matter"); } } - \ No newline at end of file + diff --git a/src/components/GiftedDialog.vue b/src/components/GiftedDialog.vue index 832c456..c7e16a2 100644 --- a/src/components/GiftedDialog.vue +++ b/src/components/GiftedDialog.vue @@ -90,7 +90,11 @@ import { Vue, Component, Prop } from "vue-facing-decorator"; import { NotificationIface } from "@/constants/app"; -import { createAndSubmitGive, didInfo, serverMessageForUser } from "@/libs/endorserServer"; +import { + createAndSubmitGive, + didInfo, + serverMessageForUser, +} from "@/libs/endorserServer"; import * as libsUtil from "@/libs/util"; import { db, retrieveSettingsForActiveAccount } from "@/db/index"; import { Contact } from "@/db/tables/contacts"; diff --git a/src/components/MembersList.vue b/src/components/MembersList.vue index d7aa884..439bf1a 100644 --- a/src/components/MembersList.vue +++ b/src/components/MembersList.vue @@ -7,8 +7,11 @@
-
+

{{ member.name }}

{{ member.did }}

@@ -16,18 +19,29 @@

No members have joined this meeting yet

-

- {{ decryptFailureMessage || "Your password failed. Please go back and try again." }} +

+ {{ + decryptFailureMessage || + "Your password failed. Please go back and try again." + }}

\ No newline at end of file + diff --git a/src/components/OfferDialog.vue b/src/components/OfferDialog.vue index 20aee1f..97fd9a2 100644 --- a/src/components/OfferDialog.vue +++ b/src/components/OfferDialog.vue @@ -83,7 +83,10 @@ import { Vue, Component, Prop } from "vue-facing-decorator"; import { NotificationIface } from "@/constants/app"; -import { createAndSubmitOffer, serverMessageForUser } from "@/libs/endorserServer"; +import { + createAndSubmitOffer, + serverMessageForUser, +} from "@/libs/endorserServer"; import * as libsUtil from "@/libs/util"; import { retrieveSettingsForActiveAccount } from "@/db/index"; diff --git a/src/libs/crypto/index.ts b/src/libs/crypto/index.ts index 74409ab..2de20e3 100644 --- a/src/libs/crypto/index.ts +++ b/src/libs/crypto/index.ts @@ -177,43 +177,43 @@ export async function encryptMessage(message: string, password: string) { // Derive key from password using PBKDF2 const keyMaterial = await crypto.subtle.importKey( - 'raw', + "raw", encoder.encode(password), - 'PBKDF2', + "PBKDF2", false, - ['deriveBits', 'deriveKey'] + ["deriveBits", "deriveKey"], ); const key = await crypto.subtle.deriveKey( { - name: 'PBKDF2', + name: "PBKDF2", salt, iterations: ITERATIONS, - hash: 'SHA-256' + hash: "SHA-256", }, keyMaterial, - { name: 'AES-GCM', length: KEY_LENGTH }, + { name: "AES-GCM", length: KEY_LENGTH }, false, - ['encrypt'] + ["encrypt"], ); // Encrypt the message const encryptedContent = await crypto.subtle.encrypt( { - name: 'AES-GCM', - iv + name: "AES-GCM", + iv, }, key, - encoder.encode(message) + encoder.encode(message), ); // Return a JSON structure with base64-encoded components const result = { salt: arrayBufferToBase64(salt), iv: arrayBufferToBase64(iv), - encrypted: arrayBufferToBase64(encryptedContent) + encrypted: arrayBufferToBase64(encryptedContent), }; - + return btoa(JSON.stringify(result)); } @@ -229,34 +229,34 @@ export async function decryptMessage(encryptedJson: string, password: string) { // Derive the same key using PBKDF2 with the extracted salt const keyMaterial = await crypto.subtle.importKey( - 'raw', + "raw", new TextEncoder().encode(password), - 'PBKDF2', + "PBKDF2", false, - ['deriveBits', 'deriveKey'] + ["deriveBits", "deriveKey"], ); const key = await crypto.subtle.deriveKey( { - name: 'PBKDF2', + name: "PBKDF2", salt: saltArray, iterations: ITERATIONS, - hash: 'SHA-256' + hash: "SHA-256", }, keyMaterial, - { name: 'AES-GCM', length: KEY_LENGTH }, + { name: "AES-GCM", length: KEY_LENGTH }, false, - ['decrypt'] + ["decrypt"], ); // Decrypt the content const decryptedContent = await crypto.subtle.decrypt( { - name: 'AES-GCM', - iv: ivArray + name: "AES-GCM", + iv: ivArray, }, key, - encryptedContent + encryptedContent, ); // Convert the decrypted content back to a string @@ -268,33 +268,33 @@ export async function testEncryptionDecryption() { try { const testMessage = "Hello, this is a test message! 🚀"; const testPassword = "myTestPassword123"; - + console.log("Original message:", testMessage); - + // Test encryption console.log("Encrypting..."); const encrypted = await encryptMessage(testMessage, testPassword); console.log("Encrypted result:", encrypted); - + // Test decryption console.log("Decrypting..."); const decrypted = await decryptMessage(encrypted, testPassword); console.log("Decrypted result:", decrypted); - + // Verify const success = testMessage === decrypted; console.log("Test " + (success ? "PASSED ✅" : "FAILED ❌")); console.log("Messages match:", success); - + // Test with wrong password console.log("\nTesting with wrong password..."); try { - const wrongDecrypted = await decryptMessage(encrypted, "wrongPassword"); + await decryptMessage(encrypted, "wrongPassword"); console.log("Should not reach here"); } catch (error) { console.log("Correctly failed with wrong password ✅"); } - + return success; } catch (error) { console.error("Test failed with error:", error); diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index ab9c4fc..3b3ec3d 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -680,6 +680,7 @@ export async function setPlanInCache( * @param error that is thrown from an Endorser server call by Axios * @returns user-friendly message, or undefined if none found */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function serverMessageForUser(error: any) { return ( // this is how most user messages are returned diff --git a/src/router/index.ts b/src/router/index.ts index 3bb330e..825db95 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -180,19 +180,19 @@ const routes: Array = [ component: () => import("../views/OfferDetailsView.vue"), }, { - path: '/onboard-meeting-list', - name: 'onboard-meeting-list', - component: () => import('../views/OnboardMeetingListView.vue'), + path: "/onboard-meeting-list", + name: "onboard-meeting-list", + component: () => import("../views/OnboardMeetingListView.vue"), }, { - path: '/onboard-meeting-members/:groupId', - name: 'onboard-meeting-members', - component: () => import('../views/OnboardMeetingMembersView.vue'), + path: "/onboard-meeting-members/:groupId", + name: "onboard-meeting-members", + component: () => import("../views/OnboardMeetingMembersView.vue"), }, { - path: '/onboard-meeting-setup', - name: 'onboard-meeting-setup', - component: () => import('../views/OnboardMeetingSetupView.vue'), + path: "/onboard-meeting-setup", + name: "onboard-meeting-setup", + component: () => import("../views/OnboardMeetingSetupView.vue"), }, { path: "/project/:id?", diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index 9ce3679..7728052 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -37,23 +37,24 @@ - - + + + icon="envelope-open-text" + class="fa-fw text-2xl" + @click=" + warning( + 'You must get registered before you can create invites.', + 'Not Registered', + ) + " + /> - +
-
@@ -47,7 +49,10 @@
-
+

Enter Meeting Password

\ No newline at end of file + diff --git a/src/views/OnboardMeetingMembersView.vue b/src/views/OnboardMeetingMembersView.vue index db0a34b..d80e5e0 100644 --- a/src/views/OnboardMeetingMembersView.vue +++ b/src/views/OnboardMeetingMembersView.vue @@ -38,10 +38,12 @@ \ No newline at end of file + diff --git a/src/views/OnboardMeetingSetupView.vue b/src/views/OnboardMeetingSetupView.vue index d213978..f88e2a7 100644 --- a/src/views/OnboardMeetingSetupView.vue +++ b/src/views/OnboardMeetingSetupView.vue @@ -22,7 +22,9 @@ title="Edit Meeting" > - {{ isInCreateMode() ? 'Create Meeting' : 'Edit Meeting' }} + {{ + isInCreateMode() ? "Create Meeting" : "Edit Meeting" + }}

Name: {{ currentMeeting.name }}

-

Expires: {{ formatExpirationTime(currentMeeting.expiresAt) }}

+

+ Expires: + {{ formatExpirationTime(currentMeeting.expiresAt) }} +

-

Share the password with the people you want to onboard.

+

+ Share the password with the people you want to onboard. +

- Your copy of the password is not saved. Edit the meeting, or delete it and create a new meeting. + Your copy of the password is not saved. Edit the meeting, or delete it + and create a new meeting.
- -
+

Delete Meeting?

-

This action cannot be undone. Are you sure you want to delete this meeting?

+

+ This action cannot be undone. Are you sure you want to delete this + meeting? +

+

{{ member.did }}

-

+

No members have joined this meeting yet

-

{{ attendingMeeting.name }}

-
+
+

{{ attendingMeeting.name }}

- Open page that meeting members see + Open shortcut page for members Date: Mon, 3 Feb 2025 19:55:41 -0700 Subject: [PATCH 10/16] add an icon for each attendee to add them to their contact list --- src/components/MembersList.vue | 151 ++++++++++++++++++++++++-- src/views/OnboardMeetingSetupView.vue | 1 + 2 files changed, 142 insertions(+), 10 deletions(-) diff --git a/src/components/MembersList.vue b/src/components/MembersList.vue index f7e5295..afa1fd7 100644 --- a/src/components/MembersList.vue +++ b/src/components/MembersList.vue @@ -7,27 +7,79 @@
+
You are not yet admitted. The organizer will admit you.
+ +
+ +
-
-

{{ member.name }}

- + + +
+
- - + + +

{{ member.did }}

+
+ +

No members have joined this meeting yet @@ -41,6 +93,7 @@ "Your password failed. Please go back and try again." }}

+
@@ -48,13 +101,14 @@ diff --git a/src/views/OnboardMeetingSetupView.vue b/src/views/OnboardMeetingSetupView.vue index 24b0c4e..b6cc8e7 100644 --- a/src/views/OnboardMeetingSetupView.vue +++ b/src/views/OnboardMeetingSetupView.vue @@ -209,6 +209,7 @@ From dffecae565911033289d6dd587ce08e32735c69d Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Mon, 3 Feb 2025 20:31:22 -0700 Subject: [PATCH 11/16] now add registration when the organizer admits them --- src/components/MembersList.vue | 49 +++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/src/components/MembersList.vue b/src/components/MembersList.vue index afa1fd7..ea1e503 100644 --- a/src/components/MembersList.vue +++ b/src/components/MembersList.vue @@ -34,7 +34,7 @@ class="flex items-center" >
@@ -101,7 +106,11 @@ diff --git a/src/views/OnboardMeetingMembersView.vue b/src/views/OnboardMeetingMembersView.vue index d80e5e0..6618fc3 100644 --- a/src/views/OnboardMeetingMembersView.vue +++ b/src/views/OnboardMeetingMembersView.vue @@ -8,15 +8,6 @@ Meeting Members - - -
diff --git a/src/views/OnboardMeetingSetupView.vue b/src/views/OnboardMeetingSetupView.vue index 5d3011c..d6af370 100644 --- a/src/views/OnboardMeetingSetupView.vue +++ b/src/views/OnboardMeetingSetupView.vue @@ -211,7 +211,7 @@ :decrypt-failure-message="DECRYPT_FAILURE_MESSAGE" :show-organizer-tools="true" @error="handleMembersError" - class="mt-8" + class="mt-4" />
From fe71c3f75447e568b71450cf2c14848973af2be7 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 5 Feb 2025 20:07:25 -0700 Subject: [PATCH 15/16] make member view available to onboard meeting organizer and reorganize buttons --- src/components/MembersList.vue | 261 +++++++++++++++----------- src/libs/util.ts | 15 ++ src/views/ContactsView.vue | 17 +- src/views/OnboardMeetingSetupView.vue | 2 +- 4 files changed, 165 insertions(+), 130 deletions(-) diff --git a/src/components/MembersList.vue b/src/components/MembersList.vue index 24983ca..58803f6 100644 --- a/src/components/MembersList.vue +++ b/src/components/MembersList.vue @@ -27,37 +27,36 @@ v-if="showOrganizerTools && isOrganizer" class="inline-flex items-center flex-wrap" > - Use these next to each person to add/remove them to/from the - -  meeting: + + Use + and + to add/remove them to/from the meeting.
- - Use this next to each person to add them to your - -  contacts: - - - + + Use + + + to add them to your contacts.
-
+
-
+

{{ member.name }}

+
+ +
+ +
+
-
- - -
-

{{ member.did }}

+

+ {{ member.did }} +

-
+
@@ -1373,21 +1373,6 @@ export default class ContactsView extends Vue { }); } - private shortDid(did: string) { - if (did.startsWith("did:peer:")) { - return ( - did.substring(0, "did:peer:".length + 2) + - "..." + - did.substring("did:peer:".length + 18, "did:peer:".length + 25) + - "..." - ); - } else if (did.startsWith("did:ethr:")) { - return did.substring(0, "did:ethr:".length + 9) + "..."; - } else { - return did.substring(0, did.indexOf(":", 4) + 7) + "..."; - } - } - private showCopySelectionsInfo() { this.$notify( { diff --git a/src/views/OnboardMeetingSetupView.vue b/src/views/OnboardMeetingSetupView.vue index d6af370..8b1c004 100644 --- a/src/views/OnboardMeetingSetupView.vue +++ b/src/views/OnboardMeetingSetupView.vue @@ -265,7 +265,7 @@ export default class OnboardMeetingView extends Vue { ) => void; DECRYPT_FAILURE_MESSAGE = - "Unable to decrypt some member information. Check your password, or have them reset theirs."; + "Unable to decrypt some member information. Check your password, or have them reset theirs if they don't show here."; currentMeeting: ServerMeeting | null = null; newOrUpdatedMeeting: MeetingSetupInfo | null = null; From 9411096ab795637cba5b568b194207b8ff354989 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 5 Feb 2025 21:03:57 -0700 Subject: [PATCH 16/16] prompt organizer about adding a contact if not in list, and other sanity checks --- src/components/MembersList.vue | 85 ++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 19 deletions(-) diff --git a/src/components/MembersList.vue b/src/components/MembersList.vue index 58803f6..32850aa 100644 --- a/src/components/MembersList.vue +++ b/src/components/MembersList.vue @@ -18,8 +18,8 @@

-
- You are not yet admitted. The organizer will admit you. +
+ You are not admitted. The organizer will admit you.
@@ -74,7 +74,7 @@

{{ member.name }}