diff --git a/src/components/InviteDialog.vue b/src/components/InviteDialog.vue new file mode 100644 index 000000000..b514608bf --- /dev/null +++ b/src/components/InviteDialog.vue @@ -0,0 +1,118 @@ + + + + {{ title }} + + {{ message }} + + + + Expiration + + + + + + Save + + + + Cancel + + + + + + + + + + diff --git a/src/components/UserNameDialog.vue b/src/components/UserNameDialog.vue index bd412ae2d..50747860c 100644 --- a/src/components/UserNameDialog.vue +++ b/src/components/UserNameDialog.vue @@ -46,7 +46,7 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; export default class UserNameDialog extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; - callback: (string?) => void = () => {}; + callback: (name?: string) => void = () => {}; givenName = ""; visible = false; diff --git a/src/libs/crypto/vc/index.ts b/src/libs/crypto/vc/index.ts index bc1caabf6..677b4a88b 100644 --- a/src/libs/crypto/vc/index.ts +++ b/src/libs/crypto/vc/index.ts @@ -54,16 +54,22 @@ export function isFromPasskey(keyMeta?: KeyMeta): boolean { export async function createEndorserJwtForKey( account: KeyMeta, payload: object, + expiresIn?: number, ) { if (account?.identity) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const identity: IIdentifier = JSON.parse(account.identity!); const privateKeyHex = identity.keys[0].privateKeyHex; const signer = await SimpleSigner(privateKeyHex as string); - return didJwt.createJWT(payload, { + const options = { issuer: account.did, signer: signer, - }); + expiresIn: undefined as number | undefined, + } + if (expiresIn) { + options.expiresIn = expiresIn; + } + return didJwt.createJWT(payload, options); } else if (account?.passkeyCredIdHex) { return createDidPeerJwt(account.did, account.passkeyCredIdHex, payload); } else { diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index f6887383f..ae6484e45 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -239,8 +239,9 @@ export interface RegisterVerifiableCredential { "@context": string; "@type": string; agent: { identifier: string }; + identifier?: string; object: string; - participant: { identifier: string }; + participant?: { identifier: string }; } // now for some of the error & other wrapper types @@ -993,9 +994,10 @@ export async function generateEndorserJwtForAccount( export async function createEndorserJwtForDid( issuerDid: string, payload: object, + expiresIn?: number, ) { const account = await getAccount(issuerDid); - return createEndorserJwtForKey(account as KeyMeta, payload); + return createEndorserJwtForKey(account as KeyMeta, payload, expiresIn); } /** @@ -1225,19 +1227,24 @@ export async function createEndorserJwtVcFromClaim( return createEndorserJwtForDid(issuerDid, vcPayload); } -export async function register( +export async function createInviteJwt( activeDid: string, - apiServer: string, - axios: Axios, - contact: Contact, -) { + contact?: Contact, + inviteId?: string, + expiresIn?: number, +): Promise { const vcClaim: RegisterVerifiableCredential = { "@context": SCHEMA_ORG_CONTEXT, "@type": "RegisterAction", agent: { identifier: activeDid }, object: SERVICE_ID, - participant: { identifier: contact.did }, }; + if (contact) { + vcClaim.participant = { identifier: contact.did }; + } + if (inviteId) { + vcClaim.identifier = inviteId; + } // Make a payload for the claim const vcPayload = { vc: { @@ -1247,7 +1254,17 @@ export async function register( }, }; // Create a signature using private key of identity - const vcJwt = await createEndorserJwtForDid(activeDid, vcPayload); + const vcJwt = await createEndorserJwtForDid(activeDid, vcPayload, expiresIn); + return vcJwt; +} + +export async function register( + activeDid: string, + apiServer: string, + axios: Axios, + contact: Contact, +): Promise<{ success?: boolean; error?: string }> { + const vcJwt = await createInviteJwt(activeDid, contact); const url = apiServer + "/api/v2/claim"; const resp = await axios.post(url, { jwtEncoded: vcJwt }); diff --git a/src/main.ts b/src/main.ts index 595a80cdd..46e9e7029 100644 --- a/src/main.ts +++ b/src/main.ts @@ -39,6 +39,7 @@ import { faDollar, faEllipsis, faEllipsisVertical, + faEnvelopeOpenText, faEye, faEyeSlash, faFileLines, @@ -109,6 +110,7 @@ library.add( faDollar, faEllipsis, faEllipsisVertical, + faEnvelopeOpenText, faEye, faEyeSlash, faFileLines, diff --git a/src/router/index.ts b/src/router/index.ts index b19aa51b2..3ceda6a6d 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -128,6 +128,11 @@ const routes: Array = [ name: "import-derive", component: () => import("../views/ImportDerivedAccountView.vue"), }, + { + path: "/invite-one", + name: "invite-one", + component: () => import("../views/InviteOneView.vue"), + }, { path: "/new-edit-account", name: "new-edit-account", diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index 7dc9dc388..b62d9b9f6 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -23,12 +23,20 @@ + + + + + - {{ showGiveNumbers ? "Hide Given Hours" : "Show Given Hours" }} + {{ + showGiveNumbers ? "Hide Given Hours etc" : "Show Given Hours etc" + }} @@ -288,7 +298,7 @@ import { Buffer } from "buffer/"; import { IndexableType } from "dexie"; import * as R from "ramda"; import { Component, Vue } from "vue-facing-decorator"; -import { Router } from "vue-router"; +import { RouteLocationNormalizedLoaded, Router } from "vue-router"; import { useClipboard } from "@vueuse/core"; import { AppString, NotificationIface } from "@/constants/app"; @@ -377,7 +387,7 @@ export default class ContactsView extends Vue { (a.name || "").localeCompare(b.name || ""), ); - const importedContactJwt = (this.$route as Router).query[ + const importedContactJwt = (this.$route as RouteLocationNormalizedLoaded).query[ "contactJwt" ] as string; if (importedContactJwt) { @@ -736,7 +746,7 @@ export default class ContactsView extends Vue { type: "confirm", title: "Register", text: "Do you want to register them?", - onCancel: async (stopAsking: boolean) => { + onCancel: async (stopAsking?: boolean) => { if (stopAsking) { await updateDefaultSettings({ hideRegisterPromptOnNewContact: stopAsking, @@ -744,7 +754,7 @@ export default class ContactsView extends Vue { this.hideRegisterPromptOnNewContact = stopAsking; } }, - onNo: async (stopAsking: boolean) => { + onNo: async (stopAsking?: boolean) => { if (stopAsking) { await updateDefaultSettings({ hideRegisterPromptOnNewContact: stopAsking, @@ -853,9 +863,14 @@ export default class ContactsView extends Vue { console.error("Error when registering:", error); let userMessage = "There was an error. See logs for more info."; const serverError = error as AxiosError; - if (serverError) { - if (serverError.response?.data?.error?.message) { - userMessage = serverError.response.data.error.message; + if (serverError.isAxiosError) { + if (serverError.response?.data + && typeof serverError.response.data === 'object' + && 'error' in serverError.response.data + && typeof serverError.response.data.error === 'object' + && serverError.response.data.error !== null + && 'message' in serverError.response.data.error){ + userMessage = serverError.response.data.error.message as string; } else if (serverError.message) { userMessage = serverError.message; // Info for the user } else { @@ -971,8 +986,8 @@ export default class ContactsView extends Vue { } private showGiftedDialog(giverDid: string, recipientDid: string) { - let giver: libsUtil.GiverReceiverInputInfo; - let receiver: libsUtil.GiverReceiverInputInfo; + let giver: libsUtil.GiverReceiverInputInfo | undefined; + let receiver: libsUtil.GiverReceiverInputInfo | undefined; if (giverDid) { giver = { did: giverDid, @@ -995,7 +1010,7 @@ export default class ContactsView extends Vue { newList[recipientDid] = (newList[recipientDid] || 0) + amount; this.givenByMeUnconfirmed = newList; }; - customTitle = "Given to " + receiver.name; + customTitle = "Given to " + (receiver?.name || "Someone Unnamed"); } else { // must be (recipientDid == this.activeDid) callback = (amount: number) => { @@ -1003,14 +1018,14 @@ export default class ContactsView extends Vue { newList[giverDid] = (newList[giverDid] || 0) + amount; this.givenToMeUnconfirmed = newList; }; - customTitle = "Received from " + giver.name; + customTitle = "Received from " + (giver?.name || "Someone Unnamed"); } (this.$refs.customGivenDialog as GiftedDialog).open( giver, receiver, - undefined as string, + undefined as unknown as string, customTitle, - undefined as string, + undefined as unknown as string, callback, ); } diff --git a/src/views/InviteOneView.vue b/src/views/InviteOneView.vue new file mode 100644 index 000000000..179a7257a --- /dev/null +++ b/src/views/InviteOneView.vue @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + Invitations + + + + + + + + + + + + + + ID + Notes + Expires At + Redeemed By + + + + + + {{ getTruncatedInviteId(invite.inviteIdentifier) }} + + {{ invite.notes }} + + {{ invite.expiresAt.substring(0, 10) }} + + + {{ getTruncatedRedeemedBy(invite.redeemedBy) }} + + + + + + No invites found. + + + diff --git a/src/views/NewEditProjectView.vue b/src/views/NewEditProjectView.vue index b89d35360..30f2cb8db 100644 --- a/src/views/NewEditProjectView.vue +++ b/src/views/NewEditProjectView.vue @@ -196,7 +196,7 @@ import { accountFromSeedWords } from "nostr-tools/nip06"; import { finalizeEvent, serializeEvent } from "nostr-tools/pure"; import { Component, Vue } from "vue-facing-decorator"; import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet"; -import { Router } from "vue-router"; +import { RouteLocationNormalizedLoaded, Router } from "vue-router"; import ImageMethodDialog from "@/components/ImageMethodDialog.vue"; import QuickNav from "@/components/QuickNav.vue"; @@ -218,7 +218,7 @@ import { getAccount } from "@/libs/util"; }) export default class NewEditProjectView extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; - errNote(message) { + errNote(message: string) { this.$notify( { group: "alert", type: "danger", title: "Error", text: message }, 5000, @@ -262,7 +262,7 @@ export default class NewEditProjectView extends Vue { this.apiServer = settings.apiServer || ""; this.showGeneralAdvanced = !!settings.showGeneralAdvanced; - this.projectId = (this.$route as Router).query["projectId"] || ""; + this.projectId = (this.$route as RouteLocationNormalizedLoaded).query["projectId"] || ""; if (this.projectId) { if (this.numAccounts === 0) { @@ -447,7 +447,7 @@ export default class NewEditProjectView extends Vue { const projectPath = encodeURIComponent(resp.data.success.handleId); - let signedPayload: VerifiedEvent; // sign something to prove ownership of pubkey + let signedPayload: VerifiedEvent | undefined; // sign something to prove ownership of pubkey if (this.sendToTrustroots) { signedPayload = await this.signPayload(); this.sendToNostrPartner( @@ -623,7 +623,7 @@ export default class NewEditProjectView extends Vue { 5000, ); } - } catch (error) { + } catch (error: any) { console.error(`Error sending to ${serviceName}`, error); let errorMessage = `There was an error sending to ${serviceName}.`; if (error.response?.data?.error?.message) {
No invites found.