do not share the invite JWT with the server; always keep it on the client, generated for P2P relay #132
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
|
||||||
Your Contact Info
|
Share Contact Info
|
||||||
</h1>
|
</h1>
|
||||||
<p
|
<p
|
||||||
v-if="!givenName"
|
v-if="!givenName"
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
|
|
||||||
<div v-if="contacts.length > 0" class="flex justify-between">
|
<div v-if="contacts.length > 0" class="flex justify-between">
|
||||||
<div class="w-full text-left">
|
<div class="w-full text-left">
|
||||||
<div v-if="!showGiveNumbers">
|
<div>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="contactsSelected.length === contacts.length"
|
:checked="contactsSelected.length === contacts.length"
|
||||||
@@ -105,7 +105,6 @@
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="!showGiveNumbers"
|
|
||||||
href=""
|
href=""
|
||||||
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-3 px-3 py-1.5 rounded-md"
|
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-3 px-3 py-1.5 rounded-md"
|
||||||
:style="
|
:style="
|
||||||
@@ -127,28 +126,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full text-right">
|
<div v-if="!showGiveNumbers" class="w-full text-right">
|
||||||
<button
|
<button
|
||||||
href=""
|
href=""
|
||||||
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
||||||
@click="toggleShowContactAmounts()"
|
@click="toggleShowContactAmounts()"
|
||||||
>
|
>
|
||||||
{{
|
See Hours, Offer, etc
|
||||||
showGiveNumbers ? "Hide Hours, Offer, etc" : "See Hours, Offer, etc"
|
|
||||||
}}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-else class="w-full text-right">
|
||||||
<div v-if="showGiveNumbers" class="flex justify-between mt-1">
|
<span class="text-sm">
|
||||||
<div class="w-full text-right">
|
Only the most recent From/To hours show in buttons. To see more,
|
||||||
In the following, only the most recent hours are included. To see more,
|
|
||||||
click
|
click
|
||||||
<span
|
<span
|
||||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 py-1 rounded-md"
|
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 py-1 rounded-md"
|
||||||
>
|
>
|
||||||
<font-awesome icon="file-lines" class="fa-fw" />
|
<font-awesome icon="file-lines" class="fa-fw" />
|
||||||
</span>
|
</span>
|
||||||
|
</span>
|
||||||
<br />
|
<br />
|
||||||
|
<span>
|
||||||
<button
|
<button
|
||||||
href=""
|
href=""
|
||||||
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md mt-1"
|
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md mt-1"
|
||||||
@@ -164,6 +162,7 @@
|
|||||||
}}
|
}}
|
||||||
<font-awesome icon="left-right" class="fa-fw" />
|
<font-awesome icon="left-right" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -179,10 +178,10 @@
|
|||||||
class="border-b border-slate-300 pt-1 pb-1"
|
class="border-b border-slate-300 pt-1 pb-1"
|
||||||
data-testId="contactListItem"
|
data-testId="contactListItem"
|
||||||
>
|
>
|
||||||
<div class="grow overflow-hidden">
|
<div class="flex justify-between items-start">
|
||||||
<div class="flex items-center gap-3">
|
<span class="grow overflow-hidden">
|
||||||
|
<span class="flex items-center gap-3">
|
||||||
<input
|
<input
|
||||||
v-if="!showGiveNumbers"
|
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="contactsSelected.includes(contact.did)"
|
:checked="contactsSelected.includes(contact.did)"
|
||||||
class="ml-2 h-6 w-6 flex-shrink-0"
|
class="ml-2 h-6 w-6 flex-shrink-0"
|
||||||
@@ -230,18 +229,20 @@
|
|||||||
{{ contact.notes }}
|
{{ contact.notes }}
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</span>
|
||||||
<div
|
</span>
|
||||||
v-if="showGiveNumbers && contact.did != activeDid"
|
<span
|
||||||
class="ml-auto flex gap-1.5 mt-2"
|
v-if="contact.did != activeDid"
|
||||||
|
class="flex gap-1.5 ml-2"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-l-md"
|
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-l-md"
|
||||||
:title="givenToMeDescriptions[contact.did] || ''"
|
:title="givenToMeDescriptions[contact.did] || ''"
|
||||||
@click="confirmShowGiftedDialog(contact.did, activeDid)"
|
@click="confirmShowGiftedDialog(contact.did, activeDid)"
|
||||||
>
|
>
|
||||||
From:
|
From…
|
||||||
<br />
|
<br />
|
||||||
|
<span v-if="showGiveNumbers">
|
||||||
{{
|
{{
|
||||||
/* eslint-disable prettier/prettier */
|
/* eslint-disable prettier/prettier */
|
||||||
showGiveTotals
|
showGiveTotals
|
||||||
@@ -252,6 +253,7 @@
|
|||||||
: (givenToMeUnconfirmed[contact.did] || 0)
|
: (givenToMeUnconfirmed[contact.did] || 0)
|
||||||
/* eslint-enable prettier/prettier */
|
/* eslint-enable prettier/prettier */
|
||||||
}}
|
}}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -259,8 +261,9 @@
|
|||||||
:title="givenByMeDescriptions[contact.did] || ''"
|
:title="givenByMeDescriptions[contact.did] || ''"
|
||||||
@click="confirmShowGiftedDialog(activeDid, contact.did)"
|
@click="confirmShowGiftedDialog(activeDid, contact.did)"
|
||||||
>
|
>
|
||||||
To:
|
To…
|
||||||
<br />
|
<br />
|
||||||
|
<span v-if="showGiveNumbers">
|
||||||
{{
|
{{
|
||||||
/* eslint-disable prettier/prettier */
|
/* eslint-disable prettier/prettier */
|
||||||
showGiveTotals
|
showGiveTotals
|
||||||
@@ -271,6 +274,7 @@
|
|||||||
: (givenByMeUnconfirmed[contact.did] || 0)
|
: (givenByMeUnconfirmed[contact.did] || 0)
|
||||||
/* eslint-enable prettier/prettier */
|
/* eslint-enable prettier/prettier */
|
||||||
}}
|
}}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -291,7 +295,7 @@
|
|||||||
>
|
>
|
||||||
<font-awesome icon="file-lines" class="fa-fw" />
|
<font-awesome icon="file-lines" class="fa-fw" />
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -299,7 +303,6 @@
|
|||||||
|
|
||||||
<div v-if="contacts.length > 0" class="mt-2 w-full text-left">
|
<div v-if="contacts.length > 0" class="mt-2 w-full text-left">
|
||||||
<input
|
<input
|
||||||
v-if="!showGiveNumbers"
|
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="contactsSelected.length === contacts.length"
|
:checked="contactsSelected.length === contacts.length"
|
||||||
class="align-middle ml-2 h-6 w-6"
|
class="align-middle ml-2 h-6 w-6"
|
||||||
@@ -311,7 +314,6 @@
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="!showGiveNumbers"
|
|
||||||
href=""
|
href=""
|
||||||
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-3 px-3 py-1.5 rounded-md"
|
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-3 px-3 py-1.5 rounded-md"
|
||||||
:style="
|
:style="
|
||||||
@@ -448,7 +450,6 @@ export default class ContactsView extends Vue {
|
|||||||
await this.processContactJwt();
|
await this.processContactJwt();
|
||||||
await this.processInviteJwt();
|
await this.processInviteJwt();
|
||||||
|
|
||||||
this.showGiveNumbers = !!settings.showContactGivesInline;
|
|
||||||
this.hideRegisterPromptOnNewContact =
|
this.hideRegisterPromptOnNewContact =
|
||||||
!!settings.hideRegisterPromptOnNewContact;
|
!!settings.hideRegisterPromptOnNewContact;
|
||||||
|
|
||||||
|
|||||||
@@ -502,6 +502,7 @@
|
|||||||
then don't use it.
|
then don't use it.
|
||||||
<br />
|
<br />
|
||||||
As for data & privacy:
|
As for data & privacy:
|
||||||
|
</p>
|
||||||
<ul class="list-disc list-outside ml-4">
|
<ul class="list-disc list-outside ml-4">
|
||||||
<li>
|
<li>
|
||||||
If using notifications, a server stores push token data. That can be revoked at any time
|
If using notifications, a server stores push token data. That can be revoked at any time
|
||||||
@@ -522,7 +523,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">How can I contribute?</h2>
|
<h2 class="text-xl font-semibold">How can I contribute?</h2>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 class="text-4xl text-center font-light">Invitations</h1>
|
<h1 class="text-4xl text-center font-light">Single Invitations</h1>
|
||||||
|
|
||||||
<ul class="ml-8 mt-4 list-outside list-disc w-5/6">
|
<ul class="ml-8 mt-4 list-outside list-disc w-5/6">
|
||||||
<li>
|
<li>
|
||||||
@@ -73,17 +73,15 @@
|
|||||||
invite.expiresAt > new Date().toISOString()
|
invite.expiresAt > new Date().toISOString()
|
||||||
"
|
"
|
||||||
class="text-center text-blue-500 cursor-pointer"
|
class="text-center text-blue-500 cursor-pointer"
|
||||||
:title="inviteLink(invite.jwt)"
|
title="Click to copy invite link"
|
||||||
@click="
|
@click="copyInviteAndNotify(invite)"
|
||||||
copyInviteAndNotify(invite.inviteIdentifier, invite.jwt)
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
{{ getTruncatedInviteId(invite.inviteIdentifier) }}
|
{{ getTruncatedInviteId(invite.inviteIdentifier) }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="text-center text-slate-500 cursor-pointer"
|
class="text-center text-slate-500 cursor-pointer"
|
||||||
:title="inviteLink(invite.jwt)"
|
title="Click to see invite details"
|
||||||
@click="
|
@click="
|
||||||
showInvite(
|
showInvite(
|
||||||
invite.inviteIdentifier,
|
invite.inviteIdentifier,
|
||||||
@@ -120,6 +118,8 @@
|
|||||||
@click="deleteInvite(invite.inviteIdentifier, invite.notes)"
|
@click="deleteInvite(invite.inviteIdentifier, invite.notes)"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="hidden" :data-testId="invite.jwt" aria-hidden="true">
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -146,7 +146,7 @@ import { logger } from "../utils/logger";
|
|||||||
interface Invite {
|
interface Invite {
|
||||||
inviteIdentifier: string;
|
inviteIdentifier: string;
|
||||||
expiresAt: string;
|
expiresAt: string;
|
||||||
jwt: string;
|
jwt?: string; // only used to store a JWT for testing, after the link is clicked
|
||||||
notes: string;
|
notes: string;
|
||||||
redeemedAt: string | null;
|
redeemedAt: string | null;
|
||||||
redeemedBy: string | null;
|
redeemedBy: string | null;
|
||||||
@@ -220,18 +220,27 @@ export default class InviteOneView extends Vue {
|
|||||||
return `${redeemedBy.slice(0, 13)}...${redeemedBy.slice(-3)}`;
|
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;
|
return APP_SERVER + "/invite-one-accept/" + jwt;
|
||||||
}
|
}
|
||||||
|
|
||||||
copyInviteAndNotify(inviteId: string, jwt: string) {
|
async copyInviteAndNotify(invite: Invite) {
|
||||||
useClipboard().copy(this.inviteLink(jwt));
|
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(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Copied",
|
title: "Copied",
|
||||||
text: "Your clipboard now contains the link for invite " + inviteId,
|
text: "Your clipboard now contains the link for invite " + invite.inviteIdentifier,
|
||||||
},
|
},
|
||||||
5000,
|
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(
|
await axios.post(
|
||||||
this.apiServer + "/api/userUtil/invite",
|
this.apiServer + "/api/userUtil/invite",
|
||||||
{ inviteJwt: inviteJwt, notes: notes },
|
{ inviteIdentifier: inviteIdentifier, notes: notes, expiresAt: expiresAt },
|
||||||
{ headers },
|
{ headers },
|
||||||
);
|
);
|
||||||
const newInvite = {
|
const newInvite = {
|
||||||
inviteIdentifier: inviteIdentifier,
|
inviteIdentifier: inviteIdentifier,
|
||||||
expiresAt: expiresAt,
|
expiresAt: expiresAt,
|
||||||
jwt: inviteJwt,
|
|
||||||
notes: notes,
|
notes: notes,
|
||||||
redeemedAt: null,
|
redeemedAt: null,
|
||||||
redeemedBy: null,
|
redeemedBy: null,
|
||||||
|
|||||||
@@ -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();
|
await expect(page.locator('div[role="alert"] button:has-text("Yes")')).toBeHidden();
|
||||||
|
|
||||||
// check that the invite is in the list
|
// 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}")`);
|
const newInviteLine = page.locator(`td:has-text("Neighbor ${neighborNum}")`);
|
||||||
await expect(newInviteLine).toBeVisible();
|
await expect(newInviteLine).toBeVisible();
|
||||||
// retrieve the link from the title
|
// retrieve the link from the title
|
||||||
|
|||||||
Reference in New Issue
Block a user