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>
<div class="text-center mt-8"> <div class="text-center mt-8">
<div class> <div>
<fa <fa
icon="camera" 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" 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 { export interface ContactMethod {
label: string; label: string;
type: string; // eg. "EMAIL", "SMS", "WHATSAPP" type: string; // eg. "EMAIL", "SMS", "WHATSAPP", maybe someday "GOOGLE-CONTACT-API"
value: string; value: string;
} }
export interface Contact { export interface Contact {
//
// When adding a property, consider whether it should be added when exporting & sharing contacts.
did: string; did: string;
contactMethods?: Array<ContactMethod>; contactMethods?: Array<ContactMethod>;
name?: string; 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 { HDNode } from "@ethersproject/hdnode";
import { import {
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
createEndorserJwtForDid, createEndorserJwtForDid,
ENDORSER_JWT_URL_LOCATION, CONTACT_URL_PATH_ENDORSER_CH_OLD,
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup"; import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
import { decodeEndorserJwt } from "@/libs/crypto/vc"; 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) } } { 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) => { export const getContactPayloadFromJwtUrl = (jwtUrlText: string) => {
let jwtText = jwtUrlText; let jwtText = jwtUrlText;
const endorserContextLoc = jwtText.indexOf(ENDORSER_JWT_URL_LOCATION); const appImportConfirmUrlLoc = jwtText.indexOf(
if (endorserContextLoc > -1) { CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
);
if (appImportConfirmUrlLoc > -1) {
jwtText = jwtText.substring( 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 { LRUCache } from "lru-cache";
import * as R from "ramda"; 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 { Contact } from "@/db/tables/contacts";
import { accessToken, deriveAddress, nextDerivationPath } from "@/libs/crypto"; import { accessToken, deriveAddress, nextDerivationPath } from "@/libs/crypto";
import { logConsoleAndDb, NonsensitiveDexie } from "@/db/index"; import { logConsoleAndDb, NonsensitiveDexie } from "@/db/index";
@ -22,10 +26,14 @@ export const SCHEMA_ORG_CONTEXT = "https://schema.org";
export const SERVICE_ID = "endorser.ch"; export const SERVICE_ID = "endorser.ch";
// the header line for contacts exported via Endorser Mobile // the header line for contacts exported via Endorser Mobile
export const CONTACT_CSV_HEADER = "name,did,pubKeyBase64,seesMe,registered"; export const CONTACT_CSV_HEADER = "name,did,pubKeyBase64,seesMe,registered";
// the prefix for the contact URL // the suffix for the contact URL in this app where they are confirmed before import
export const CONTACT_URL_PREFIX = "https://endorser.ch"; export const CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI = "/contact-import/";
// the suffix for the contact URL // the suffix for the contact URL in this app where a single one gets imported automatically
export const ENDORSER_JWT_URL_LOCATION = "/contact?jwt="; 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 // the prefix for handle IDs, the permanent ID for claims on Endorser
export const ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/"; export const ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/";
@ -692,7 +700,6 @@ export async function getNewOffersToUser(
url += "&beforeId=" + beforeOfferJwtId; url += "&beforeId=" + beforeOfferJwtId;
} }
const headers = await getHeaders(activeDid); const headers = await getHeaders(activeDid);
console.log("Using headers: ", headers);
const response = await axios.get(url, { headers }); const response = await axios.get(url, { headers });
return response.data; return response.data;
} }
@ -1090,7 +1097,7 @@ export async function createAndSubmitClaim(
} }
} }
export async function generateEndorserJwtForAccount( export async function generateEndorserJwtUrlForAccount(
account: Account, account: Account,
isRegistered?: boolean, isRegistered?: boolean,
name?: string, name?: string,
@ -1130,7 +1137,7 @@ export async function generateEndorserJwtForAccount(
const vcJwt = await createEndorserJwtForDid(account.did, contactInfo); 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; return viewPrefix + vcJwt;
} }

1
src/views/ClaimCertificateView.vue

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

4
src/views/ContactEditView.vue

@ -17,7 +17,7 @@
</div> </div>
<!-- Contact Name --> <!-- Contact Name -->
<div class="mt-4 flex"> <div class="mt-4 flex" data-testId="contactName">
<label <label
for="contactName" for="contactName"
class="block text-sm font-medium text-gray-700 mt-2" class="block text-sm font-medium text-gray-700 mt-2"
@ -227,7 +227,7 @@ export default class ContactEditView extends Vue {
this.$notify({ this.$notify({
group: "alert", group: "alert",
type: "success", type: "success",
title: "Notes Saved", title: "Contact Saved",
text: "The contact info has been updated successfully.", text: "The contact info has been updated successfully.",
}); });
(this.$router as Router).push({ (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.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || ""; 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[ const importedContacts = (this.$route as Router).query[
"contacts" "contacts"
] as string; ] as string;
@ -146,7 +146,7 @@ export default class ContactImportView extends Vue {
await this.setContactsSelected(JSON.parse(importedContacts)); 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( const jwt = window.location.pathname.match(
/\/contact-import\/(ey.+)$/, /\/contact-import\/(ey.+)$/,
)?.[1]; )?.[1];

16
src/views/ContactQRScanShowView.vue

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

39
src/views/ContactsView.vue

@ -344,7 +344,6 @@ import { getContactPayloadFromJwtUrl } from "@/libs/crypto";
import { decodeEndorserJwt } from "@/libs/crypto/vc"; import { decodeEndorserJwt } from "@/libs/crypto/vc";
import { import {
CONTACT_CSV_HEADER, CONTACT_CSV_HEADER,
CONTACT_URL_PREFIX,
createEndorserJwtForDid, createEndorserJwtForDid,
errorStringForLog, errorStringForLog,
GiveSummaryRecord, GiveSummaryRecord,
@ -354,6 +353,9 @@ import {
setVisibilityUtil, setVisibilityUtil,
UserInfo, UserInfo,
VerifiableCredential, VerifiableCredential,
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
CONTACT_URL_PATH_ENDORSER_CH_OLD,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { generateSaveAndActivateIdentity } from "@/libs/util"; import { generateSaveAndActivateIdentity } from "@/libs/util";
@ -426,7 +428,9 @@ 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 //
// 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) const importedContactJwt = (this.$route as RouteLocationNormalizedLoaded)
.query["contactJwt"] as string; .query["contactJwt"] as string;
if (importedContactJwt) { if (importedContactJwt) {
@ -709,7 +713,11 @@ export default class ContactsView extends Vue {
return; 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); await this.addContactFromScan(contactInput);
return; return;
} }
@ -872,6 +880,13 @@ export default class ContactsView extends Vue {
); );
return; return;
} else { } 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({ return this.addContact({
did: payload.iss, did: payload.iss,
name: payload.own.name, name: payload.own.name,
@ -1278,9 +1293,25 @@ export default class ContactsView extends Vue {
this.danger("You must select contacts to copy."); this.danger("You must select contacts to copy.");
return; return;
} }
const selectedContacts = this.contacts.filter((c) => const selectedContactsFull = this.contacts.filter((c) =>
this.contactsSelected.includes(c.did), 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( // console.log(
// "Array of selected contacts:", // "Array of selected contacts:",
// JSON.stringify(selectedContacts), // JSON.stringify(selectedContacts),

19
src/views/DIDView.vue

@ -278,8 +278,23 @@ export default class DIDView extends Vue {
this.apiServer = settings.apiServer || ""; this.apiServer = settings.apiServer || "";
const pathParam = window.location.pathname.substring("/did/".length); const pathParam = window.location.pathname.substring("/did/".length);
if (pathParam) { let showDid = pathParam;
this.viewingDid = decodeURIComponent(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); this.contactFromDid = await db.contacts.get(this.viewingDid);
if (this.contactFromDid) { if (this.contactFromDid) {
this.contactYaml = yaml.dump(this.contactFromDid); this.contactYaml = yaml.dump(this.contactFromDid);

8
src/views/HelpNotificationsView.vue

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

1
src/views/HomeView.vue

@ -557,7 +557,6 @@ export default class HomeView extends Vue {
this.activeDid, this.activeDid,
this.lastAckedOfferToUserJwtId, this.lastAckedOfferToUserJwtId,
); );
console.log("offersToUserData", offersToUserData);
this.numNewOffersToUser = offersToUserData.data.length; this.numNewOffersToUser = offersToUserData.data.length;
this.newOffersToUserHitLimit = offersToUserData.hitLimit; 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 TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app"; import { NotificationIface } from "@/constants/app";
import { db, retrieveSettingsForActiveAccount } from "@/db/index"; import { db, retrieveSettingsForActiveAccount } from "@/db/index";
import { generateEndorserJwtForAccount } from "@/libs/endorserServer"; import { generateEndorserJwtUrlForAccount } from "@/libs/endorserServer";
import { retrieveAccountMetadata } from "@/libs/util"; import { retrieveAccountMetadata } from "@/libs/util";
@Component({ @Component({
@ -70,7 +70,7 @@ export default class ShareMyContactInfoView extends Vue {
const numContacts = await db.contacts.count(); const numContacts = await db.contacts.count();
if (account) { if (account) {
const message = await generateEndorserJwtForAccount( const message = await generateEndorserJwtUrlForAccount(
account, account,
isRegistered, isRegistered,
givenName, givenName,

Loading…
Cancel
Save