From f33dee9b7a7ccb9f38c07615442e90b706b585c3 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 25 Apr 2025 20:14:30 -0600 Subject: [PATCH] do not share the invite JWT with the server; always keep it on the client, generated at will --- src/views/InviteOneView.vue | 39 ++++++++++++++++--------------- test-playwright/05-invite.spec.ts | 5 ++++ 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/views/InviteOneView.vue b/src/views/InviteOneView.vue index 563bec4b..f1cbfa36 100644 --- a/src/views/InviteOneView.vue +++ b/src/views/InviteOneView.vue @@ -73,17 +73,15 @@ invite.expiresAt > new Date().toISOString() " class="text-center text-blue-500 cursor-pointer" - :title="inviteLink(invite.jwt)" - @click=" - copyInviteAndNotify(invite.inviteIdentifier, invite.jwt) - " + title="Click to copy invite link" + @click="copyInviteAndNotify(invite)" > {{ getTruncatedInviteId(invite.inviteIdentifier) }} + + @@ -146,7 +146,7 @@ import { logger } from "../utils/logger"; interface Invite { inviteIdentifier: string; expiresAt: string; - jwt: string; + jwt?: string; // only used to store a JWT for testing, after the link is clicked notes: string; redeemedAt: string | null; redeemedBy: string | null; @@ -220,18 +220,27 @@ export default class InviteOneView extends Vue { return `${redeemedBy.slice(0, 13)}...${redeemedBy.slice(-3)}`; } - inviteLink(jwt: string): string { + inviteLink(jwt: string | undefined): string { + if (!jwt) return "Click to set JWT link"; return APP_SERVER + "/invite-one-accept/" + jwt; } - copyInviteAndNotify(inviteId: string, jwt: string) { - useClipboard().copy(this.inviteLink(jwt)); + async copyInviteAndNotify(invite: Invite) { + const expiresIn = (new Date(invite.expiresAt).getTime() - Date.now()) / 1000; + const inviteJwt = await createInviteJwt( + this.activeDid, + undefined, + invite.inviteIdentifier, + expiresIn, + ); + invite.jwt = inviteJwt; // set for testing + useClipboard().copy(this.inviteLink(inviteJwt)); this.$notify( { group: "alert", type: "success", title: "Copied", - text: "Your clipboard now contains the link for invite " + inviteId, + text: "Your clipboard now contains the link for invite " + invite.inviteIdentifier, }, 5000, ); @@ -295,22 +304,14 @@ export default class InviteOneView extends Vue { }, }; } - const expiresIn = (new Date(expiresAt).getTime() - Date.now()) / 1000; - const inviteJwt = await createInviteJwt( - this.activeDid, - undefined, - inviteIdentifier, - expiresIn, - ); await axios.post( this.apiServer + "/api/userUtil/invite", - { inviteJwt: inviteJwt, notes: notes }, + { inviteIdentifier: inviteIdentifier, notes: notes, expiresAt: expiresAt }, { headers }, ); const newInvite = { inviteIdentifier: inviteIdentifier, expiresAt: expiresAt, - jwt: inviteJwt, notes: notes, redeemedAt: null, redeemedBy: null, diff --git a/test-playwright/05-invite.spec.ts b/test-playwright/05-invite.spec.ts index 821f7c39..c0c34668 100644 --- a/test-playwright/05-invite.spec.ts +++ b/test-playwright/05-invite.spec.ts @@ -44,6 +44,11 @@ test('Check User 0 can invite someone', async ({ page }) => { await expect(page.locator('div[role="alert"] button:has-text("Yes")')).toBeHidden(); // check that the invite is in the list + const newInviteRow = page.locator(`tr:has(td:has-text("Neighbor ${neighborNum}"))`); + await expect(newInviteRow).toBeVisible(); + // click on the link in the first column, which generates the JWT in the other column + await newInviteRow.locator('td:first-child').click(); + const newInviteLine = page.locator(`td:has-text("Neighbor ${neighborNum}")`); await expect(newInviteLine).toBeVisible(); // retrieve the link from the title