forked from trent_larson/crowd-funder-for-time-pwa
change the contact-sharing data into a JWT for the contact-import page
This commit is contained in:
@@ -9,7 +9,6 @@
|
|||||||
import { Buffer } from "buffer/";
|
import { Buffer } from "buffer/";
|
||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from "did-jwt";
|
||||||
import { JWTVerified } from "did-jwt";
|
import { JWTVerified } from "did-jwt";
|
||||||
import { JWTDecoded } from "did-jwt/lib/JWT";
|
|
||||||
import { Resolver } from "did-resolver";
|
import { Resolver } from "did-resolver";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
import * as u8a from "uint8arrays";
|
import * as u8a from "uint8arrays";
|
||||||
@@ -41,7 +40,7 @@ export interface KeyMeta {
|
|||||||
passkeyCredIdHex?: string;
|
passkeyCredIdHex?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolver = new Resolver({ ethr: didEthLocalResolver });
|
const ethLocalResolver = new Resolver({ ethr: didEthLocalResolver });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tell whether a key is from a passkey
|
* Tell whether a key is from a passkey
|
||||||
@@ -62,6 +61,7 @@ export async function createEndorserJwtForKey(
|
|||||||
const privateKeyHex = identity.keys[0].privateKeyHex;
|
const privateKeyHex = identity.keys[0].privateKeyHex;
|
||||||
const signer = await SimpleSigner(privateKeyHex as string);
|
const signer = await SimpleSigner(privateKeyHex as string);
|
||||||
const options = {
|
const options = {
|
||||||
|
// alg: "ES256K", // "K" is the default, "K-R" is used by the server in tests
|
||||||
issuer: account.did,
|
issuer: account.did,
|
||||||
signer: signer,
|
signer: signer,
|
||||||
expiresIn: undefined as number | undefined,
|
expiresIn: undefined as number | undefined,
|
||||||
@@ -124,7 +124,8 @@ function bytesToHex(b: Uint8Array): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We should be calling 'verify' in more places, showing warnings if it fails.
|
// We should be calling 'verify' in more places, showing warnings if it fails.
|
||||||
export function decodeEndorserJwt(jwt: string): JWTDecoded {
|
// @returns JWTDecoded with { header: JWTHeader, payload: string, signature: string, data: string } (but doesn't verify the signature)
|
||||||
|
export function decodeEndorserJwt(jwt: string) {
|
||||||
return didJwt.decodeJWT(jwt);
|
return didJwt.decodeJWT(jwt);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,10 +135,8 @@ export async function decodeAndVerifyJwt(
|
|||||||
jwt: string,
|
jwt: string,
|
||||||
): Promise<Omit<JWTVerified, "didResolutionResult" | "signer" | "jwt">> {
|
): Promise<Omit<JWTVerified, "didResolutionResult" | "signer" | "jwt">> {
|
||||||
const pieces = jwt.split(".");
|
const pieces = jwt.split(".");
|
||||||
console.log("WTF decodeAndVerifyJwt", typeof jwt, jwt, pieces);
|
|
||||||
const header = JSON.parse(base64urlDecodeString(pieces[0]));
|
const header = JSON.parse(base64urlDecodeString(pieces[0]));
|
||||||
const payload = JSON.parse(base64urlDecodeString(pieces[1]));
|
const payload = JSON.parse(base64urlDecodeString(pieces[1]));
|
||||||
console.log("WTF decodeAndVerifyJwt after", header, payload);
|
|
||||||
const issuerDid = payload.iss;
|
const issuerDid = payload.iss;
|
||||||
if (!issuerDid) {
|
if (!issuerDid) {
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
@@ -149,7 +148,9 @@ export async function decodeAndVerifyJwt(
|
|||||||
|
|
||||||
if (issuerDid.startsWith(ETHR_DID_PREFIX)) {
|
if (issuerDid.startsWith(ETHR_DID_PREFIX)) {
|
||||||
try {
|
try {
|
||||||
const verified = await didJwt.verifyJWT(jwt, { resolver });
|
const verified = await didJwt.verifyJWT(jwt, {
|
||||||
|
resolver: ethLocalResolver,
|
||||||
|
});
|
||||||
return verified;
|
return verified;
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
component: () => import("../views/ContactGiftingView.vue"),
|
component: () => import("../views/ContactGiftingView.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/contact-import",
|
path: "/contact-import/:jwt?",
|
||||||
name: "contact-import",
|
name: "contact-import",
|
||||||
component: () => import("../views/ContactImportView.vue"),
|
component: () => import("../views/ContactImportView.vue"),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,10 +16,11 @@
|
|||||||
Contact Import
|
Contact Import
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<span class="flex justify-center">
|
<span v-if="contactsImporting.length > 0" class="flex justify-center">
|
||||||
<input type="checkbox" v-model="makeVisible" class="mr-2" />
|
<input type="checkbox" v-model="makeVisible" class="mr-2" />
|
||||||
Make my activity visible to these contacts.
|
Make my activity visible to these contacts.
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div v-if="sameCount > 0">
|
<div v-if="sameCount > 0">
|
||||||
<span v-if="sameCount == 1"
|
<span v-if="sameCount == 1"
|
||||||
>One contact is the same as an existing contact</span
|
>One contact is the same as an existing contact</span
|
||||||
@@ -85,17 +86,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { JWTVerified } from "did-jwt";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
|
import OfferDialog from "@/components/OfferDialog.vue";
|
||||||
import { AppString, NotificationIface } from "@/constants/app";
|
import { AppString, NotificationIface } from "@/constants/app";
|
||||||
import { db, retrieveSettingsForActiveAccount } from "@/db/index";
|
import { db, retrieveSettingsForActiveAccount } from "@/db/index";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import * as libsUtil from "@/libs/util";
|
import * as libsUtil from "@/libs/util";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import { decodeAndVerifyJwt } from "@/libs/crypto/vc/index";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
|
||||||
import OfferDialog from "@/components/OfferDialog.vue";
|
|
||||||
import { setVisibilityUtil } from "@/libs/endorserServer";
|
import { setVisibilityUtil } from "@/libs/endorserServer";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -127,9 +130,27 @@ export default class ContactImportView extends Vue {
|
|||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || "";
|
||||||
|
|
||||||
// Retrieve the imported contacts from the query parameter
|
// Retrieve the imported contacts from the query parameter
|
||||||
const importedContacts =
|
const importedContacts = (this.$route as Router).query[
|
||||||
((this.$route as Router).query["contacts"] as string) || "[]";
|
"contacts"
|
||||||
this.contactsImporting = JSON.parse(importedContacts);
|
] as string;
|
||||||
|
if (importedContacts) {
|
||||||
|
await this.setContactsSelected(JSON.parse(importedContacts));
|
||||||
|
}
|
||||||
|
|
||||||
|
// match everything after /contact-import/ in the window.location.pathname
|
||||||
|
const jwt = window.location.pathname.match(
|
||||||
|
/\/contact-import\/(ey.+)$/,
|
||||||
|
)?.[1];
|
||||||
|
if (jwt) {
|
||||||
|
// decode the JWT
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
const parsedJwt: Omit<JWTVerified, "didResolutionResult" | "signer" | "jwt"> = await decodeAndVerifyJwt(jwt);
|
||||||
|
await this.setContactsSelected(parsedJwt.payload.contacts as Contact[]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setContactsSelected(contacts: Array<Contact>) {
|
||||||
|
this.contactsImporting = contacts;
|
||||||
this.contactsSelected = new Array(this.contactsImporting.length).fill(true);
|
this.contactsSelected = new Array(this.contactsImporting.length).fill(true);
|
||||||
|
|
||||||
await db.open();
|
await db.open();
|
||||||
|
|||||||
@@ -323,7 +323,7 @@ import GiftedDialog from "@/components/GiftedDialog.vue";
|
|||||||
import OfferDialog from "@/components/OfferDialog.vue";
|
import OfferDialog from "@/components/OfferDialog.vue";
|
||||||
import ContactNameDialog from "@/components/ContactNameDialog.vue";
|
import ContactNameDialog from "@/components/ContactNameDialog.vue";
|
||||||
import TopMessage from "@/components/TopMessage.vue";
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
import { AppString, NotificationIface } from "@/constants/app";
|
import { APP_SERVER, AppString, NotificationIface } from "@/constants/app";
|
||||||
import {
|
import {
|
||||||
db,
|
db,
|
||||||
retrieveSettingsForActiveAccount,
|
retrieveSettingsForActiveAccount,
|
||||||
@@ -336,6 +336,7 @@ import { decodeEndorserJwt } from "@/libs/crypto/vc";
|
|||||||
import {
|
import {
|
||||||
CONTACT_CSV_HEADER,
|
CONTACT_CSV_HEADER,
|
||||||
CONTACT_URL_PREFIX,
|
CONTACT_URL_PREFIX,
|
||||||
|
createEndorserJwtForDid,
|
||||||
GiveSummaryRecord,
|
GiveSummaryRecord,
|
||||||
getHeaders,
|
getHeaders,
|
||||||
isDid,
|
isDid,
|
||||||
@@ -415,6 +416,7 @@ export default class ContactsView extends Vue {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// handle a contact sent via URL
|
// handle a contact sent via URL
|
||||||
|
// @deprecated: use /contact-import/:jwt with a JWT that has an array of contacts
|
||||||
const importedContactJwt = (this.$route as RouteLocationNormalizedLoaded)
|
const importedContactJwt = (this.$route as RouteLocationNormalizedLoaded)
|
||||||
.query["contactJwt"] as string;
|
.query["contactJwt"] as string;
|
||||||
if (importedContactJwt) {
|
if (importedContactJwt) {
|
||||||
@@ -1247,7 +1249,7 @@ export default class ContactsView extends Vue {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private copySelectedContacts() {
|
private async copySelectedContacts() {
|
||||||
if (this.contactsSelected.length === 0) {
|
if (this.contactsSelected.length === 0) {
|
||||||
this.danger("You must select contacts to copy.");
|
this.danger("You must select contacts to copy.");
|
||||||
return;
|
return;
|
||||||
@@ -1255,18 +1257,23 @@ export default class ContactsView extends Vue {
|
|||||||
const selectedContacts = this.contacts.filter((c) =>
|
const selectedContacts = this.contacts.filter((c) =>
|
||||||
this.contactsSelected.includes(c.did),
|
this.contactsSelected.includes(c.did),
|
||||||
);
|
);
|
||||||
const message =
|
console.log(
|
||||||
"To add contacts, paste this into the box on the 'Contacts' screen.\n\n" +
|
"Array of selected contacts:",
|
||||||
JSON.stringify(selectedContacts);
|
JSON.stringify(selectedContacts),
|
||||||
|
);
|
||||||
|
const contactsJwt = await createEndorserJwtForDid(this.activeDid, {
|
||||||
|
contacts: selectedContacts,
|
||||||
|
});
|
||||||
|
const contactsJwtUrl = APP_SERVER + "/contact-import/" + contactsJwt;
|
||||||
useClipboard()
|
useClipboard()
|
||||||
.copy(message)
|
.copy(contactsJwtUrl)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "info",
|
type: "info",
|
||||||
title: "Copied",
|
title: "Copied",
|
||||||
text: "Those contacts were copied to the clipboard. Have them paste it in the box on their 'Contacts' screen.",
|
text: "The link for those contacts is now in the clipboard.",
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -114,7 +114,7 @@
|
|||||||
{{
|
{{
|
||||||
giverDid
|
giverDid
|
||||||
? "This was provided by " + giverName + "."
|
? "This was provided by " + giverName + "."
|
||||||
: "No individual gave."
|
: "No named individual gave."
|
||||||
}}
|
}}
|
||||||
</label>
|
</label>
|
||||||
<fa
|
<fa
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ export default class IdentitySwitcherView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "warning",
|
type: "warning",
|
||||||
title: "Cannot Delete",
|
title: "Cannot Delete",
|
||||||
text: "You cannot delete the active identity.",
|
text: "You cannot delete the active identity. Set to another identity or 'no identity' first.",
|
||||||
},
|
},
|
||||||
3000,
|
3000,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user