Browse Source

Merge pull request 'Refactor JWT-creation calls through single function' (#119) from passkey-all into master

Reviewed-on: https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/pulls/119
master
trentlarson 4 months ago
parent
commit
bc00eac143
  1. 3
      src/components/GiftedDialog.vue
  2. 3
      src/components/OfferDialog.vue
  3. 4
      src/components/PhotoDialog.vue
  4. 17
      src/components/World/components/objects/landmarks.js
  5. 32
      src/libs/crypto/index.ts
  6. 4
      src/libs/crypto/passkeyHelpers.ts
  7. 34
      src/libs/didPeer.ts
  8. 144
      src/libs/endorserServer.ts
  9. 46
      src/libs/util.ts
  10. 3
      src/test/index.ts
  11. 132
      src/views/AccountViewView.vue
  12. 30
      src/views/ClaimAddRawView.vue
  13. 58
      src/views/ClaimView.vue
  14. 48
      src/views/ConfirmGiftView.vue
  15. 74
      src/views/ContactAmountsView.vue
  16. 31
      src/views/ContactGiftingView.vue
  17. 44
      src/views/ContactQRScanShowView.vue
  18. 54
      src/views/ContactsView.vue
  19. 28
      src/views/DIDView.vue
  20. 31
      src/views/DiscoverView.vue
  21. 15
      src/views/GiftedDetails.vue
  22. 118
      src/views/HomeView.vue
  23. 80
      src/views/NewEditProjectView.vue
  24. 76
      src/views/ProjectViewView.vue
  25. 22
      src/views/ProjectsView.vue
  26. 5
      src/views/QuickActionBvcBeginView.vue
  27. 25
      src/views/QuickActionBvcEndView.vue
  28. 4
      src/views/SharedPhotoView.vue
  29. 73
      src/views/StartView.vue
  30. 26
      src/views/TestView.vue

3
src/components/GiftedDialog.vue

@ -287,11 +287,10 @@ export default class GiftedDialog extends Vue {
unitCode: string = "HUR", unitCode: string = "HUR",
) { ) {
try { try {
const identity = await libsUtil.getIdentity(this.activeDid);
const result = await createAndSubmitGive( const result = await createAndSubmitGive(
this.axios, this.axios,
this.apiServer, this.apiServer,
identity, this.activeDid,
giverDid, giverDid,
this.receiver?.did as string, this.receiver?.did as string,
description, description,

3
src/components/OfferDialog.vue

@ -223,11 +223,10 @@ export default class OfferDialog extends Vue {
} }
try { try {
const identity = await libsUtil.getIdentity(this.activeDid);
const result = await createAndSubmitOffer( const result = await createAndSubmitOffer(
this.axios, this.axios,
this.apiServer, this.apiServer,
identity, this.activeDid,
description, description,
amount, amount,
unitCode, unitCode,

4
src/components/PhotoDialog.vue

@ -126,7 +126,6 @@ import { Component, Vue } from "vue-facing-decorator";
import VuePictureCropper, { cropper } from "vue-picture-cropper"; import VuePictureCropper, { cropper } from "vue-picture-cropper";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app"; import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import { getIdentity } from "@/libs/util";
import { db } from "@/db/index"; import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
@ -348,8 +347,7 @@ export default class PhotoDialog extends Vue {
this.blob = (await cropper?.getBlob()) || undefined; this.blob = (await cropper?.getBlob()) || undefined;
} }
const identifier = await getIdentity(this.activeDid); const token = await accessToken(this.activeDid);
const token = await accessToken(identifier);
const headers = { const headers = {
Authorization: "Bearer " + token, Authorization: "Bearer " + token,
}; };

17
src/components/World/components/objects/landmarks.js

@ -1,12 +1,11 @@
import axios from "axios"; import axios from "axios";
import * as R from "ramda";
import * as THREE from "three"; import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader"; import { GLTFLoader } from "three/addons/loaders/GLTFLoader";
import * as SkeletonUtils from "three/addons/utils/SkeletonUtils"; import * as SkeletonUtils from "three/addons/utils/SkeletonUtils";
import * as TWEEN from "@tweenjs/tween.js"; import * as TWEEN from "@tweenjs/tween.js";
import { accountsDB, db } from "@/db"; import { db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { getHeaders } from "@/libs/endorserServer";
const ANIMATION_DURATION_SECS = 10; const ANIMATION_DURATION_SECS = 10;
const ENDORSER_ENTITY_PREFIX = "https://endorser.ch/entity/"; const ENDORSER_ENTITY_PREFIX = "https://endorser.ch/entity/";
@ -19,17 +18,7 @@ export async function loadLandmarks(vue, world, scene, loop) {
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || ""; const activeDid = settings?.activeDid || "";
const apiServer = settings?.apiServer; const apiServer = settings?.apiServer;
await accountsDB.open(); const headers = await getHeaders(activeDid);
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
const headers = {
"Content-Type": "application/json",
};
const identity = JSON.parse(account?.identity || "null");
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
const url = apiServer + "/api/v2/report/claims?claimType=GiveAction"; const url = apiServer + "/api/v2/report/claims?claimType=GiveAction";
const resp = await axios.get(url, { headers: headers }); const resp = await axios.get(url, { headers: headers });

32
src/libs/crypto/index.ts

@ -6,7 +6,10 @@ import { HDNode } from "@ethersproject/hdnode";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import * as u8a from "uint8arrays"; import * as u8a from "uint8arrays";
import { ENDORSER_JWT_URL_LOCATION } from "@/libs/endorserServer"; import {
createEndorserJwt,
ENDORSER_JWT_URL_LOCATION,
} from "@/libs/endorserServer";
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup"; import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'"; export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'";
@ -85,32 +88,17 @@ export const generateSeed = (): string => {
/** /**
* Retreive an access token * Retreive an access token
* *
* @param {IIdentifier} identifier
* @return {*} * @return {*}
*/ */
export const accessToken = async (identifier: IIdentifier) => { export const accessToken = async (did?: string) => {
const did: string = identifier.did; if (did) {
const privateKeyHex: string = identifier.keys[0].privateKeyHex as string;
const signer = SimpleSigner(privateKeyHex);
const nowEpoch = Math.floor(Date.now() / 1000); const nowEpoch = Math.floor(Date.now() / 1000);
const endEpoch = nowEpoch + 60; // add one minute const endEpoch = nowEpoch + 60; // add one minute
const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did }; const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
const alg = undefined; // defaults to 'ES256K', more standardized but harder to verify vs ES256K-R return createEndorserJwt(did, tokenPayload);
const jwt: string = await didJwt.createJWT(tokenPayload, { } else {
alg, return "";
issuer: did, }
signer,
});
return jwt;
};
export const sign = async (privateKeyHex: string) => {
const signer = SimpleSigner(privateKeyHex);
return signer;
}; };
/** /**

4
src/libs/crypto/passkeyHelpers.ts

@ -88,7 +88,7 @@ export function getWebCrypto(): Promise<{ subtle: SubtleCrypto }> {
); );
return toResolve; return toResolve;
} }
export class MissingWebCrypto extends Error { class MissingWebCrypto extends Error {
constructor() { constructor() {
const message = "An instance of the Crypto API could not be located"; const message = "An instance of the Crypto API could not be located";
super(message); super(message);
@ -96,7 +96,7 @@ export class MissingWebCrypto extends Error {
} }
} }
// Make it possible to stub return values during testing // Make it possible to stub return values during testing
export const _getWebCryptoInternals = { const _getWebCryptoInternals = {
stubThisGlobalThisCrypto: () => globalThis.crypto, stubThisGlobalThisCrypto: () => globalThis.crypto,
// Make it possible to reset the `webCrypto` at the top of the file // Make it possible to reset the `webCrypto` at the top of the file
setCachedCrypto: (newCrypto: { subtle: SubtleCrypto }) => { setCachedCrypto: (newCrypto: { subtle: SubtleCrypto }) => {

34
src/libs/didPeer.ts

@ -20,6 +20,7 @@ import {
PublicKeyCredentialRequestOptionsJSON, PublicKeyCredentialRequestOptionsJSON,
} from "@simplewebauthn/types"; } from "@simplewebauthn/types";
import { AppString } from "@/constants/app";
import { getWebCrypto, unwrapEC2Signature } from "@/libs/crypto/passkeyHelpers"; import { getWebCrypto, unwrapEC2Signature } from "@/libs/crypto/passkeyHelpers";
const PEER_DID_PREFIX = "did:peer:"; const PEER_DID_PREFIX = "did:peer:";
@ -42,9 +43,9 @@ function arrayToBase64Url(anything: Uint8Array) {
export async function registerCredential(passkeyName?: string) { export async function registerCredential(passkeyName?: string) {
const options: PublicKeyCredentialCreationOptionsJSON = const options: PublicKeyCredentialCreationOptionsJSON =
await generateRegistrationOptions({ await generateRegistrationOptions({
rpName: "Time Safari", rpName: AppString.APP_NAME,
rpID: window.location.hostname, rpID: window.location.hostname,
userName: passkeyName || "Time Safari User", userName: passkeyName || AppString.APP_NAME + " User",
// Don't prompt users for additional information about the authenticator // Don't prompt users for additional information about the authenticator
// (Recommended for smoother UX) // (Recommended for smoother UX)
attestationType: "none", attestationType: "none",
@ -116,13 +117,17 @@ export class PeerSetup {
issuerDid: string, issuerDid: string,
payload: object, payload: object,
credIdHex: string, credIdHex: string,
expMinutes: number = 1,
) { ) {
const credentialId = arrayBufferToBase64URLString( const credentialId = arrayBufferToBase64URLString(
Buffer.from(credIdHex, "hex").buffer, Buffer.from(credIdHex, "hex").buffer,
); );
const issuedAt = Math.floor(Date.now() / 1000);
const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60; // some minutes from now
const fullPayload = { const fullPayload = {
...payload, ...payload,
iat: Math.floor(Date.now() / 1000), exp: expiryTime,
iat: issuedAt,
iss: issuerDid, iss: issuerDid,
}; };
this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload))); this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload)));
@ -158,7 +163,8 @@ export class PeerSetup {
const dataInJwt = { const dataInJwt = {
AuthenticationDataB64URL: authenticatorDataBase64Url, AuthenticationDataB64URL: authenticatorDataBase64Url,
ClientDataJSONB64URL: this.clientDataJsonBase64Url, ClientDataJSONB64URL: this.clientDataJsonBase64Url,
iat: Math.floor(Date.now() / 1000), exp: expiryTime,
iat: issuedAt,
iss: issuerDid, iss: issuerDid,
}; };
const dataInJwtString = JSON.stringify(dataInJwt); const dataInJwtString = JSON.stringify(dataInJwt);
@ -177,10 +183,14 @@ export class PeerSetup {
issuerDid: string, issuerDid: string,
payload: object, payload: object,
credIdHex: string, credIdHex: string,
expMinutes: number = 1,
) { ) {
const issuedAt = Math.floor(Date.now() / 1000);
const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60; // some minutes from now
const fullPayload = { const fullPayload = {
...payload, ...payload,
iat: Math.floor(Date.now() / 1000), exp: expiryTime,
iat: issuedAt,
iss: issuerDid, iss: issuerDid,
}; };
const dataToSignString = JSON.stringify(fullPayload); const dataToSignString = JSON.stringify(fullPayload);
@ -226,7 +236,8 @@ export class PeerSetup {
const dataInJwt = { const dataInJwt = {
AuthenticationDataB64URL: authenticatorDataBase64Url, AuthenticationDataB64URL: authenticatorDataBase64Url,
ClientDataJSONB64URL: this.clientDataJsonBase64Url, ClientDataJSONB64URL: this.clientDataJsonBase64Url,
iat: Math.floor(Date.now() / 1000), exp: expiryTime,
iat: issuedAt,
iss: issuerDid, iss: issuerDid,
}; };
const dataInJwtString = JSON.stringify(dataInJwt); const dataInJwtString = JSON.stringify(dataInJwt);
@ -307,6 +318,16 @@ export class PeerSetup {
// } // }
} }
export async function createDidPeerJwt(
did: string,
credIdHex: string,
payload: object,
): Promise<string> {
const peerSetup = new PeerSetup();
const jwt = await peerSetup.createJwtNavigator(did, payload, credIdHex);
return jwt;
}
// I'd love to use this but it doesn't verify. // I'd love to use this but it doesn't verify.
// Requires: // Requires:
// npm install @noble/curves // npm install @noble/curves
@ -379,6 +400,7 @@ export async function verifyJwtSimplewebauthn(
return verification.verified; return verification.verified;
} }
// similar code is in endorser-ch util-crypto.ts verifyPeerSignature
export async function verifyJwtWebCrypto( export async function verifyJwtWebCrypto(
credId: Base64URLString, credId: Base64URLString,
issuerDid: string, issuerDid: string,

144
src/libs/endorserServer.ts

@ -1,19 +1,14 @@
import { import { Axios, AxiosRequestConfig, AxiosResponse } from "axios";
Axios,
AxiosRequestConfig,
AxiosResponse,
RawAxiosRequestHeaders,
} from "axios";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import { LRUCache } from "lru-cache"; import { LRUCache } from "lru-cache";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { DEFAULT_IMAGE_API_SERVER } from "@/constants/app"; import { DEFAULT_IMAGE_API_SERVER } from "@/constants/app";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken, SimpleSigner } from "@/libs/crypto";
import { NonsensitiveDexie } from "@/db/index"; import { NonsensitiveDexie } from "@/db/index";
import { getIdentity } from "@/libs/util"; import { createDidPeerJwt } from "@/libs/didPeer";
import { getAccount, getIdentity } from "@/libs/util";
export const SCHEMA_ORG_CONTEXT = "https://schema.org"; export const SCHEMA_ORG_CONTEXT = "https://schema.org";
// the object in RegisterAction claims // the object in RegisterAction claims
@ -160,7 +155,7 @@ export interface OfferVerifiableCredential {
// Note that previous VCs may have additional fields. // Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id7 // https://endorser.ch/doc/html/transactions.html#id7
export interface PlanVerifiableCredential { export interface PlanVerifiableCredential {
"@context": SCHEMA_ORG_CONTEXT; "@context": "https://schema.org";
"@type": "PlanAction"; "@type": "PlanAction";
name: string; name: string;
agent?: { identifier: string }; agent?: { identifier: string };
@ -453,28 +448,30 @@ export function didInfo(
return didInfoForContact(did, activeDid, contact, allMyDids).displayName; return didInfoForContact(did, activeDid, contact, allMyDids).displayName;
} }
async function getHeaders(identity: IIdentifier | null) { export async function getHeaders(did?: string) {
const headers: RawAxiosRequestHeaders = { const headers: { "Content-Type": string; Authorization?: string } = {
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
if (identity) { if (did) {
const token = await accessToken(identity); const token = await accessToken(did);
headers["Authorization"] = "Bearer " + token; headers["Authorization"] = "Bearer " + token;
} else {
// it's often OK to request without auth; we assume necessary checks are done earlier
} }
return headers; return headers;
} }
/** /**
* @param handleId nullable, in which case "undefined" will be returned * @param handleId nullable, in which case "undefined" will be returned
* @param identity nullable, in which case no private info will be returned * @param requesterDid optional, in which case no private info will be returned
* @param axios * @param axios
* @param apiServer * @param apiServer
*/ */
export async function getPlanFromCache( export async function getPlanFromCache(
handleId: string | null, handleId: string | null,
identity: IIdentifier | null,
axios: Axios, axios: Axios,
apiServer: string, apiServer: string,
requesterDid?: string,
): Promise<PlanSummaryRecord | undefined> { ): Promise<PlanSummaryRecord | undefined> {
if (!handleId) { if (!handleId) {
return undefined; return undefined;
@ -485,7 +482,7 @@ export async function getPlanFromCache(
apiServer + apiServer +
"/api/v2/report/plans?handleId=" + "/api/v2/report/plans?handleId=" +
encodeURIComponent(handleId); encodeURIComponent(handleId);
const headers = await getHeaders(identity); const headers = await getHeaders(requesterDid);
try { try {
const resp = await axios.get(url, { headers }); const resp = await axios.get(url, { headers });
if (resp.status === 200 && resp.data?.data?.length > 0) { if (resp.status === 200 && resp.data?.data?.length > 0) {
@ -518,6 +515,9 @@ export async function setPlanInCache(
planCache.set(handleId, planSummary); planCache.set(handleId, planSummary);
} }
/**
* Construct GiveAction VC for submission to server
*/
export function constructGive( export function constructGive(
fromDid?: string | null, fromDid?: string | null,
toDid?: string, toDid?: string,
@ -572,7 +572,7 @@ export function constructGive(
export async function createAndSubmitGive( export async function createAndSubmitGive(
axios: Axios, axios: Axios,
apiServer: string, apiServer: string,
identity: IIdentifier, issuerDid: string,
fromDid?: string | null, fromDid?: string | null,
toDid?: string, toDid?: string,
description?: string, description?: string,
@ -596,7 +596,7 @@ export async function createAndSubmitGive(
); );
return createAndSubmitClaim( return createAndSubmitClaim(
vcClaim as GenericCredWrapper, vcClaim as GenericCredWrapper,
identity, issuerDid,
apiServer, apiServer,
axios, axios,
); );
@ -614,7 +614,7 @@ export async function createAndSubmitGive(
export async function createAndSubmitOffer( export async function createAndSubmitOffer(
axios: Axios, axios: Axios,
apiServer: string, apiServer: string,
identity: IIdentifier, issuerDid: string,
description?: string, description?: string,
amount?: number, amount?: number,
unitCode?: string, unitCode?: string,
@ -625,7 +625,7 @@ export async function createAndSubmitOffer(
const vcClaim: OfferVerifiableCredential = { const vcClaim: OfferVerifiableCredential = {
"@context": SCHEMA_ORG_CONTEXT, "@context": SCHEMA_ORG_CONTEXT,
"@type": "Offer", "@type": "Offer",
offeredBy: { identifier: identity.did }, offeredBy: { identifier: issuerDid },
validThrough: expirationDate || undefined, validThrough: expirationDate || undefined,
}; };
if (amount) { if (amount) {
@ -649,7 +649,7 @@ export async function createAndSubmitOffer(
} }
return createAndSubmitClaim( return createAndSubmitClaim(
vcClaim as GenericCredWrapper, vcClaim as GenericCredWrapper,
identity, issuerDid,
apiServer, apiServer,
axios, axios,
); );
@ -657,7 +657,7 @@ export async function createAndSubmitOffer(
// similar logic is found in endorser-mobile // similar logic is found in endorser-mobile
export const createAndSubmitConfirmation = async ( export const createAndSubmitConfirmation = async (
identifier: IIdentifier, issuerDid: string,
claim: GenericVerifiableCredential, claim: GenericVerifiableCredential,
lastClaimId: string, // used to set the lastClaimId lastClaimId: string, // used to set the lastClaimId
handleId: string | undefined, handleId: string | undefined,
@ -674,12 +674,12 @@ export const createAndSubmitConfirmation = async (
"@type": "AgreeAction", "@type": "AgreeAction",
object: goodClaim, object: goodClaim,
}; };
return createAndSubmitClaim(confirmationClaim, identifier, apiServer, axios); return createAndSubmitClaim(confirmationClaim, issuerDid, apiServer, axios);
}; };
export async function createAndSubmitClaim( export async function createAndSubmitClaim(
vcClaim: GenericVerifiableCredential, vcClaim: GenericVerifiableCredential,
identity: IIdentifier, issuerDid: string,
apiServer: string, apiServer: string,
axios: Axios, axios: Axios,
): Promise<CreateAndSubmitClaimResult> { ): Promise<CreateAndSubmitClaimResult> {
@ -692,34 +692,15 @@ export async function createAndSubmitClaim(
}, },
}; };
// Create a signature using private key of identity const vcJwt: string = await createEndorserJwt(issuerDid, vcPayload);
const firstKey = identity.keys[0];
const privateKeyHex = firstKey?.privateKeyHex;
if (!privateKeyHex) {
throw {
error: "No private key",
message: `Your identifier ${identity.did} is not configured correctly. Use a different identifier.`,
};
}
const signer = await SimpleSigner(privateKeyHex);
// Create a JWT for the request
const vcJwt: string = await didJwt.createJWT(vcPayload, {
issuer: identity.did,
signer,
});
// Make the xhr request payload // Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt }); const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = `${apiServer}/api/v2/claim`; const url = `${apiServer}/api/v2/claim`;
const token = await accessToken(identity);
const response = await axios.post(url, payload, { const response = await axios.post(url, payload, {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${token}`,
}, },
}); });
@ -944,18 +925,49 @@ export const bvcMeetingJoinClaim = (did: string, startTime: string) => {
}; };
}; };
export async function createEndorserJwtVcFromClaim(
issuerDid: string,
claim: object,
) {
// Make a payload for the claim
const vcPayload = {
vc: {
"@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"],
credentialSubject: claim,
},
};
return createEndorserJwt(issuerDid, vcPayload);
}
export async function createEndorserJwt(issuerDid: string, payload: object) {
const account = await getAccount(issuerDid);
if (account?.identity) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const identity = JSON.parse(account.identity!);
const privateKeyHex = identity.keys[0].privateKeyHex;
const signer = await SimpleSigner(privateKeyHex);
return didJwt.createJWT(payload, {
issuer: issuerDid,
signer: signer,
});
} else if (account?.passkeyCredIdHex) {
return createDidPeerJwt(issuerDid, account.passkeyCredIdHex, payload);
} else {
throw new Error("No identity data found to sign for DID " + issuerDid);
}
}
export async function register( export async function register(
activeDid: string, activeDid: string,
apiServer: string, apiServer: string,
axios: Axios, axios: Axios,
contact: Contact, contact: Contact,
) { ) {
const identity = await getIdentity(activeDid);
const vcClaim: RegisterVerifiableCredential = { const vcClaim: RegisterVerifiableCredential = {
"@context": SCHEMA_ORG_CONTEXT, "@context": SCHEMA_ORG_CONTEXT,
"@type": "RegisterAction", "@type": "RegisterAction",
agent: { identifier: identity.did }, agent: { identifier: activeDid },
object: SERVICE_ID, object: SERVICE_ID,
participant: { identifier: contact.did }, participant: { identifier: contact.did },
}; };
@ -968,26 +980,10 @@ export async function register(
}, },
}; };
// Create a signature using private key of identity // Create a signature using private key of identity
if (identity.keys[0].privateKeyHex == null) { const vcJwt = await createEndorserJwt(activeDid, vcPayload);
return { error: "Private key not found." };
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
const signer = await SimpleSigner(privateKeyHex);
const alg = undefined;
// Create a JWT for the request
const vcJwt: string = await didJwt.createJWT(vcPayload, {
alg: alg,
issuer: identity.did,
signer: signer,
});
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = apiServer + "/api/v2/claim"; const url = apiServer + "/api/v2/claim";
const headers = await getHeaders(identity); const resp = await axios.post(url, { jwtEncoded: vcJwt });
const resp = await axios.post(url, payload, { headers });
if (resp.data?.success?.handleId) { if (resp.data?.success?.handleId) {
return { success: true }; return { success: true };
} else if (resp.data?.success?.embeddedRecordError) { } else if (resp.data?.success?.embeddedRecordError) {
@ -1017,7 +1013,7 @@ export async function setVisibilityUtil(
const url = const url =
apiServer + "/api/report/" + (visibility ? "canSeeMe" : "cannotSeeMe"); apiServer + "/api/report/" + (visibility ? "canSeeMe" : "cannotSeeMe");
const identity = await getIdentity(activeDid); const identity = await getIdentity(activeDid);
const headers = await getHeaders(identity); const headers = await getHeaders(identity.did);
const payload = JSON.stringify({ did: contact.did }); const payload = JSON.stringify({ did: contact.did });
try { try {
@ -1046,16 +1042,16 @@ export async function setVisibilityUtil(
* *
* @param apiServer endorser server URL string * @param apiServer endorser server URL string
* @param axios Axios instance * @param axios Axios instance
* @param {IIdentifier} identity - The identity object to check rate limits for. * @param {string} issuerDid - The DID for which to check rate limits.
* @returns {Promise<AxiosResponse>} The Axios response object. * @returns {Promise<AxiosResponse>} The Axios response object.
*/ */
export async function fetchEndorserRateLimits( export async function fetchEndorserRateLimits(
apiServer: string, apiServer: string,
axios: Axios, axios: Axios,
identity: IIdentifier, issuerDid: string,
) { ) {
const url = `${apiServer}/api/report/rateLimits`; const url = `${apiServer}/api/report/rateLimits`;
const headers = await getHeaders(identity); const headers = await getHeaders(issuerDid);
return await axios.get(url, { headers } as AxiosRequestConfig); return await axios.get(url, { headers } as AxiosRequestConfig);
} }
@ -1064,15 +1060,11 @@ export async function fetchEndorserRateLimits(
* *
* @param apiServer image server URL string * @param apiServer image server URL string
* @param axios Axios instance * @param axios Axios instance
* @param {IIdentifier} identity - The identity object to check rate limits for. * @param {string} issuerDid - The DID for which to check rate limits.
* @returns {Promise<AxiosResponse>} The Axios response object. * @returns {Promise<AxiosResponse>} The Axios response object.
*/ */
export async function fetchImageRateLimits( export async function fetchImageRateLimits(axios: Axios, issuerDid: string) {
apiServer: string,
axios: Axios,
identity: IIdentifier,
) {
const url = DEFAULT_IMAGE_API_SERVER + "/image-limits"; const url = DEFAULT_IMAGE_API_SERVER + "/image-limits";
const headers = await getHeaders(identity); const headers = await getHeaders(issuerDid);
return await axios.get(url, { headers } as AxiosRequestConfig); return await axios.get(url, { headers } as AxiosRequestConfig);
} }

46
src/libs/util.ts

@ -11,9 +11,12 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto"; import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
import { GenericCredWrapper, containsHiddenDid } from "@/libs/endorserServer"; import { GenericCredWrapper, containsHiddenDid } from "@/libs/endorserServer";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
import { createPeerDid, registerCredential } from "@/libs/didPeer";
import { Buffer } from "buffer";
export const PRIVACY_MESSAGE = export const PRIVACY_MESSAGE =
"The data you send be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to those you allow."; "The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow.";
/* eslint-disable prettier/prettier */ /* eslint-disable prettier/prettier */
export const UNIT_SHORT: Record<string, string> = { export const UNIT_SHORT: Record<string, string> = {
@ -193,12 +196,19 @@ export function findAllVisibleToDids(
* *
**/ **/
export const getIdentity = async (activeDid: string): Promise<IIdentifier> => { export const getAccount = async (
activeDid: string,
): Promise<Account | undefined> => {
await accountsDB.open(); await accountsDB.open();
const account = (await accountsDB.accounts const account = (await accountsDB.accounts
.where("did") .where("did")
.equals(activeDid) .equals(activeDid)
.first()) as Account; .first()) as Account;
return account;
};
export const getIdentity = async (activeDid: string): Promise<IIdentifier> => {
const account = await getAccount(activeDid);
const identity = JSON.parse(account?.identity || "null"); const identity = JSON.parse(account?.identity || "null");
if (!identity) { if (!identity) {
@ -239,6 +249,38 @@ export const generateSaveAndActivateIdentity = async (): Promise<string> => {
return newId.did; return newId.did;
}; };
export const registerAndSavePasskey = async (
keyName: string,
): Promise<Account> => {
const cred = await registerCredential(keyName);
const publicKeyBytes = cred.publicKeyBytes;
const did = createPeerDid(publicKeyBytes as Uint8Array);
const passkeyCredIdHex = cred.credIdHex as string;
const account = {
dateCreated: new Date().toISOString(),
did,
passkeyCredIdHex,
publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"),
};
await accountsDB.open();
await accountsDB.accounts.add(account);
return account;
};
export const registerSaveAndActivatePasskey = async (
keyName: string,
): Promise<Account> => {
const account = await registerAndSavePasskey(keyName);
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: account.did,
});
return account;
};
export const sendTestThroughPushServer = async ( export const sendTestThroughPushServer = async (
subscriptionJSON: PushSubscriptionJSON, subscriptionJSON: PushSubscriptionJSON,
skipFilter: boolean, skipFilter: boolean,

3
src/test/index.ts

@ -6,6 +6,9 @@ import { SERVICE_ID } from "../libs/endorserServer";
import { deriveAddress, newIdentifier } from "../libs/crypto"; import { deriveAddress, newIdentifier } from "../libs/crypto";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
/**
* Get User #0 to sign & submit a RegisterAction for the user's activeDid.
*/
export async function testServerRegisterUser() { export async function testServerRegisterUser() {
const testUser0Mnem = const testUser0Mnem =
"seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control"; "seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control";

132
src/views/AccountViewView.vue

@ -359,6 +359,7 @@
<div class="text-slate-500 text-sm font-bold">Derivation Path</div> <div class="text-slate-500 text-sm font-bold">Derivation Path</div>
<div <div
v-if="derivationPath"
class="text-sm text-slate-500 flex justify-start items-center mb-1" class="text-sm text-slate-500 flex justify-start items-center mb-1"
> >
<code class="truncate">{{ derivationPath }}</code> <code class="truncate">{{ derivationPath }}</code>
@ -375,6 +376,12 @@
</button> </button>
<span v-show="showDerCopy">Copied</span> <span v-show="showDerCopy">Copied</span>
</div> </div>
<div
v-else
class="text-sm text-slate-500 flex justify-start items-center mb-1"
>
(none)
</div>
</div> </div>
<!-- id used by puppeteer test script --> <!-- id used by puppeteer test script -->
@ -646,13 +653,16 @@
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { Buffer } from "buffer/";
import Dexie from "dexie"; import Dexie from "dexie";
import "dexie-export-import"; import "dexie-export-import";
import { ImportProgress } from "dexie-export-import/dist/import"; import { ImportProgress } from "dexie-export-import/dist/import";
import { IIdentifier } from "@veramo/core";
import { ref } from "vue"; import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import EntityIcon from "@/components/EntityIcon.vue";
import ImageMethodDialog from "@/components/ImageMethodDialog.vue"; import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
@ -664,9 +674,9 @@ import {
NotificationIface, NotificationIface,
} from "@/constants/app"; } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core";
import { import {
ErrorResponse, ErrorResponse,
EndorserRateLimits, EndorserRateLimits,
@ -674,15 +684,7 @@ import {
fetchEndorserRateLimits, fetchEndorserRateLimits,
fetchImageRateLimits, fetchImageRateLimits,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { Buffer } from "buffer/"; import { getAccount } from "@/libs/util";
import EntityIcon from "@/components/EntityIcon.vue";
interface IAccount {
did: string;
publicKeyHex: string;
privateHex?: string;
derivationPath: string;
}
const inputImportFileNameRef = ref<Blob>(); const inputImportFileNameRef = ref<Blob>();
@ -705,6 +707,7 @@ export default class AccountViewView extends Vue {
givenName = ""; givenName = "";
hideRegisterPromptOnNewContact = false; hideRegisterPromptOnNewContact = false;
imageLimits: ImageRateLimits | null = null; imageLimits: ImageRateLimits | null = null;
imageServer = "";
isRegistered = false; isRegistered = false;
isSubscribed = false; isSubscribed = false;
limitsMessage = ""; limitsMessage = "";
@ -738,18 +741,9 @@ export default class AccountViewView extends Vue {
*/ */
async mounted() { async mounted() {
try { try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
// Initialize component state with values from the database or defaults // Initialize component state with values from the database or defaults
this.initializeState(settings); await this.initializeState();
await this.processIdentity();
// Get and process the identity
const identity = await this.getIdentity(this.activeDid);
if (identity) {
this.processIdentity(identity);
}
const registration = await navigator.serviceWorker.ready; const registration = await navigator.serviceWorker.ready;
this.subscription = await registration.pushManager.getSubscription(); this.subscription = await registration.pushManager.getSubscription();
@ -768,9 +762,12 @@ export default class AccountViewView extends Vue {
/** /**
* Initializes component state with values from the database or defaults. * Initializes component state with values from the database or defaults.
* @param {SettingsType} settings - Object containing settings from the database.
*/ */
initializeState(settings: Settings | undefined) { async initializeState() {
await db.open();
const settings: Settings | undefined =
await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = (settings?.activeDid as string) || ""; this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || ""; this.apiServer = (settings?.apiServer as string) || "";
this.apiServerInput = (settings?.apiServer as string) || ""; this.apiServerInput = (settings?.apiServer as string) || "";
@ -778,6 +775,7 @@ export default class AccountViewView extends Vue {
(settings?.firstName || "") + (settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3 (settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
this.imageServer = (settings?.imageServer as string) || "";
this.profileImageUrl = settings?.profileImageUrl as string; this.profileImageUrl = settings?.profileImageUrl as string;
this.showContactGives = !!settings?.showContactGivesInline; this.showContactGives = !!settings?.showContactGivesInline;
this.hideRegisterPromptOnNewContact = this.hideRegisterPromptOnNewContact =
@ -790,49 +788,6 @@ export default class AccountViewView extends Vue {
this.webPushServerInput = (settings?.webPushServer as string) || ""; this.webPushServerInput = (settings?.webPushServer as string) || "";
} }
public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
try {
// Open the accounts database
await accountsDB.open();
// Search for the account with the matching DID (decentralized identifier)
const account: { identity?: string } | undefined =
await accountsDB.accounts.where("did").equals(activeDid).first();
// Return parsed identity or null if not found
return JSON.parse((account?.identity as string) || "null");
} catch (error) {
console.error("Failed to find account:", error);
return null;
}
}
/**
* Asynchronously retrieves headers for HTTP requests.
*
* @param {IIdentifier} identity - The identity object for which to generate the headers.
* @returns {Promise<Record<string,string>>} A Promise that resolves to an object containing the headers.
*
* @throws Will throw an error if unable to generate an access token.
*/
public async getHeaders(
identity: IIdentifier,
): Promise<Record<string, string>> {
try {
const token = await accessToken(identity);
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
};
return headers;
} catch (error) {
console.error("Failed to get headers:", error);
return Promise.reject(error);
}
}
// call fn, copy text to the clipboard, then redo fn after 2 seconds // call fn, copy text to the clipboard, then redo fn after 2 seconds
doCopyTwoSecRedo(text: string, fn: () => void) { doCopyTwoSecRedo(text: string, fn: () => void) {
fn(); fn();
@ -872,21 +827,19 @@ export default class AccountViewView extends Vue {
/** /**
* Processes the identity and updates the component's state. * Processes the identity and updates the component's state.
* @param {IdentityType} identity - Object containing identity information.
*/ */
processIdentity(identity: IIdentifier) { async processIdentity() {
if ( const account: Account | undefined = await getAccount(this.activeDid);
identity && if (account?.identity) {
identity.keys && const identity = JSON.parse(account.identity as string) as IIdentifier;
identity.keys.length > 0 &&
identity.keys[0].meta
) {
this.publicHex = identity.keys[0].publicKeyHex; this.publicHex = identity.keys[0].publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64"); this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
this.derivationPath = identity.keys[0].meta?.derivationPath as string; this.derivationPath = identity.keys[0].meta?.derivationPath as string;
this.checkLimitsFor(identity); this.checkLimitsFor(this.activeDid);
} else { } else if (account?.publicKeyHex) {
// Handle the case where any of these are null or undefined this.publicHex = account.publicKeyHex as string;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
this.checkLimitsFor(this.activeDid);
} }
} }
@ -1238,9 +1191,8 @@ export default class AccountViewView extends Vue {
} }
async checkLimits() { async checkLimits() {
const identity = await this.getIdentity(this.activeDid); if (this.activeDid) {
if (identity) { this.checkLimitsFor(this.activeDid);
this.checkLimitsFor(identity);
} else { } else {
this.limitsMessage = this.limitsMessage =
"You have no identifier, or your data has been corrupted."; "You have no identifier, or your data has been corrupted.";
@ -1252,7 +1204,7 @@ export default class AccountViewView extends Vue {
* *
* Updates component state variables `limits`, `limitsMessage`, and `loadingLimits`. * Updates component state variables `limits`, `limitsMessage`, and `loadingLimits`.
*/ */
public async checkLimitsFor(identity: IIdentifier) { public async checkLimitsFor(did: string) {
this.loadingLimits = true; this.loadingLimits = true;
this.limitsMessage = ""; this.limitsMessage = "";
@ -1260,7 +1212,7 @@ export default class AccountViewView extends Vue {
const resp = await fetchEndorserRateLimits( const resp = await fetchEndorserRateLimits(
this.apiServer, this.apiServer,
this.axios, this.axios,
identity, did,
); );
if (resp.status === 200) { if (resp.status === 200) {
this.endorserLimits = resp.data; this.endorserLimits = resp.data;
@ -1285,11 +1237,7 @@ export default class AccountViewView extends Vue {
); );
} }
} }
const imageResp = await fetchImageRateLimits( const imageResp = await fetchImageRateLimits(this.axios, did);
this.apiServer,
this.axios,
identity,
);
if (imageResp.status === 200) { if (imageResp.status === 200) {
this.imageLimits = imageResp.data; this.imageLimits = imageResp.data;
} }
@ -1386,9 +1334,9 @@ export default class AccountViewView extends Vue {
* *
* @param {AccountType} account - The account object. * @param {AccountType} account - The account object.
*/ */
private updateActiveAccountProperties(account: IAccount) { private updateActiveAccountProperties(account: Account) {
this.activeDid = account.did; this.activeDid = account.did;
this.derivationPath = account.derivationPath; this.derivationPath = account.derivationPath || "";
this.publicHex = account.publicKeyHex; this.publicHex = account.publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64"); this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
} }
@ -1459,11 +1407,7 @@ export default class AccountViewView extends Vue {
return; return;
} }
try { try {
const identity = await this.getIdentity(this.activeDid); const token = await accessToken(this.activeDid);
if (!identity) {
throw Error("No identity found.");
}
const token = await accessToken(identity);
const response = await this.axios.delete( const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER + DEFAULT_IMAGE_API_SERVER +
"/image/" + "/image/" +

30
src/views/ClaimAddRawView.vue

@ -29,7 +29,6 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { RawAxiosRequestHeaders } from "axios";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
@ -37,7 +36,6 @@ import GiftedDialog from "@/components/GiftedDialog.vue";
import { NotificationIface } from "@/constants/app"; import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
@ -68,37 +66,11 @@ export default class ClaimAddRawView extends Vue {
} }
} }
public async getIdentity(activeDid: string): Promise<IIdentifier> {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("Cannot submit a claim without an identifier.");
}
return identity;
}
public async getHeaders(identity: IIdentifier) {
const headers: RawAxiosRequestHeaders = {
"Content-Type": "application/json",
};
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
return headers;
}
// similar code is found in ProjectViewView
async submitClaim() { async submitClaim() {
const fullClaim = JSON.parse(this.claimStr); const fullClaim = JSON.parse(this.claimStr);
const result = await serverUtil.createAndSubmitClaim( const result = await serverUtil.createAndSubmitClaim(
fullClaim, fullClaim,
await this.getIdentity(this.activeDid), this.activeDid,
this.apiServer, this.apiServer,
this.axios, this.axios,
); );

58
src/views/ClaimView.vue

@ -407,7 +407,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError, RawAxiosRequestHeaders } from "axios"; import { AxiosError } from "axios";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
@ -419,7 +419,6 @@ import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
@ -432,7 +431,6 @@ import { GiverReceiverInputInfo } from "@/libs/endorserServer";
export default class ClaimView extends Vue { export default class ClaimView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
accountIdentityStr: string = "null";
activeDid = ""; activeDid = "";
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
@ -485,15 +483,12 @@ export default class ClaimView extends Vue {
const accounts = accountsDB.accounts; const accounts = accountsDB.accounts;
const accountsArr: Array<Account> = await accounts?.toArray(); const accountsArr: Array<Account> = await accounts?.toArray();
this.allMyDids = accountsArr.map((acc) => acc.did); this.allMyDids = accountsArr.map((acc) => acc.did);
const account = accountsArr.find((acc) => acc.did === this.activeDid);
this.accountIdentityStr = (account?.identity as string) || "null";
const identity = JSON.parse(this.accountIdentityStr);
const pathParam = window.location.pathname.substring("/claim/".length); const pathParam = window.location.pathname.substring("/claim/".length);
let claimId; let claimId;
if (pathParam) { if (pathParam) {
claimId = decodeURIComponent(pathParam); claimId = decodeURIComponent(pathParam);
await this.loadClaim(claimId, identity); await this.loadClaim(claimId, this.activeDid);
} else { } else {
this.$notify( this.$notify(
{ {
@ -527,33 +522,6 @@ export default class ClaimView extends Vue {
); );
} }
public async getIdentity(activeDid: string): Promise<IIdentifier> {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identifier available.",
);
}
return identity;
}
public async getHeaders(identity: IIdentifier) {
const headers: RawAxiosRequestHeaders = {
"Content-Type": "application/json",
};
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
return headers;
}
// Isn't there a better way to make this available to the template? // Isn't there a better way to make this available to the template?
didInfo(did: string) { didInfo(did: string) {
return serverUtil.didInfo( return serverUtil.didInfo(
@ -564,12 +532,12 @@ export default class ClaimView extends Vue {
); );
} }
async loadClaim(claimId: string, identity: IIdentifier) { async loadClaim(claimId: string, userDid: string) {
const urlPath = libsUtil.isGlobalUri(claimId) const urlPath = libsUtil.isGlobalUri(claimId)
? "/api/claim/byHandle/" ? "/api/claim/byHandle/"
: "/api/claim/"; : "/api/claim/";
const url = this.apiServer + urlPath + encodeURIComponent(claimId); const url = this.apiServer + urlPath + encodeURIComponent(claimId);
const headers = await this.getHeaders(identity); const headers = await serverUtil.getHeaders(userDid);
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
@ -601,7 +569,7 @@ export default class ClaimView extends Vue {
this.apiServer + this.apiServer +
"/api/v2/report/gives?handleId=" + "/api/v2/report/gives?handleId=" +
encodeURIComponent(this.veriClaim.handleId as string); encodeURIComponent(this.veriClaim.handleId as string);
const giveHeaders = await this.getHeaders(identity); const giveHeaders = await serverUtil.getHeaders(userDid);
const giveResp = await this.axios.get(giveUrl, { const giveResp = await this.axios.get(giveUrl, {
headers: giveHeaders, headers: giveHeaders,
}); });
@ -615,7 +583,7 @@ export default class ClaimView extends Vue {
this.apiServer + this.apiServer +
"/api/v2/report/offers?handleId=" + "/api/v2/report/offers?handleId=" +
encodeURIComponent(this.veriClaim.handleId as string); encodeURIComponent(this.veriClaim.handleId as string);
const offerHeaders = await this.getHeaders(identity); const offerHeaders = await serverUtil.getHeaders(userDid);
const offerResp = await this.axios.get(offerUrl, { const offerResp = await this.axios.get(offerUrl, {
headers: offerHeaders, headers: offerHeaders,
}); });
@ -631,7 +599,7 @@ export default class ClaimView extends Vue {
this.apiServer + this.apiServer +
"/api/report/issuersWhoClaimedOrConfirmed?claimId=" + "/api/report/issuersWhoClaimedOrConfirmed?claimId=" +
encodeURIComponent(serverUtil.stripEndorserPrefix(claimId)); encodeURIComponent(serverUtil.stripEndorserPrefix(claimId));
const confirmHeaders = await this.getHeaders(identity); const confirmHeaders = await serverUtil.getHeaders(userDid);
const response = await this.axios.get(confirmUrl, { const response = await this.axios.get(confirmUrl, {
headers: confirmHeaders, headers: confirmHeaders,
}); });
@ -673,15 +641,9 @@ export default class ClaimView extends Vue {
} }
async showFullClaim(claimId: string) { async showFullClaim(claimId: string) {
await accountsDB.open();
const accounts = accountsDB.accounts;
const accountsArr: Account[] = await accounts?.toArray();
const account = accountsArr.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse((account?.identity as string) || "null");
const url = const url =
this.apiServer + "/api/claim/full/" + encodeURIComponent(claimId); this.apiServer + "/api/claim/full/" + encodeURIComponent(claimId);
const headers = await this.getHeaders(identity); const headers = await serverUtil.getHeaders(this.activeDid);
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
@ -760,7 +722,7 @@ export default class ClaimView extends Vue {
}; };
const result = await serverUtil.createAndSubmitClaim( const result = await serverUtil.createAndSubmitClaim(
confirmationClaim, confirmationClaim,
await this.getIdentity(this.activeDid), this.activeDid,
this.apiServer, this.apiServer,
this.axios, this.axios,
); );
@ -794,7 +756,7 @@ export default class ClaimView extends Vue {
}; };
this.$router.push(route).then(async () => { this.$router.push(route).then(async () => {
this.resetThisValues(); this.resetThisValues();
await this.loadClaim(claimId, JSON.parse(this.accountIdentityStr)); await this.loadClaim(claimId, this.activeDid);
}); });
} }

48
src/views/ConfirmGiftView.vue

@ -393,7 +393,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError, RawAxiosRequestHeaders } from "axios"; import { AxiosError } from "axios";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
@ -407,7 +407,6 @@ import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
import { displayAmount, GiverReceiverInputInfo } from "@/libs/endorserServer"; import { displayAmount, GiverReceiverInputInfo } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
@ -420,7 +419,6 @@ import { isGiveAction } from "@/libs/util";
export default class ClaimView extends Vue { export default class ClaimView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
accountIdentityStr: string = "null";
activeDid = ""; activeDid = "";
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
@ -471,9 +469,6 @@ export default class ClaimView extends Vue {
const accounts = accountsDB.accounts; const accounts = accountsDB.accounts;
const accountsArr: Array<Account> = await accounts?.toArray(); const accountsArr: Array<Account> = await accounts?.toArray();
this.allMyDids = accountsArr.map((acc) => acc.did); this.allMyDids = accountsArr.map((acc) => acc.did);
const account = accountsArr.find((acc) => acc.did === this.activeDid);
this.accountIdentityStr = (account?.identity as string) || "null";
const identity = JSON.parse(this.accountIdentityStr);
const pathParam = window.location.pathname.substring( const pathParam = window.location.pathname.substring(
"/confirm-gift/".length, "/confirm-gift/".length,
@ -481,7 +476,7 @@ export default class ClaimView extends Vue {
let claimId; let claimId;
if (pathParam) { if (pathParam) {
claimId = decodeURIComponent(pathParam); claimId = decodeURIComponent(pathParam);
await this.loadClaim(claimId, identity); await this.loadClaim(claimId, this.activeDid);
} else { } else {
this.$notify( this.$notify(
{ {
@ -530,33 +525,6 @@ export default class ClaimView extends Vue {
); );
} }
public async getIdentity(activeDid: string): Promise<IIdentifier> {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identifier available.",
);
}
return identity;
}
public async getHeaders(identity: IIdentifier) {
const headers: RawAxiosRequestHeaders = {
"Content-Type": "application/json",
};
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
return headers;
}
// Isn't there a better way to make this available to the template? // Isn't there a better way to make this available to the template?
didInfo(did: string | undefined) { didInfo(did: string | undefined) {
return serverUtil.didInfo( return serverUtil.didInfo(
@ -567,14 +535,14 @@ export default class ClaimView extends Vue {
); );
} }
async loadClaim(claimId: string, identity: IIdentifier) { async loadClaim(claimId: string, userDid: string) {
const urlPath = libsUtil.isGlobalUri(claimId) const urlPath = libsUtil.isGlobalUri(claimId)
? "/api/claim/byHandle/" ? "/api/claim/byHandle/"
: "/api/claim/"; : "/api/claim/";
const url = this.apiServer + urlPath + encodeURIComponent(claimId); const url = this.apiServer + urlPath + encodeURIComponent(claimId);
try { try {
const headers = await this.getHeaders(identity); const headers = await serverUtil.getHeaders(userDid);
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
// resp.data is: // resp.data is:
// - a Jwt from https://api.endorser.ch/api-docs/ // - a Jwt from https://api.endorser.ch/api-docs/
@ -614,7 +582,7 @@ export default class ClaimView extends Vue {
this.apiServer + this.apiServer +
"/api/v2/report/gives?handleId=" + "/api/v2/report/gives?handleId=" +
encodeURIComponent(this.veriClaim.handleId as string); encodeURIComponent(this.veriClaim.handleId as string);
const giveHeaders = await this.getHeaders(identity); const giveHeaders = await serverUtil.getHeaders(userDid);
const giveResp = await this.axios.get(giveUrl, { const giveResp = await this.axios.get(giveUrl, {
headers: giveHeaders, headers: giveHeaders,
}); });
@ -685,7 +653,7 @@ export default class ClaimView extends Vue {
this.apiServer + this.apiServer +
"/api/report/issuersWhoClaimedOrConfirmed?claimId=" + "/api/report/issuersWhoClaimedOrConfirmed?claimId=" +
encodeURIComponent(serverUtil.stripEndorserPrefix(claimId)); encodeURIComponent(serverUtil.stripEndorserPrefix(claimId));
const confirmHeaders = await this.getHeaders(identity); const confirmHeaders = await serverUtil.getHeaders(userDid);
const response = await this.axios.get(confirmUrl, { const response = await this.axios.get(confirmUrl, {
headers: confirmHeaders, headers: confirmHeaders,
}); });
@ -760,7 +728,7 @@ export default class ClaimView extends Vue {
}; };
const result = await serverUtil.createAndSubmitClaim( const result = await serverUtil.createAndSubmitClaim(
confirmationClaim, confirmationClaim,
await this.getIdentity(this.activeDid), this.activeDid,
this.apiServer, this.apiServer,
this.axios, this.axios,
); );
@ -794,7 +762,7 @@ export default class ClaimView extends Vue {
}; };
this.$router.push(route).then(async () => { this.$router.push(route).then(async () => {
this.resetThisValues(); this.resetThisValues();
await this.loadClaim(claimId, JSON.parse(this.accountIdentityStr)); await this.loadClaim(claimId, this.activeDid);
}); });
} }

74
src/views/ContactAmountsView.vue

@ -106,9 +106,7 @@
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import * as didJwt from "did-jwt";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
@ -116,14 +114,17 @@ import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } 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 { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { import {
AgreeVerifiableCredential, AgreeVerifiableCredential,
createEndorserJwtVcFromClaim,
displayAmount, displayAmount,
getHeaders,
GiveSummaryRecord, GiveSummaryRecord,
GiveVerifiableCredential, GiveVerifiableCredential,
SCHEMA_ORG_CONTEXT, SCHEMA_ORG_CONTEXT,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class ContactAmountssView extends Vue { export default class ContactAmountssView extends Vue {
@ -142,31 +143,6 @@ export default class ContactAmountssView extends Vue {
this.numAccounts = await accountsDB.accounts.count(); this.numAccounts = await accountsDB.accounts.count();
} }
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records with no identifier available.",
);
}
return identity;
}
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
async created() { async created() {
try { try {
await db.open(); await db.open();
@ -174,8 +150,8 @@ export default class ContactAmountssView extends Vue {
this.contact = (await db.contacts.get(contactDid)) || null; this.contact = (await db.contacts.get(contactDid)) || null;
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || ""; this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = (settings?.apiServer as string) || "";
if (this.activeDid && this.contact) { if (this.activeDid && this.contact) {
this.loadGives(this.activeDid, this.contact); this.loadGives(this.activeDid, this.contact);
@ -199,7 +175,7 @@ export default class ContactAmountssView extends Vue {
async loadGives(activeDid: string, contact: Contact) { async loadGives(activeDid: string, contact: Contact) {
try { try {
const identity = await this.getIdentity(this.activeDid); const identity = await libsUtil.getIdentity(this.activeDid);
let result: Array<GiveSummaryRecord> = []; let result: Array<GiveSummaryRecord> = [];
const url = const url =
this.apiServer + this.apiServer +
@ -207,7 +183,7 @@ export default class ContactAmountssView extends Vue {
encodeURIComponent(identity.did) + encodeURIComponent(identity.did) +
"&recipientDid=" + "&recipientDid=" +
encodeURIComponent(contact.did); encodeURIComponent(contact.did);
const headers = await this.getHeaders(identity); const headers = await getHeaders(activeDid);
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
if (resp.status === 200) { if (resp.status === 200) {
result = resp.data.data; result = resp.data.data;
@ -234,7 +210,7 @@ export default class ContactAmountssView extends Vue {
encodeURIComponent(contact.did) + encodeURIComponent(contact.did) +
"&recipientDid=" + "&recipientDid=" +
encodeURIComponent(identity.did); encodeURIComponent(identity.did);
const headers2 = await this.getHeaders(identity); const headers2 = await getHeaders(activeDid);
const resp2 = await this.axios.get(url2, { headers: headers2 }); const resp2 = await this.axios.get(url2, { headers: headers2 });
if (resp2.status === 200) { if (resp2.status === 200) {
result = R.concat(result, resp2.data.data); result = R.concat(result, resp2.data.data);
@ -289,33 +265,15 @@ export default class ContactAmountssView extends Vue {
object: origClaim, object: origClaim,
}; };
// Make a payload for the claim const vcJwt: string = await createEndorserJwtVcFromClaim(
const vcPayload = { this.activeDid,
vc: { vcClaim,
"@context": ["https://www.w3.org/2018/credentials/v1"], );
type: ["VerifiableCredential"],
credentialSubject: vcClaim,
},
};
// Create a signature using private key of identity
const identity = await this.getIdentity(this.activeDid);
if (identity.keys[0].privateKeyHex !== null) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
const signer = await SimpleSigner(privateKeyHex);
const alg = undefined;
// Create a JWT for the request
const vcJwt: string = await didJwt.createJWT(vcPayload, {
alg: alg,
issuer: identity.did,
signer: signer,
});
// Make the xhr request payload // Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt }); const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = this.apiServer + "/api/v2/claim"; const url = this.apiServer + "/api/v2/claim";
const token = await accessToken(identity); const token = await accessToken(this.activeDid);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + token, Authorization: "Bearer " + token,
@ -324,7 +282,8 @@ export default class ContactAmountssView extends Vue {
try { try {
const resp = await this.axios.post(url, payload, { headers }); const resp = await this.axios.post(url, payload, { headers });
if (resp.data?.success) { if (resp.data?.success) {
record.amountConfirmed = origClaim.object?.amountOfThisGood || 1; record.amountConfirmed =
(origClaim.object?.amountOfThisGood as number) || 1;
} }
} catch (error) { } catch (error) {
let userMessage = "There was an error. See logs for more info."; let userMessage = "There was an error. See logs for more info.";
@ -350,7 +309,6 @@ export default class ContactAmountssView extends Vue {
); );
} }
} }
}
cannotConfirmMessage() { cannotConfirmMessage() {
this.$notify( this.$notify(

31
src/views/ContactGiftingView.vue

@ -72,17 +72,15 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { IIdentifier } from "@veramo/core";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import { NotificationIface } from "@/constants/app"; import { NotificationIface } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { Account, AccountsSchema } from "@/db/tables/accounts"; import { AccountsSchema } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { GiverReceiverInputInfo } from "@/libs/endorserServer"; import { GiverReceiverInputInfo } from "@/libs/endorserServer";
@Component({ @Component({
@ -134,32 +132,7 @@ export default class ContactGiftingView extends Vue {
} }
} }
public async getIdentity(activeDid: string) { openDialog(giver?: GiverReceiverInputInfo) {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records with no identifier available.",
);
}
return identity;
}
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
openDialog(giver: GiverReceiverInputInfo) {
const recipient = this.projectId const recipient = this.projectId
? undefined ? undefined
: { did: this.activeDid, name: "you" }; : { did: this.activeDid, name: "you" };

44
src/views/ContactQRScanShowView.vue

@ -24,6 +24,7 @@
> >
<span class="text-red">Beware!</span> <span class="text-red">Beware!</span>
You aren't sharing your name, so quickly You aren't sharing your name, so quickly
<br />
<router-link <router-link
:to="{ name: 'new-edit-account' }" :to="{ name: 'new-edit-account' }"
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-1.5 py-1 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-1.5 py-1 rounded-md"
@ -72,7 +73,7 @@
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import * as didJwt from "did-jwt"; import { Buffer } from "buffer/";
import { sha256 } from "ethereum-cryptography/sha256.js"; import { sha256 } from "ethereum-cryptography/sha256.js";
import QRCodeVue3 from "qr-code-generator-vue3"; import QRCodeVue3 from "qr-code-generator-vue3";
import * as R from "ramda"; import * as R from "ramda";
@ -90,17 +91,16 @@ import {
deriveAddress, deriveAddress,
getContactPayloadFromJwtUrl, getContactPayloadFromJwtUrl,
nextDerivationPath, nextDerivationPath,
SimpleSigner,
} from "@/libs/crypto"; } from "@/libs/crypto";
import { import {
CONTACT_URL_PREFIX, CONTACT_URL_PREFIX,
createEndorserJwt,
ENDORSER_JWT_URL_LOCATION, ENDORSER_JWT_URL_LOCATION,
isDid, isDid,
register, register,
setVisibilityUtil, setVisibilityUtil,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
import { Buffer } from "buffer/";
@Component({ @Component({
components: { components: {
@ -133,12 +133,15 @@ export default class ContactQRScanShow extends Vue {
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts); const account = R.find((acc) => acc.did === this.activeDid, accounts);
if (account) { if (account) {
const identity = await this.getIdentity(this.activeDid); const identity = await libsUtil.getIdentity(this.activeDid);
const publicKeyHex = identity.keys[0].publicKeyHex; const publicKeyHex = identity.keys[0].publicKeyHex;
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64"); const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
const newDerivPath = nextDerivationPath(account.derivationPath); const newDerivPath = nextDerivationPath(account.derivationPath as string);
const nextPublicHex = deriveAddress(account.mnemonic, newDerivPath)[2]; const nextPublicHex = deriveAddress(
account.mnemonic as string,
newDerivPath,
)[2];
const nextPublicEncKey = Buffer.from(nextPublicHex, "hex"); const nextPublicEncKey = Buffer.from(nextPublicHex, "hex");
const nextPublicEncKeyHash = sha256(nextPublicEncKey); const nextPublicEncKeyHash = sha256(nextPublicEncKey);
const nextPublicEncKeyHashBase64 = const nextPublicEncKeyHashBase64 =
@ -158,15 +161,7 @@ export default class ContactQRScanShow extends Vue {
}, },
}; };
const alg = undefined; const vcJwt: string = await createEndorserJwt(identity.did, contactInfo);
const privateKeyHex: string = identity.keys[0].privateKeyHex;
const signer = await SimpleSigner(privateKeyHex);
// create a JWT for the request
const vcJwt: string = await didJwt.createJWT(contactInfo, {
alg: alg,
issuer: identity.did,
signer: signer,
});
const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION; const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION;
this.qrValue = viewPrefix + vcJwt; this.qrValue = viewPrefix + vcJwt;
} }
@ -184,23 +179,6 @@ export default class ContactQRScanShow extends Vue {
); );
} }
public async getIdentity(activeDid: string) {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account: Account | undefined = R.find(
(acc) => acc.did === activeDid,
accounts,
);
const identity = JSON.parse((account?.identity as string) || "null");
if (!identity) {
throw new Error(
"Attempted to show contact info with no identifier available.",
);
}
return identity;
}
/** /**
* *
* @param content is the result of a QR scan, an array with one item with a rawValue property * @param content is the result of a QR scan, an array with one item with a rawValue property

54
src/views/ContactsView.vue

@ -303,20 +303,20 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { IndexableType } from "dexie"; import { IndexableType } from "dexie";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
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 { AppString, NotificationIface } from "@/constants/app"; import { AppString, NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken, getContactPayloadFromJwtUrl } from "@/libs/crypto"; import { getContactPayloadFromJwtUrl } from "@/libs/crypto";
import { import {
CONTACT_CSV_HEADER, CONTACT_CSV_HEADER,
CONTACT_URL_PREFIX, CONTACT_URL_PREFIX,
GiverReceiverInputInfo, GiverReceiverInputInfo,
GiveSummaryRecord, GiveSummaryRecord,
getHeaders,
isDid, isDid,
register, register,
setVisibilityUtil, setVisibilityUtil,
@ -326,7 +326,6 @@ import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import OfferDialog from "@/components/OfferDialog.vue"; import OfferDialog from "@/components/OfferDialog.vue";
import { Account } from "@/db/tables/accounts";
import { Buffer } from "buffer/"; import { Buffer } from "buffer/";
@ -400,36 +399,6 @@ export default class ContactsView extends Vue {
); );
} }
public async getIdentity(activeDid: string): Promise<IIdentifier> {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records with no identifier available.",
);
}
return identity;
}
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
public async getHeadersAndIdentity(activeDid: string) {
const identity = await this.getIdentity(activeDid);
const headers = await this.getHeaders(identity);
return { headers, identity };
}
async loadGives() { async loadGives() {
if (!this.activeDid) { if (!this.activeDid) {
return; return;
@ -481,7 +450,7 @@ export default class ContactsView extends Vue {
}; };
try { try {
const { headers } = await this.getHeadersAndIdentity(this.activeDid); const headers = await getHeaders(this.activeDid);
const givenByUrl = const givenByUrl =
this.apiServer + this.apiServer +
"/api/v2/report/gives?agentDid=" + "/api/v2/report/gives?agentDid=" +
@ -954,8 +923,19 @@ export default class ContactsView extends Vue {
this.apiServer + this.apiServer +
"/api/report/canDidExplicitlySeeMe?did=" + "/api/report/canDidExplicitlySeeMe?did=" +
encodeURIComponent(contact.did); encodeURIComponent(contact.did);
const identity = await this.getIdentity(this.activeDid); const headers = await getHeaders(this.activeDid);
const headers = await this.getHeaders(identity); if (!headers["Authorization"]) {
this.$notify(
{
group: "alert",
type: "danger",
title: "No Identity",
text: "There is no identity to use to check visibility.",
},
3000,
);
return;
}
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });

28
src/views/DIDView.vue

@ -136,11 +136,11 @@ import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { import {
capitalizeAndInsertSpacesBeforeCaps, capitalizeAndInsertSpacesBeforeCaps,
didInfoForContact, didInfoForContact,
displayAmount, displayAmount,
getHeaders,
GenericCredWrapper, GenericCredWrapper,
GenericVerifiableCredential, GenericVerifiableCredential,
GiveVerifiableCredential, GiveVerifiableCredential,
@ -203,30 +203,6 @@ export default class DIDView extends Vue {
this.allMyDids = allAccounts.map((acc) => acc.did); this.allMyDids = allAccounts.map((acc) => acc.did);
} }
public async buildHeaders(): Promise<HeadersInit> {
const headers: HeadersInit = {
"Content-Type": "application/json",
};
if (this.activeDid) {
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
const account = allAccounts.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse((account?.identity as string) || "null");
if (!identity) {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.",
);
}
headers["Authorization"] = "Bearer " + (await accessToken(identity));
} else {
// it's OK without auth... we just won't get any identifiers
}
return headers;
}
/** /**
* Data loader used by infinite scroller * Data loader used by infinite scroller
* @param payload is the flag from the InfiniteScroll indicating if it should load * @param payload is the flag from the InfiniteScroll indicating if it should load
@ -255,7 +231,7 @@ export default class DIDView extends Vue {
this.apiServer + "/api/v2/report/claims?" + queryParams + postfix, this.apiServer + "/api/v2/report/claims?" + queryParams + postfix,
{ {
method: "GET", method: "GET",
headers: await this.buildHeaders(), headers: await getHeaders(this.activeDid),
}, },
); );

31
src/views/DiscoverView.vue

@ -138,8 +138,7 @@ import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { didInfo, getHeaders, PlanData } from "@/libs/endorserServer";
import { didInfo, PlanData } from "@/libs/endorserServer";
@Component({ @Component({
components: { components: {
@ -203,30 +202,6 @@ export default class DiscoverView extends Vue {
} }
} }
public async buildHeaders(): Promise<HeadersInit> {
const headers: HeadersInit = {
"Content-Type": "application/json",
};
if (this.activeDid) {
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
const account = allAccounts.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.",
);
}
headers["Authorization"] = "Bearer " + (await accessToken(identity));
} else {
// it's OK without auth... we just won't get any identifiers
}
return headers;
}
public async searchAll(beforeId?: string) { public async searchAll(beforeId?: string) {
this.resetCounts(); this.resetCounts();
@ -247,7 +222,7 @@ export default class DiscoverView extends Vue {
this.apiServer + "/api/v2/report/plans?" + queryParams, this.apiServer + "/api/v2/report/plans?" + queryParams,
{ {
method: "GET", method: "GET",
headers: await this.buildHeaders(), headers: await getHeaders(this.activeDid),
}, },
); );
@ -337,7 +312,7 @@ export default class DiscoverView extends Vue {
this.apiServer + "/api/v2/report/plansByLocation?" + queryParams, this.apiServer + "/api/v2/report/plansByLocation?" + queryParams,
{ {
method: "GET", method: "GET",
headers: await this.buildHeaders(), headers: await getHeaders(this.activeDid),
}, },
); );

15
src/views/GiftedDetails.vue

@ -180,16 +180,17 @@ import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app"; import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import {accountsDB, db} from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { import {
constructGive, constructGive,
createAndSubmitGive, didInfo, createAndSubmitGive,
didInfo,
getPlanFromCache, getPlanFromCache,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import {Contact} from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
@Component({ @Component({
components: { components: {
@ -315,9 +316,9 @@ export default class GiftedDetails extends Vue {
const identity = await libsUtil.getIdentity(this.activeDid); const identity = await libsUtil.getIdentity(this.activeDid);
const project = await getPlanFromCache( const project = await getPlanFromCache(
this.projectId, this.projectId,
identity,
this.axios, this.axios,
this.apiServer, this.apiServer,
identity.did,
); );
this.projectName = project?.name this.projectName = project?.name
? "the project: " + project.name ? "the project: " + project.name
@ -380,8 +381,7 @@ export default class GiftedDetails extends Vue {
return; return;
} }
try { try {
const identity = await libsUtil.getIdentity(this.activeDid); const token = await accessToken(this.activeDid);
const token = await accessToken(identity);
const response = await this.axios.delete( const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER + DEFAULT_IMAGE_API_SERVER +
"/image/" + "/image/" +
@ -549,7 +549,6 @@ export default class GiftedDetails extends Vue {
*/ */
public async recordGive() { public async recordGive() {
try { try {
const identity = await libsUtil.getIdentity(this.activeDid);
const recipientDid = this.givenToRecipient const recipientDid = this.givenToRecipient
? this.recipientDid ? this.recipientDid
: undefined; : undefined;
@ -557,7 +556,7 @@ export default class GiftedDetails extends Vue {
const result = await createAndSubmitGive( const result = await createAndSubmitGive(
this.axios, this.axios,
this.apiServer, this.apiServer,
identity, this.activeDid,
this.giverDid, this.giverDid,
recipientDid, recipientDid,
this.description, this.description,

118
src/views/HomeView.vue

@ -5,7 +5,7 @@
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-2 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-2 pb-24 max-w-3xl mx-auto">
<h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-8">
Time Safari {{ AppString.APP_NAME }}
</h1> </h1>
<!-- prompt to install notifications --> <!-- prompt to install notifications -->
@ -79,27 +79,37 @@
<!-- !isCreatingIdentifier --> <!-- !isCreatingIdentifier -->
<div <div
v-if="!activeDid" v-if="!activeDid"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4" class="bg-amber-200 rounded-md text-center px-4 py-3 mb-4"
> >
<p class="text-lg mb-3"> <p class="text-lg mb-3">
Want to connect with your contacts, or share contributions or To recognize giving, have someone register you:
projects?
</p> </p>
<div class="flex justify-center">
<!-- <button-->
<!-- class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"-->
<!-- @click="generateIdentifier()"-->
<!-- >-->
<!-- Let me start the easiest (with a passkey).-->
<!-- </button>-->
<router-link <router-link
:to="{ name: 'start' }" :to="{ name: 'contact-qr' }"
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md" class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
> >
Create An Identifier Share your contact info.
</router-link> </router-link>
</div> </div>
</div>
<div v-else class="mb-4">
<!-- activeDid -->
<div <div
v-else-if="!isRegistered" v-if="!isRegistered"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4" class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
> >
<!-- activeDid && !isRegistered --> <!-- activeDid && !isRegistered -->
Someone must register you before you can give kudos or make offers or Someone must register you before you can give kudos or make offers
create projects... basically before doing anything. or create projects... basically before doing anything.
<router-link <router-link
:to="{ name: 'contact-qr' }" :to="{ name: 'contact-qr' }"
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md" class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
@ -166,6 +176,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<GiftedDialog ref="customDialog" /> <GiftedDialog ref="customDialog" />
<GiftedPrompts ref="giftedPrompts" /> <GiftedPrompts ref="giftedPrompts" />
@ -305,10 +316,10 @@
<script lang="ts"> <script lang="ts">
import { UAParser } from "ua-parser-js"; import { UAParser } from "ua-parser-js";
import { IIdentifier } from "@veramo/core";
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 App from "../App.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import GiftedPrompts from "@/components/GiftedPrompts.vue"; import GiftedPrompts from "@/components/GiftedPrompts.vue";
@ -316,9 +327,8 @@ import FeedFilters from "@/components/FeedFilters.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue"; import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app"; import { AppString, NotificationIface } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { import {
BoundingBox, BoundingBox,
@ -326,17 +336,17 @@ import {
MASTER_SETTINGS_KEY, MASTER_SETTINGS_KEY,
Settings, Settings,
} from "@/db/tables/settings"; } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { import {
contactForDid, contactForDid,
containsNonHiddenDid, containsNonHiddenDid,
didInfoForContact, didInfoForContact,
fetchEndorserRateLimits, fetchEndorserRateLimits,
getHeaders,
getPlanFromCache, getPlanFromCache,
GiverReceiverInputInfo, GiverReceiverInputInfo,
GiveSummaryRecord, GiveSummaryRecord,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { generateSaveAndActivateIdentity } from "@/libs/util"; import { registerSaveAndActivatePasskey } from "@/libs/util";
interface GiveRecordWithContactInfo extends GiveSummaryRecord { interface GiveRecordWithContactInfo extends GiveSummaryRecord {
giver: { giver: {
@ -354,6 +364,11 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord {
} }
@Component({ @Component({
computed: {
App() {
return App;
},
},
components: { components: {
GiftedDialog, GiftedDialog,
GiftedPrompts, GiftedPrompts,
@ -367,6 +382,8 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord {
export default class HomeView extends Vue { export default class HomeView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
AppString = AppString;
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
@ -374,6 +391,7 @@ export default class HomeView extends Vue {
feedData: GiveRecordWithContactInfo[] = []; feedData: GiveRecordWithContactInfo[] = [];
feedPreviousOldestId?: string; feedPreviousOldestId?: string;
feedLastViewedClaimId?: string; feedLastViewedClaimId?: string;
givenName = "";
isAnyFeedFilterOn: boolean; isAnyFeedFilterOn: boolean;
isCreatingIdentifier = false; isCreatingIdentifier = false;
isFeedFilteredByVisible = false; isFeedFilteredByVisible = false;
@ -387,25 +405,6 @@ export default class HomeView extends Vue {
showShortcutBvc = false; showShortcutBvc = false;
userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html
public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
return identity; // may be null
}
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
async mounted() { async mounted() {
try { try {
await accountsDB.open(); await accountsDB.open();
@ -418,6 +417,7 @@ export default class HomeView extends Vue {
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
this.feedLastViewedClaimId = settings?.lastViewedClaimId; this.feedLastViewedClaimId = settings?.lastViewedClaimId;
this.givenName = settings?.firstName || "";
this.isFeedFilteredByVisible = !!settings?.filterFeedByVisible; this.isFeedFilteredByVisible = !!settings?.filterFeedByVisible;
this.isFeedFilteredByNearby = !!settings?.filterFeedByNearby; this.isFeedFilteredByNearby = !!settings?.filterFeedByNearby;
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
@ -426,21 +426,13 @@ export default class HomeView extends Vue {
this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings); this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings);
if (this.allMyDids.length === 0) { // someone may have have registered after sharing contact info, so recheck
this.isCreatingIdentifier = true;
this.activeDid = await generateSaveAndActivateIdentity();
this.allMyDids = [this.activeDid];
this.isCreatingIdentifier = false;
}
// someone may have have registered after sharing contact info
if (!this.isRegistered && this.activeDid) { if (!this.isRegistered && this.activeDid) {
const identity = await this.getIdentity(this.activeDid);
try { try {
const resp = await fetchEndorserRateLimits( const resp = await fetchEndorserRateLimits(
this.apiServer, this.apiServer,
this.axios, this.axios,
identity as IIdentifier, this.activeDid,
); );
if (resp.status === 200) { if (resp.status === 200) {
// we just needed to know that they're registered // we just needed to know that they're registered
@ -475,6 +467,15 @@ export default class HomeView extends Vue {
} }
} }
async generateIdentifier() {
this.isCreatingIdentifier = true;
const account = await registerSaveAndActivatePasskey(
AppString.APP_NAME + (this.givenName ? " - " + this.givenName : ""),
);
this.activeDid = account.did;
this.allMyDids = this.allMyDids.concat(this.activeDid);
this.isCreatingIdentifier = false;
}
resultsAreFiltered() { resultsAreFiltered() {
return this.isFeedFilteredByVisible || this.isFeedFilteredByNearby; return this.isFeedFilteredByVisible || this.isFeedFilteredByNearby;
} }
@ -483,26 +484,6 @@ export default class HomeView extends Vue {
return "Notification" in window; return "Notification" in window;
} }
public async buildHeaders() {
const headers: HeadersInit = {
"Content-Type": "application/json",
};
const identity = await this.getIdentity(this.activeDid);
if (this.activeDid) {
if (identity) {
headers["Authorization"] = "Bearer " + (await accessToken(identity));
} else {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.",
);
}
} else {
// it's OK without auth... we just won't get any identifiers
}
return headers;
}
// only called when a setting was changed // only called when a setting was changed
async reloadFeedOnChange() { async reloadFeedOnChange() {
await db.open(); await db.open();
@ -520,7 +501,7 @@ export default class HomeView extends Vue {
* Data loader used by infinite scroller * Data loader used by infinite scroller
* @param payload is the flag from the InfiniteScroll indicating if it should load * @param payload is the flag from the InfiniteScroll indicating if it should load
**/ **/
public async loadMoreGives(payload: boolean) { async loadMoreGives(payload: boolean) {
// Since feed now loads projects along the way, it takes longer // Since feed now loads projects along the way, it takes longer
// and the InfiniteScroll component triggers a load before finished. // and the InfiniteScroll component triggers a load before finished.
// One alternative is to totally separate the project link loading. // One alternative is to totally separate the project link loading.
@ -542,7 +523,7 @@ export default class HomeView extends Vue {
} }
} }
public async updateAllFeed() { async updateAllFeed() {
this.isFeedLoading = true; this.isFeedLoading = true;
let endOfResults = true; let endOfResults = true;
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId) await this.retrieveGives(this.apiServer, this.feedPreviousOldestId)
@ -550,7 +531,6 @@ export default class HomeView extends Vue {
if (results.data.length > 0) { if (results.data.length > 0) {
endOfResults = false; endOfResults = false;
// include the descriptions of the giver and receiver // include the descriptions of the giver and receiver
const identity = await this.getIdentity(this.activeDid);
for (const record: GiveSummaryRecord of results.data) { for (const record: GiveSummaryRecord of results.data) {
// similar code is in endorser-mobile utility.ts // similar code is in endorser-mobile utility.ts
// claim.claim happen for some claims wrapped in a Verifiable Credential // claim.claim happen for some claims wrapped in a Verifiable Credential
@ -567,9 +547,9 @@ export default class HomeView extends Vue {
// We should display it immediately and then get the plan later. // We should display it immediately and then get the plan later.
const plan = await getPlanFromCache( const plan = await getPlanFromCache(
record.fulfillsPlanHandleId, record.fulfillsPlanHandleId,
identity,
this.axios, this.axios,
this.apiServer, this.apiServer,
this.activeDid,
); );
// check if the record should be filtered out // check if the record should be filtered out
@ -650,7 +630,7 @@ export default class HomeView extends Vue {
* @param beforeId the earliest ID (of previous searches) to search earlier * @param beforeId the earliest ID (of previous searches) to search earlier
* @return claims in reverse chronological order * @return claims in reverse chronological order
*/ */
public async retrieveGives(endorserApiServer: string, beforeId?: string) { async retrieveGives(endorserApiServer: string, beforeId?: string) {
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId; const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
const response = await fetch( const response = await fetch(
endorserApiServer + endorserApiServer +
@ -658,7 +638,7 @@ export default class HomeView extends Vue {
beforeQuery, beforeQuery,
{ {
method: "GET", method: "GET",
headers: await this.buildHeaders(), headers: await getHeaders(this.activeDid),
}, },
); );

80
src/views/NewEditProjectView.vue

@ -174,21 +174,23 @@
<script lang="ts"> <script lang="ts">
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import * as didJwt from "did-jwt";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet"; import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app"; import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import {
createEndorserJwtVcFromClaim,
PlanVerifiableCredential,
} from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { useAppStore } from "@/store/app"; import { useAppStore } from "@/store/app";
import { PlanVerifiableCredential } from "@/libs/endorserServer";
import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
@Component({ @Component({
components: { ImageMethodDialog, LMap, LMarker, LTileLayer, QuickNav }, components: { ImageMethodDialog, LMap, LMarker, LTileLayer, QuickNav },
@ -229,31 +231,6 @@ export default class NewEditProjectView extends Vue {
libsUtil = libsUtil; libsUtil = libsUtil;
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse((account?.identity as string) || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identifier available.",
);
}
return identity;
}
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
async mounted() { async mounted() {
await accountsDB.open(); await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count(); this.numAccounts = await accountsDB.accounts.count();
@ -267,23 +244,17 @@ export default class NewEditProjectView extends Vue {
if (this.numAccounts === 0) { if (this.numAccounts === 0) {
this.errNote("There was a problem loading your account info."); this.errNote("There was a problem loading your account info.");
} else { } else {
const identity = await this.getIdentity(this.activeDid); this.loadProject(this.activeDid);
if (!identity) {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.",
);
}
this.loadProject(identity);
} }
} }
} }
async loadProject(identity: IIdentifier) { async loadProject(userDid: string) {
const url = const url =
this.apiServer + this.apiServer +
"/api/claim/byHandle/" + "/api/claim/byHandle/" +
encodeURIComponent(this.projectId); encodeURIComponent(this.projectId);
const token = await accessToken(identity); const token = await accessToken(userDid);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + token, Authorization: "Bearer " + token,
@ -342,8 +313,7 @@ export default class NewEditProjectView extends Vue {
return; return;
} }
try { try {
const identity = await libsUtil.getIdentity(this.activeDid); const token = await accessToken(this.activeDid);
const token = await accessToken(identity);
const response = await this.axios.delete( const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER + DEFAULT_IMAGE_API_SERVER +
"/image/" + "/image/" +
@ -446,31 +416,13 @@ export default class NewEditProjectView extends Vue {
} else { } else {
delete vcClaim.startTime; delete vcClaim.startTime;
} }
// Make a payload for the claim const vcJwt = await createEndorserJwtVcFromClaim(identity.did, vcClaim);
const vcPayload = {
vc: {
"@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"],
credentialSubject: vcClaim,
},
};
// create a signature using private key of identity
if (identity.keys[0].privateKeyHex != null) {
const privateKeyHex: string = identity.keys[0].privateKeyHex;
const signer = await SimpleSigner(privateKeyHex);
const alg = undefined;
// create a JWT for the request
const vcJwt: string = await didJwt.createJWT(vcPayload, {
alg: alg,
issuer: identity.did,
signer: signer,
});
// Make the xhr request payload // Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt }); const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = this.apiServer + "/api/v2/claim"; const url = this.apiServer + "/api/v2/claim";
const token = await accessToken(identity); const token = await accessToken(identity.did);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + token, Authorization: "Bearer " + token,
@ -533,10 +485,7 @@ export default class NewEditProjectView extends Vue {
); );
} }
} else { } else {
console.error( console.error("Here's the full error trying to save the claim:", error);
"Here's the full error trying to save the claim:",
error,
);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -551,7 +500,6 @@ export default class NewEditProjectView extends Vue {
this.errorMessage = userMessage; this.errorMessage = userMessage;
} }
} }
}
public async onSaveProjectClick() { public async onSaveProjectClick() {
this.isHiddenSave = true; this.isHiddenSave = true;
@ -560,7 +508,7 @@ export default class NewEditProjectView extends Vue {
if (this.numAccounts === 0) { if (this.numAccounts === 0) {
console.error("Error: there is no account."); console.error("Error: there is no account.");
} else { } else {
const identity = await this.getIdentity(this.activeDid); const identity = await libsUtil.getIdentity(this.activeDid);
this.saveProject(identity); this.saveProject(identity);
} }
} }

76
src/views/ProjectViewView.vue

@ -402,8 +402,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError, RawAxiosRequestHeaders } from "axios"; import { AxiosError } from "axios";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
@ -422,6 +421,7 @@ import * as libsUtil from "@/libs/util";
import { import {
BLANK_GENERIC_SERVER_RECORD, BLANK_GENERIC_SERVER_RECORD,
GenericCredWrapper, GenericCredWrapper,
getHeaders,
GiverReceiverInputInfo, GiverReceiverInputInfo,
GiveSummaryRecord, GiveSummaryRecord,
OfferSummaryRecord, OfferSummaryRecord,
@ -484,24 +484,12 @@ export default class ProjectViewView extends Vue {
const accounts = accountsDB.accounts; const accounts = accountsDB.accounts;
const accountsArr: Account[] = await accounts?.toArray(); const accountsArr: Account[] = await accounts?.toArray();
this.allMyDids = accountsArr.map((acc) => acc.did); this.allMyDids = accountsArr.map((acc) => acc.did);
const account = accountsArr.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse((account?.identity as string) || "null");
const pathParam = window.location.pathname.substring("/project/".length); const pathParam = window.location.pathname.substring("/project/".length);
if (pathParam) { if (pathParam) {
this.projectId = decodeURIComponent(pathParam); this.projectId = decodeURIComponent(pathParam);
} }
this.loadProject(this.projectId, identity); this.loadProject(this.projectId, this.activeDid);
}
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
return identity;
} }
onEditClick() { onEditClick() {
@ -521,18 +509,12 @@ export default class ProjectViewView extends Vue {
this.expanded = false; this.expanded = false;
} }
async loadProject(projectId: string, identity: IIdentifier) { async loadProject(projectId: string, userDid: string) {
this.projectId = projectId; this.projectId = projectId;
const url = const url =
this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(projectId); this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(projectId);
const headers: RawAxiosRequestHeaders = { const headers = await getHeaders(userDid);
"Content-Type": "application/json",
};
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
@ -602,8 +584,8 @@ export default class ProjectViewView extends Vue {
this.loadPlanFulfillersTo(); this.loadPlanFulfillersTo();
// now load fulfilled-by, a single project // now load fulfilled-by, a single project
if (identity) { if (this.activeDid) {
const token = await accessToken(identity); const token = await accessToken(this.activeDid);
headers["Authorization"] = "Bearer " + token; headers["Authorization"] = "Bearer " + token;
} }
const fulfilledByUrl = const fulfilledByUrl =
@ -655,15 +637,7 @@ export default class ProjectViewView extends Vue {
} }
const givesInUrl = givesUrl + postfix; const givesInUrl = givesUrl + postfix;
const headers: RawAxiosRequestHeaders = { const headers = await getHeaders(this.activeDid);
"Content-Type": "application/json",
};
const identity = await this.getIdentity(this.activeDid);
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
try { try {
const resp = await this.axios.get(givesInUrl, { headers }); const resp = await this.axios.get(givesInUrl, { headers });
if (resp.status === 200 && resp.data.data) { if (resp.status === 200 && resp.data.data) {
@ -710,15 +684,7 @@ export default class ProjectViewView extends Vue {
} }
const offersInUrl = offersUrl + postfix; const offersInUrl = offersUrl + postfix;
const headers: RawAxiosRequestHeaders = { const headers = await getHeaders(this.activeDid);
"Content-Type": "application/json",
};
const identity = await this.getIdentity(this.activeDid);
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
try { try {
const resp = await this.axios.get(offersInUrl, { headers }); const resp = await this.axios.get(offersInUrl, { headers });
if (resp.status === 200 && resp.data.data) { if (resp.status === 200 && resp.data.data) {
@ -766,15 +732,7 @@ export default class ProjectViewView extends Vue {
} }
const fulfillsInUrl = fulfillsUrl + postfix; const fulfillsInUrl = fulfillsUrl + postfix;
const headers: RawAxiosRequestHeaders = { const headers = await getHeaders(this.activeDid);
"Content-Type": "application/json",
};
const identity = await this.getIdentity(this.activeDid);
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
try { try {
const resp = await this.axios.get(fulfillsInUrl, { headers }); const resp = await this.axios.get(fulfillsInUrl, { headers });
if (resp.status === 200) { if (resp.status === 200) {
@ -822,15 +780,7 @@ export default class ProjectViewView extends Vue {
} }
const providedByFullUrl = providedByUrl + postfix; const providedByFullUrl = providedByUrl + postfix;
const headers: RawAxiosRequestHeaders = { const headers = await getHeaders(this.activeDid);
"Content-Type": "application/json",
};
const identity = await this.getIdentity(this.activeDid);
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
try { try {
const resp = await this.axios.get(providedByFullUrl, { headers }); const resp = await this.axios.get(providedByFullUrl, { headers });
if (resp.status === 200) { if (resp.status === 200) {
@ -877,7 +827,7 @@ export default class ProjectViewView extends Vue {
path: "/project/" + encodeURIComponent(projectId), path: "/project/" + encodeURIComponent(projectId),
}; };
this.$router.push(route); this.$router.push(route);
this.loadProject(projectId, await this.getIdentity(this.activeDid)); this.loadProject(projectId, this.activeDid);
} }
getOpenStreetMapUrl() { getOpenStreetMapUrl() {
@ -1021,7 +971,7 @@ export default class ProjectViewView extends Vue {
}; };
const result = await serverUtil.createAndSubmitClaim( const result = await serverUtil.createAndSubmitClaim(
confirmationClaim, confirmationClaim,
await this.getIdentity(this.activeDid), this.activeDid,
this.apiServer, this.apiServer,
this.axios, this.axios,
); );

22
src/views/ProjectsView.vue

@ -281,7 +281,7 @@ export default class ProjectsView extends Vue {
console.error("No accounts found."); console.error("No accounts found.");
this.errNote("You need an identifier to load your projects."); this.errNote("You need an identifier to load your projects.");
} else { } else {
this.currentIid = await this.getIdentity(activeDid); this.currentIid = await libsUtil.getIdentity(activeDid);
await this.loadOffers(); await this.loadOffers();
} }
} catch (err) { } catch (err) {
@ -356,26 +356,10 @@ export default class ProjectsView extends Vue {
async loadProjects(identifier?: IIdentifier, urlExtra: string = "") { async loadProjects(identifier?: IIdentifier, urlExtra: string = "") {
const identity = identifier || this.currentIid; const identity = identifier || this.currentIid;
const url = `${this.apiServer}/api/v2/report/plansByIssuer?${urlExtra}`; const url = `${this.apiServer}/api/v2/report/plansByIssuer?${urlExtra}`;
const token: string = await accessToken(identity); const token: string = await accessToken(identity.did);
await this.projectDataLoader(url, token); await this.projectDataLoader(url, token);
} }
public async getIdentity(activeDid: string): Promise<IIdentifier> {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse((account?.identity as string) || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identifier available.",
);
}
return identity;
}
/** /**
* Handle clicking on a project entry found in the list * Handle clicking on a project entry found in the list
* @param id of the project * @param id of the project
@ -474,7 +458,7 @@ export default class ProjectsView extends Vue {
async loadOffers(identifier?: IIdentifier, urlExtra: string = "") { async loadOffers(identifier?: IIdentifier, urlExtra: string = "") {
const identity = identifier || this.currentIid; const identity = identifier || this.currentIid;
const url = `${this.apiServer}/api/v2/report/offers?offeredByDid=${identity.did}${urlExtra}`; const url = `${this.apiServer}/api/v2/report/offers?offeredByDid=${identity.did}${urlExtra}`;
const token: string = await accessToken(identity); const token: string = await accessToken(identity.did);
await this.offerDataLoader(url, token); await this.offerDataLoader(url, token);
} }

5
src/views/QuickActionBvcBeginView.vue

@ -124,7 +124,6 @@ export default class QuickActionBvcBeginView extends Vue {
try { try {
const hoursNum = libsUtil.numberOrZero(this.hoursStr); const hoursNum = libsUtil.numberOrZero(this.hoursStr);
const identity = await libsUtil.getIdentity(activeDid);
this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000); this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000);
@ -134,7 +133,7 @@ export default class QuickActionBvcBeginView extends Vue {
const timeResult = await createAndSubmitGive( const timeResult = await createAndSubmitGive(
axios, axios,
apiServer, apiServer,
identity, activeDid,
activeDid, activeDid,
undefined, undefined,
undefined, undefined,
@ -165,7 +164,7 @@ export default class QuickActionBvcBeginView extends Vue {
if (this.attended) { if (this.attended) {
const attendResult = await createAndSubmitClaim( const attendResult = await createAndSubmitClaim(
bvcMeetingJoinClaim(activeDid, this.todayOrPreviousStartDate), bvcMeetingJoinClaim(activeDid, this.todayOrPreviousStartDate),
identity, activeDid,
apiServer, apiServer,
axios, axios,
); );

25
src/views/QuickActionBvcEndView.vue

@ -138,28 +138,25 @@
import axios from "axios"; import axios from "axios";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; 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 { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { import {
BVC_MEETUPS_PROJECT_CLAIM_ID, BVC_MEETUPS_PROJECT_CLAIM_ID,
claimSpecialDescription, claimSpecialDescription,
containsHiddenDid, containsHiddenDid,
createAndSubmitConfirmation, createAndSubmitConfirmation,
createAndSubmitGive, createAndSubmitGive,
ErrorResult,
GenericCredWrapper, GenericCredWrapper,
GenericVerifiableCredential, GenericVerifiableCredential,
getHeaders,
ErrorResult,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
@Component({ @Component({
methods: { claimSpecialDescription }, methods: { claimSpecialDescription },
@ -213,16 +210,7 @@ export default class QuickActionBvcBeginView extends Vue {
await accountsDB.open(); await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray(); const allAccounts = await accountsDB.accounts.toArray();
this.allMyDids = allAccounts.map((acc) => acc.did); this.allMyDids = allAccounts.map((acc) => acc.did);
const account: Account | undefined = await accountsDB.accounts const headers = await getHeaders(this.activeDid);
.where("did")
.equals(this.activeDid)
.first();
const identity: IIdentifier = JSON.parse(
(account?.identity as string) || "null",
);
const headers = {
Authorization: "Bearer " + (await accessToken(identity)),
};
try { try {
const response = await fetch( const response = await fetch(
this.apiServer + this.apiServer +
@ -275,8 +263,6 @@ export default class QuickActionBvcBeginView extends Vue {
async record() { async record() {
try { try {
const identity = await libsUtil.getIdentity(this.activeDid);
this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000); this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000);
// in parallel, make a confirmation for each selected claim and send them all to the server // in parallel, make a confirmation for each selected claim and send them all to the server
@ -288,9 +274,8 @@ export default class QuickActionBvcBeginView extends Vue {
if (!record) { if (!record) {
return { type: "error", error: "Record not found." }; return { type: "error", error: "Record not found." };
} }
const identity = await libsUtil.getIdentity(this.activeDid);
return createAndSubmitConfirmation( return createAndSubmitConfirmation(
identity, this.activeDid,
record.claim as GenericVerifiableCredential, record.claim as GenericVerifiableCredential,
record.id, record.id,
record.handleId, record.handleId,
@ -324,7 +309,7 @@ export default class QuickActionBvcBeginView extends Vue {
const giveResult = await createAndSubmitGive( const giveResult = await createAndSubmitGive(
axios, axios,
this.apiServer, this.apiServer,
identity, this.activeDid,
undefined, undefined,
this.activeDid, this.activeDid,
this.description, this.description,

4
src/views/SharedPhotoView.vue

@ -65,7 +65,6 @@ import {
} from "@/constants/app"; } from "@/constants/app";
import { db } from "@/db/index"; import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { getIdentity } from "@/libs/util";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
@Component({ components: { PhotoDialog, QuickNav } }) @Component({ components: { PhotoDialog, QuickNav } })
@ -152,8 +151,7 @@ export default class SharedPhotoView extends Vue {
let result; let result;
try { try {
// send the image to the server // send the image to the server
const identifier = await getIdentity(this.activeDid as string); const token = await accessToken(this.activeDid);
const token = await accessToken(identifier);
const headers = { const headers = {
Authorization: "Bearer " + token, Authorization: "Bearer " + token,
}; };

73
src/views/StartView.vue

@ -17,7 +17,7 @@
<!-- Heading --> <!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Start Here Generate an Identity
</h1> </h1>
</div> </div>
@ -25,33 +25,57 @@
<div id="start-question" class="mt-8"> <div id="start-question" class="mt-8">
<div class="max-w-3xl mx-auto"> <div class="max-w-3xl mx-auto">
<p class="text-center text-xl font-light"> <p class="text-center text-xl font-light">
Do you want a new identifier of your own? How do you want to create this identifier?
</p> </p>
<p class="text-center font-light"> <p class="text-center font-light mt-6">
If you haven't used this before, click "Yes" to generate a new A <strong>passkey</strong> is easy to manage, though it is less
identifier. interoperable with other systems for advanced uses.
<a
href="https://www.perplexity.ai/search/what-are-passkeys-v2SHV3yLQlyA2CYH6.Nvhg"
target="_blank"
>
<fa icon="info-circle" class="fa-fw text-blue-500" />
</a>
</p> </p>
<p class="text-center mb-4 font-light"> <p class="text-center font-light mt-4">
Only click "No" if you have a seed of 12 or 24 words generated A <strong>new seed</strong> allows you full control over the keys,
elsewhere. though you are responsible for backups.
<a
href="https://www.perplexity.ai/search/what-is-a-seed-phrase-OqiP9foVRXidr_2le5OFKA"
target="_blank"
>
<fa icon="info-circle" class="fa-fw text-blue-500" />
</a>
</p> </p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-4">
<a
@click="onClickNewPasskey()"
class="block w-full text-center text-lg uppercase 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-3 rounded-md mb-2 cursor-pointer"
>
Generate one with a passkey
</a>
<a <a
@click="onClickYes()" @click="onClickNewSeed()"
class="block w-full text-center text-lg uppercase 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-3 rounded-md mb-2" class="block w-full text-center text-lg uppercase 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-3 rounded-md mb-2 cursor-pointer"
> >
Yes, generate one Generate one with a new seed
</a> </a>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2"> </div>
<p class="text-center font-light mt-4">
You can also import an existing seed or derive a new address from an
existing seed.
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-2">
<a <a
@click="onClickNo()" @click="onClickNo()"
class="block w-full text-center text-md 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.5 py-2 rounded-md" class="block w-full text-center text-md uppercase 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-1.5 py-2 rounded-md cursor-pointer"
> >
No, I have a seed You have a seed
</a> </a>
<a <a
v-if="numAccounts > 0" v-if="numAccounts > 0"
@click="onClickDerive()" @click="onClickDerive()"
class="block w-full text-center text-md 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.5 py-2 rounded-md" class="block w-full text-center text-md uppercase 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-1.5 py-2 rounded-md cursor-pointer"
> >
Derive new address from existing seed Derive new address from existing seed
</a> </a>
@ -64,23 +88,38 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { accountsDB } from "@/db/index"; import { AppString } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { registerSaveAndActivatePasskey } from "@/libs/util";
@Component({ @Component({
components: {}, components: {},
}) })
export default class StartView extends Vue { export default class StartView extends Vue {
givenName = "";
numAccounts = 0; numAccounts = 0;
async mounted() { async mounted() {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.givenName = settings?.firstName || "";
await accountsDB.open(); await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count(); this.numAccounts = await accountsDB.accounts.count();
} }
public onClickYes() { public onClickNewSeed() {
this.$router.push({ name: "new-identifier" }); this.$router.push({ name: "new-identifier" });
} }
public async onClickNewPasskey() {
const keyName =
AppString.APP_NAME + (this.givenName ? " - " + this.givenName : "");
await registerSaveAndActivatePasskey(keyName);
this.$router.push({ name: "account" });
}
public onClickNo() { public onClickNo() {
this.$router.push({ name: "import-account" }); this.$router.push({ name: "import-account" });
} }

26
src/views/TestView.vue

@ -244,17 +244,16 @@ import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app"; import { AppString, NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { import {
createPeerDid,
PeerSetup, PeerSetup,
registerCredential,
verifyJwtP256, verifyJwtP256,
verifyJwtSimplewebauthn, verifyJwtSimplewebauthn,
verifyJwtWebCrypto, verifyJwtWebCrypto,
} from "@/libs/didPeer"; } from "@/libs/didPeer";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { registerAndSavePasskey } from "@/libs/util";
const inputFileNameRef = ref<Blob>(); const inputFileNameRef = ref<Blob>();
@ -333,14 +332,14 @@ export default class Help extends Vue {
} }
public async register() { public async register() {
const DEFAULT_USERNAME = "Time Safari Tester"; const DEFAULT_USERNAME = AppString.APP_NAME + " Tester";
if (!this.userName) { if (!this.userName) {
this.$notify( this.$notify(
{ {
group: "modal", group: "modal",
type: "confirm", type: "confirm",
title: "No Name", title: "No Name",
text: "You must have a name to attach to this passkey. Would you like to enter your own name first?", text: "You should have a name to attach to this passkey. Would you like to enter your own name first?",
onNo: async () => { onNo: async () => {
this.userName = DEFAULT_USERNAME; this.userName = DEFAULT_USERNAME;
}, },
@ -353,18 +352,11 @@ export default class Help extends Vue {
); );
return; return;
} }
const cred = await registerCredential("Time Safari - " + this.userName); const account = await registerAndSavePasskey(
const publicKeyBytes = cred.publicKeyBytes; AppString.APP_NAME + " - " + this.userName,
this.activeDid = createPeerDid(publicKeyBytes as Uint8Array); );
this.credIdHex = cred.credIdHex as string; this.activeDid = account.did;
this.credIdHex = account.passkeyCredIdHex;
await accountsDB.open();
await accountsDB.accounts.add({
dateCreated: new Date().toISOString(),
did: this.activeDid,
passkeyCredIdHex: this.credIdHex,
publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"),
});
} }
public async createJwtSimplewebauthn() { public async createJwtSimplewebauthn() {

Loading…
Cancel
Save