Browse Source

switch so personal contact JWT is link to this server (not endorser.ch), make empty-did URL show user's info

master
Trent Larson 2 weeks ago
parent
commit
702e44872f
  1. 2
      src/components/ImageMethodDialog.vue
  2. 5
      src/db/tables/contacts.ts
  3. 31
      src/libs/crypto/index.ts
  4. 23
      src/libs/endorserServer.ts
  5. 1
      src/views/ClaimCertificateView.vue
  6. 4
      src/views/ContactEditView.vue
  7. 4
      src/views/ContactImportView.vue
  8. 16
      src/views/ContactQRScanShowView.vue
  9. 39
      src/views/ContactsView.vue
  10. 19
      src/views/DIDView.vue
  11. 8
      src/views/HelpNotificationsView.vue
  12. 1
      src/views/HomeView.vue
  13. 4
      src/views/ShareMyContactInfoView.vue

2
src/components/ImageMethodDialog.vue

@ -18,7 +18,7 @@
<div>
<div class="text-center mt-8">
<div class>
<div>
<fa
icon="camera"
class="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-2 rounded-md"

5
src/db/tables/contacts.ts

@ -1,10 +1,13 @@
export interface ContactMethod {
label: string;
type: string; // eg. "EMAIL", "SMS", "WHATSAPP"
type: string; // eg. "EMAIL", "SMS", "WHATSAPP", maybe someday "GOOGLE-CONTACT-API"
value: string;
}
export interface Contact {
//
// When adding a property, consider whether it should be added when exporting & sharing contacts.
did: string;
contactMethods?: Array<ContactMethod>;
name?: string;

31
src/libs/crypto/index.ts

@ -5,8 +5,10 @@ import { wordlist } from "ethereum-cryptography/bip39/wordlists/english";
import { HDNode } from "@ethersproject/hdnode";
import {
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
createEndorserJwtForDid,
ENDORSER_JWT_URL_LOCATION,
CONTACT_URL_PATH_ENDORSER_CH_OLD,
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
} from "@/libs/endorserServer";
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
import { decodeEndorserJwt } from "@/libs/crypto/vc";
@ -101,17 +103,34 @@ export const accessToken = async (did?: string) => {
};
/**
@return results of uportJwtPayload:
@return payload of JWT pulled out of the URL and decoded:
{ iat: number, iss: string (DID), own: { name, publicEncKey (base64-encoded key) } }
Note that similar code is also contained in time-safari
Result may be a single contact or it may be { contacts: [ contact, ... ] }
*/
export const getContactPayloadFromJwtUrl = (jwtUrlText: string) => {
let jwtText = jwtUrlText;
const endorserContextLoc = jwtText.indexOf(ENDORSER_JWT_URL_LOCATION);
if (endorserContextLoc > -1) {
const appImportConfirmUrlLoc = jwtText.indexOf(
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
);
if (appImportConfirmUrlLoc > -1) {
jwtText = jwtText.substring(
endorserContextLoc + ENDORSER_JWT_URL_LOCATION.length,
appImportConfirmUrlLoc +
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI.length,
);
}
const appImportOneUrlLoc = jwtText.indexOf(
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
);
if (appImportOneUrlLoc > -1) {
jwtText = jwtText.substring(
appImportOneUrlLoc + CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI.length,
);
}
const endorserUrlPathLoc = jwtText.indexOf(CONTACT_URL_PATH_ENDORSER_CH_OLD);
if (endorserUrlPathLoc > -1) {
jwtText = jwtText.substring(
endorserUrlPathLoc + CONTACT_URL_PATH_ENDORSER_CH_OLD.length,
);
}

23
src/libs/endorserServer.ts

@ -4,7 +4,11 @@ import { sha256 } from "ethereum-cryptography/sha256";
import { LRUCache } from "lru-cache";
import * as R from "ramda";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import {
APP_SERVER,
DEFAULT_IMAGE_API_SERVER,
NotificationIface,
} from "@/constants/app";
import { Contact } from "@/db/tables/contacts";
import { accessToken, deriveAddress, nextDerivationPath } from "@/libs/crypto";
import { logConsoleAndDb, NonsensitiveDexie } from "@/db/index";
@ -22,10 +26,14 @@ export const SCHEMA_ORG_CONTEXT = "https://schema.org";
export const SERVICE_ID = "endorser.ch";
// the header line for contacts exported via Endorser Mobile
export const CONTACT_CSV_HEADER = "name,did,pubKeyBase64,seesMe,registered";
// the prefix for the contact URL
export const CONTACT_URL_PREFIX = "https://endorser.ch";
// the suffix for the contact URL
export const ENDORSER_JWT_URL_LOCATION = "/contact?jwt=";
// the suffix for the contact URL in this app where they are confirmed before import
export const CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI = "/contact-import/";
// the suffix for the contact URL in this app where a single one gets imported automatically
export const CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI = "/contacts?contactJwt=";
// the suffix for the old contact URL -- deprecated Jan 2025, though "endorser.ch/contact?jwt=" shows data on endorser.ch server
export const CONTACT_URL_PATH_ENDORSER_CH_OLD = "/contact?jwt=";
// unused now that we match on the URL path; just note that it was used for a while to create URLs that showed at endorser.ch
//export const CONTACT_URL_PREFIX_ENDORSER_CH_OLD = "https://endorser.ch";
// the prefix for handle IDs, the permanent ID for claims on Endorser
export const ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/";
@ -692,7 +700,6 @@ export async function getNewOffersToUser(
url += "&beforeId=" + beforeOfferJwtId;
}
const headers = await getHeaders(activeDid);
console.log("Using headers: ", headers);
const response = await axios.get(url, { headers });
return response.data;
}
@ -1090,7 +1097,7 @@ export async function createAndSubmitClaim(
}
}
export async function generateEndorserJwtForAccount(
export async function generateEndorserJwtUrlForAccount(
account: Account,
isRegistered?: boolean,
name?: string,
@ -1130,7 +1137,7 @@ export async function generateEndorserJwtForAccount(
const vcJwt = await createEndorserJwtForDid(account.did, contactInfo);
const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION;
const viewPrefix = APP_SERVER + CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI;
return viewPrefix + vcJwt;
}

1
src/views/ClaimCertificateView.vue

@ -181,7 +181,6 @@ export default class ClaimCertificateView extends Vue {
}
// Draw claim issuer
console.log("claimData.issuer", claimData.issuer);
if (
claimData.issuer == null ||
serverUtil.isHiddenDid(claimData.issuer) ||

4
src/views/ContactEditView.vue

@ -17,7 +17,7 @@
</div>
<!-- Contact Name -->
<div class="mt-4 flex">
<div class="mt-4 flex" data-testId="contactName">
<label
for="contactName"
class="block text-sm font-medium text-gray-700 mt-2"
@ -227,7 +227,7 @@ export default class ContactEditView extends Vue {
this.$notify({
group: "alert",
type: "success",
title: "Notes Saved",
title: "Contact Saved",
text: "The contact info has been updated successfully.",
});
(this.$router as Router).push({

4
src/views/ContactImportView.vue

@ -138,7 +138,7 @@ export default class ContactImportView extends Vue {
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
// Retrieve the imported contacts from the query parameter
// look for any imported contacts from the query parameter
const importedContacts = (this.$route as Router).query[
"contacts"
] as string;
@ -146,7 +146,7 @@ export default class ContactImportView extends Vue {
await this.setContactsSelected(JSON.parse(importedContacts));
}
// match everything after /contact-import/ in the window.location.pathname
// look for a JWT after /contact-import/ in the window.location.pathname
const jwt = window.location.pathname.match(
/\/contact-import\/(ey.+)$/,
)?.[1];

16
src/views/ContactQRScanShowView.vue

@ -93,17 +93,18 @@ import { AxiosError } from "axios";
import QRCodeVue3 from "qr-code-generator-vue3";
import { Component, Vue } from "vue-facing-decorator";
import { QrcodeStream } from "vue-qrcode-reader";
import { Router } from "vue-router";
import { useClipboard } from "@vueuse/core";
import QuickNav from "@/components/QuickNav.vue";
import UserNameDialog from "@/components/UserNameDialog.vue";
import { NotificationIface } from "@/constants/app";
import { APP_SERVER, NotificationIface } from "@/constants/app";
import { db, retrieveSettingsForActiveAccount } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { getContactPayloadFromJwtUrl } from "@/libs/crypto";
import {
generateEndorserJwtForAccount,
generateEndorserJwtUrlForAccount,
isDid,
register,
setVisibilityUtil,
@ -146,7 +147,7 @@ export default class ContactQRScanShow extends Vue {
(settings.firstName || "") +
(settings.lastName ? ` ${settings.lastName}` : ""); // lastName is deprecated, pre v 0.1.3
this.qrValue = await generateEndorserJwtForAccount(
this.qrValue = await generateEndorserJwtUrlForAccount(
account,
!!settings.isRegistered,
name,
@ -192,6 +193,13 @@ export default class ContactQRScanShow extends Vue {
);
return;
}
if (Array.isArray(payload.contacts)) {
// reroute to the ContactsImport
(this.$router as Router).push({
path: '/contacts-import/' + url.substring(url.lastIndexOf('/') + 1),
});
return;
}
newContact = {
did: payload.iss as string,
name: payload.own.name,
@ -405,7 +413,7 @@ export default class ContactQRScanShow extends Vue {
useClipboard()
.copy(this.qrValue)
.then(() => {
console.log("Contact URL:", this.qrValue);
// console.log("Contact URL:", this.qrValue);
this.$notify(
{
group: "alert",

39
src/views/ContactsView.vue

@ -344,7 +344,6 @@ import { getContactPayloadFromJwtUrl } from "@/libs/crypto";
import { decodeEndorserJwt } from "@/libs/crypto/vc";
import {
CONTACT_CSV_HEADER,
CONTACT_URL_PREFIX,
createEndorserJwtForDid,
errorStringForLog,
GiveSummaryRecord,
@ -354,6 +353,9 @@ import {
setVisibilityUtil,
UserInfo,
VerifiableCredential,
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
CONTACT_URL_PATH_ENDORSER_CH_OLD,
} from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
import { generateSaveAndActivateIdentity } from "@/libs/util";
@ -426,7 +428,9 @@ export default class ContactsView extends Vue {
);
// handle a contact sent via URL
// @deprecated: use /contact-import/:jwt with a JWT that has an array of contacts
//
// Prefer use of /contact-import/:jwt with a JWT that has an array of contacts
// unless you want them to import a single contact without confirmation.
const importedContactJwt = (this.$route as RouteLocationNormalizedLoaded)
.query["contactJwt"] as string;
if (importedContactJwt) {
@ -709,7 +713,11 @@ export default class ContactsView extends Vue {
return;
}
if (contactInput.startsWith(CONTACT_URL_PREFIX)) {
if (
contactInput.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI) ||
contactInput.includes(CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI) ||
contactInput.includes(CONTACT_URL_PATH_ENDORSER_CH_OLD)
) {
await this.addContactFromScan(contactInput);
return;
}
@ -872,6 +880,13 @@ export default class ContactsView extends Vue {
);
return;
} else {
if (Array.isArray(payload.contacts)) {
// reroute to the ContactsImport
(this.$router as Router).push({
path: '/contacts-import/' + url.substring(url.lastIndexOf('/') + 1),
});
return;
}
return this.addContact({
did: payload.iss,
name: payload.own.name,
@ -1278,9 +1293,25 @@ export default class ContactsView extends Vue {
this.danger("You must select contacts to copy.");
return;
}
const selectedContacts = this.contacts.filter((c) =>
const selectedContactsFull = this.contacts.filter((c) =>
this.contactsSelected.includes(c.did),
);
const selectedContacts: Array<Contact> = selectedContactsFull.map((c) => {
const contact: Contact = {
did: c.did,
name: c.name,
};
if (c.nextPubKeyHashB64) {
contact.nextPubKeyHashB64 = c.nextPubKeyHashB64;
}
if (c.profileImageUrl) {
contact.profileImageUrl = c.profileImageUrl;
}
if (c.publicKeyBase64) {
contact.publicKeyBase64 = c.publicKeyBase64;
}
return contact;
});
// console.log(
// "Array of selected contacts:",
// JSON.stringify(selectedContacts),

19
src/views/DIDView.vue

@ -278,8 +278,23 @@ export default class DIDView extends Vue {
this.apiServer = settings.apiServer || "";
const pathParam = window.location.pathname.substring("/did/".length);
if (pathParam) {
this.viewingDid = decodeURIComponent(pathParam);
let showDid = pathParam;
if (!showDid) {
showDid = this.activeDid;
if (showDid) {
this.$notify(
{
group: "alert",
type: "toast",
title: "Your Info",
text: "No user was specified so showing your info.",
},
3000,
);
}
}
if (showDid) {
this.viewingDid = decodeURIComponent(showDid);
this.contactFromDid = await db.contacts.get(this.viewingDid);
if (this.contactFromDid) {
this.contactYaml = yaml.dump(this.contactFromDid);

8
src/views/HelpNotificationsView.vue

@ -331,10 +331,10 @@ export default class HelpNotificationsView extends Vue {
}
alertWebPushSubscription() {
console.log(
"Web push subscription:",
JSON.parse(JSON.stringify(this.subscriptionJSON)), // gives more info than plain console logging
);
// console.log(
// "Web push subscription:",
// JSON.parse(JSON.stringify(this.subscriptionJSON)), // gives more info than plain console logging
// );
alert(JSON.stringify(this.subscriptionJSON));
}

1
src/views/HomeView.vue

@ -557,7 +557,6 @@ export default class HomeView extends Vue {
this.activeDid,
this.lastAckedOfferToUserJwtId,
);
console.log("offersToUserData", offersToUserData);
this.numNewOffersToUser = offersToUserData.data.length;
this.newOffersToUserHitLimit = offersToUserData.hitLimit;
}

4
src/views/ShareMyContactInfoView.vue

@ -49,7 +49,7 @@ import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app";
import { db, retrieveSettingsForActiveAccount } from "@/db/index";
import { generateEndorserJwtForAccount } from "@/libs/endorserServer";
import { generateEndorserJwtUrlForAccount } from "@/libs/endorserServer";
import { retrieveAccountMetadata } from "@/libs/util";
@Component({
@ -70,7 +70,7 @@ export default class ShareMyContactInfoView extends Vue {
const numContacts = await db.contacts.count();
if (account) {
const message = await generateEndorserJwtForAccount(
const message = await generateEndorserJwtUrlForAccount(
account,
isRegistered,
givenName,

Loading…
Cancel
Save