Refactor JWT-creation calls through single function #119

Merged
trentlarson merged 7 commits from passkey-all into master 4 months ago
  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",
) {
try {
const identity = await libsUtil.getIdentity(this.activeDid);
const result = await createAndSubmitGive(
this.axios,
this.apiServer,
identity,
this.activeDid,
giverDid,
this.receiver?.did as string,
description,

3
src/components/OfferDialog.vue

@ -223,11 +223,10 @@ export default class OfferDialog extends Vue {
}
try {
const identity = await libsUtil.getIdentity(this.activeDid);
const result = await createAndSubmitOffer(
this.axios,
this.apiServer,
identity,
this.activeDid,
description,
amount,
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 { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import { getIdentity } from "@/libs/util";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
@ -348,8 +347,7 @@ export default class PhotoDialog extends Vue {
this.blob = (await cropper?.getBlob()) || undefined;
}
const identifier = await getIdentity(this.activeDid);
const token = await accessToken(identifier);
const token = await accessToken(this.activeDid);
const headers = {
Authorization: "Bearer " + token,
};

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

@ -1,12 +1,11 @@
import axios from "axios";
import * as R from "ramda";
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader";
import * as SkeletonUtils from "three/addons/utils/SkeletonUtils";
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 { accessToken } from "@/libs/crypto";
import { getHeaders } from "@/libs/endorserServer";
const ANIMATION_DURATION_SECS = 10;
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 activeDid = settings?.activeDid || "";
const apiServer = settings?.apiServer;
await accountsDB.open();
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 headers = await getHeaders(activeDid);
const url = apiServer + "/api/v2/report/claims?claimType=GiveAction";
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 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";
export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'";
@ -85,32 +88,17 @@ export const generateSeed = (): string => {
/**
* Retreive an access token
*
* @param {IIdentifier} identifier
* @return {*}
*/
export const accessToken = async (identifier: IIdentifier) => {
const did: string = identifier.did;
const privateKeyHex: string = identifier.keys[0].privateKeyHex as string;
const signer = SimpleSigner(privateKeyHex);
export const accessToken = async (did?: string) => {
if (did) {
const nowEpoch = Math.floor(Date.now() / 1000);
const endEpoch = nowEpoch + 60; // add one minute
const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
const alg = undefined; // defaults to 'ES256K', more standardized but harder to verify vs ES256K-R
const jwt: string = await didJwt.createJWT(tokenPayload, {
alg,
issuer: did,
signer,
});
return jwt;
};
export const sign = async (privateKeyHex: string) => {
const signer = SimpleSigner(privateKeyHex);
return signer;
return createEndorserJwt(did, tokenPayload);
} else {
return "";
}
};
/**

4
src/libs/crypto/passkeyHelpers.ts

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

34
src/libs/didPeer.ts

@ -20,6 +20,7 @@ import {
PublicKeyCredentialRequestOptionsJSON,
} from "@simplewebauthn/types";
import { AppString } from "@/constants/app";
import { getWebCrypto, unwrapEC2Signature } from "@/libs/crypto/passkeyHelpers";
const PEER_DID_PREFIX = "did:peer:";
@ -42,9 +43,9 @@ function arrayToBase64Url(anything: Uint8Array) {
export async function registerCredential(passkeyName?: string) {
const options: PublicKeyCredentialCreationOptionsJSON =
await generateRegistrationOptions({
rpName: "Time Safari",
rpName: AppString.APP_NAME,
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
// (Recommended for smoother UX)
attestationType: "none",
@ -116,13 +117,17 @@ export class PeerSetup {
issuerDid: string,
payload: object,
credIdHex: string,
expMinutes: number = 1,
) {
const credentialId = arrayBufferToBase64URLString(
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 = {
...payload,
iat: Math.floor(Date.now() / 1000),
exp: expiryTime,
iat: issuedAt,
iss: issuerDid,
};
this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload)));
@ -158,7 +163,8 @@ export class PeerSetup {
const dataInJwt = {
AuthenticationDataB64URL: authenticatorDataBase64Url,
ClientDataJSONB64URL: this.clientDataJsonBase64Url,
iat: Math.floor(Date.now() / 1000),
exp: expiryTime,
iat: issuedAt,
iss: issuerDid,
};
const dataInJwtString = JSON.stringify(dataInJwt);
@ -177,10 +183,14 @@ export class PeerSetup {
issuerDid: string,
payload: object,
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 = {
...payload,
iat: Math.floor(Date.now() / 1000),
exp: expiryTime,
iat: issuedAt,
iss: issuerDid,
};
const dataToSignString = JSON.stringify(fullPayload);
@ -226,7 +236,8 @@ export class PeerSetup {
const dataInJwt = {
AuthenticationDataB64URL: authenticatorDataBase64Url,
ClientDataJSONB64URL: this.clientDataJsonBase64Url,
iat: Math.floor(Date.now() / 1000),
exp: expiryTime,
iat: issuedAt,
iss: issuerDid,
};
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.
// Requires:
// npm install @noble/curves
@ -379,6 +400,7 @@ export async function verifyJwtSimplewebauthn(
return verification.verified;
}
// similar code is in endorser-ch util-crypto.ts verifyPeerSignature
export async function verifyJwtWebCrypto(
credId: Base64URLString,
issuerDid: string,

144
src/libs/endorserServer.ts

@ -1,19 +1,14 @@
import {
Axios,
AxiosRequestConfig,
AxiosResponse,
RawAxiosRequestHeaders,
} from "axios";
import { Axios, AxiosRequestConfig, AxiosResponse } from "axios";
import * as didJwt from "did-jwt";
import { LRUCache } from "lru-cache";
import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { DEFAULT_IMAGE_API_SERVER } from "@/constants/app";
import { Contact } from "@/db/tables/contacts";
import { accessToken, SimpleSigner } from "@/libs/crypto";
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";
// the object in RegisterAction claims
@ -160,7 +155,7 @@ export interface OfferVerifiableCredential {
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id7
export interface PlanVerifiableCredential {
"@context": SCHEMA_ORG_CONTEXT;
"@context": "https://schema.org";
"@type": "PlanAction";
name: string;
agent?: { identifier: string };
@ -453,28 +448,30 @@ export function didInfo(
return didInfoForContact(did, activeDid, contact, allMyDids).displayName;
}
async function getHeaders(identity: IIdentifier | null) {
const headers: RawAxiosRequestHeaders = {
export async function getHeaders(did?: string) {
const headers: { "Content-Type": string; Authorization?: string } = {
"Content-Type": "application/json",
};
if (identity) {
const token = await accessToken(identity);
if (did) {
const token = await accessToken(did);
headers["Authorization"] = "Bearer " + token;
} else {
// it's often OK to request without auth; we assume necessary checks are done earlier
}
return headers;
}
/**
* @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 apiServer
*/
export async function getPlanFromCache(
handleId: string | null,
identity: IIdentifier | null,
axios: Axios,
apiServer: string,
requesterDid?: string,
): Promise<PlanSummaryRecord | undefined> {
if (!handleId) {
return undefined;
@ -485,7 +482,7 @@ export async function getPlanFromCache(
apiServer +
"/api/v2/report/plans?handleId=" +
encodeURIComponent(handleId);
const headers = await getHeaders(identity);
const headers = await getHeaders(requesterDid);
try {
const resp = await axios.get(url, { headers });
if (resp.status === 200 && resp.data?.data?.length > 0) {
@ -518,6 +515,9 @@ export async function setPlanInCache(
planCache.set(handleId, planSummary);
}
/**
* Construct GiveAction VC for submission to server
*/
export function constructGive(
fromDid?: string | null,
toDid?: string,
@ -572,7 +572,7 @@ export function constructGive(
export async function createAndSubmitGive(
axios: Axios,
apiServer: string,
identity: IIdentifier,
issuerDid: string,
fromDid?: string | null,
toDid?: string,
description?: string,
@ -596,7 +596,7 @@ export async function createAndSubmitGive(
);
return createAndSubmitClaim(
vcClaim as GenericCredWrapper,
identity,
issuerDid,
apiServer,
axios,
);
@ -614,7 +614,7 @@ export async function createAndSubmitGive(
export async function createAndSubmitOffer(
axios: Axios,
apiServer: string,
identity: IIdentifier,
issuerDid: string,
description?: string,
amount?: number,
unitCode?: string,
@ -625,7 +625,7 @@ export async function createAndSubmitOffer(
const vcClaim: OfferVerifiableCredential = {
"@context": SCHEMA_ORG_CONTEXT,
"@type": "Offer",
offeredBy: { identifier: identity.did },
offeredBy: { identifier: issuerDid },
validThrough: expirationDate || undefined,
};
if (amount) {
@ -649,7 +649,7 @@ export async function createAndSubmitOffer(
}
return createAndSubmitClaim(
vcClaim as GenericCredWrapper,
identity,
issuerDid,
apiServer,
axios,
);
@ -657,7 +657,7 @@ export async function createAndSubmitOffer(
// similar logic is found in endorser-mobile
export const createAndSubmitConfirmation = async (
identifier: IIdentifier,
issuerDid: string,
claim: GenericVerifiableCredential,
lastClaimId: string, // used to set the lastClaimId
handleId: string | undefined,
@ -674,12 +674,12 @@ export const createAndSubmitConfirmation = async (
"@type": "AgreeAction",
object: goodClaim,
};
return createAndSubmitClaim(confirmationClaim, identifier, apiServer, axios);
return createAndSubmitClaim(confirmationClaim, issuerDid, apiServer, axios);
};
export async function createAndSubmitClaim(
vcClaim: GenericVerifiableCredential,
identity: IIdentifier,
issuerDid: string,
apiServer: string,
axios: Axios,
): Promise<CreateAndSubmitClaimResult> {
@ -692,34 +692,15 @@ export async function createAndSubmitClaim(
},
};
// Create a signature using private key of identity
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,
});
const vcJwt: string = await createEndorserJwt(issuerDid, vcPayload);
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = `${apiServer}/api/v2/claim`;
const token = await accessToken(identity);
const response = await axios.post(url, payload, {
headers: {
"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(
activeDid: string,
apiServer: string,
axios: Axios,
contact: Contact,
) {
const identity = await getIdentity(activeDid);
const vcClaim: RegisterVerifiableCredential = {
"@context": SCHEMA_ORG_CONTEXT,
"@type": "RegisterAction",
agent: { identifier: identity.did },
agent: { identifier: activeDid },
object: SERVICE_ID,
participant: { identifier: contact.did },
};
@ -968,26 +980,10 @@ export async function register(
},
};
// Create a signature using private key of identity
if (identity.keys[0].privateKeyHex == null) {
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,
});
const vcJwt = await createEndorserJwt(activeDid, vcPayload);
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = apiServer + "/api/v2/claim";
const headers = await getHeaders(identity);
const resp = await axios.post(url, payload, { headers });
const resp = await axios.post(url, { jwtEncoded: vcJwt });
if (resp.data?.success?.handleId) {
return { success: true };
} else if (resp.data?.success?.embeddedRecordError) {
@ -1017,7 +1013,7 @@ export async function setVisibilityUtil(
const url =
apiServer + "/api/report/" + (visibility ? "canSeeMe" : "cannotSeeMe");
const identity = await getIdentity(activeDid);
const headers = await getHeaders(identity);
const headers = await getHeaders(identity.did);
const payload = JSON.stringify({ did: contact.did });
try {
@ -1046,16 +1042,16 @@ export async function setVisibilityUtil(
*
* @param apiServer endorser server URL string
* @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.
*/
export async function fetchEndorserRateLimits(
apiServer: string,
axios: Axios,
identity: IIdentifier,
issuerDid: string,
) {
const url = `${apiServer}/api/report/rateLimits`;
const headers = await getHeaders(identity);
const headers = await getHeaders(issuerDid);
return await axios.get(url, { headers } as AxiosRequestConfig);
}
@ -1064,15 +1060,11 @@ export async function fetchEndorserRateLimits(
*
* @param apiServer image server URL string
* @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.
*/
export async function fetchImageRateLimits(
apiServer: string,
axios: Axios,
identity: IIdentifier,
) {
export async function fetchImageRateLimits(axios: Axios, issuerDid: string) {
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);
}

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 { GenericCredWrapper, containsHiddenDid } from "@/libs/endorserServer";
import * as serverUtil from "@/libs/endorserServer";
import { createPeerDid, registerCredential } from "@/libs/didPeer";
import { Buffer } from "buffer";
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 */
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();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.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");
if (!identity) {
@ -239,6 +249,38 @@ export const generateSaveAndActivateIdentity = async (): Promise<string> => {
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 (
subscriptionJSON: PushSubscriptionJSON,
skipFilter: boolean,

3
src/test/index.ts

@ -6,6 +6,9 @@ import { SERVICE_ID } from "../libs/endorserServer";
import { deriveAddress, newIdentifier } from "../libs/crypto";
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() {
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";

132
src/views/AccountViewView.vue

@ -359,6 +359,7 @@
<div class="text-slate-500 text-sm font-bold">Derivation Path</div>
<div
v-if="derivationPath"
class="text-sm text-slate-500 flex justify-start items-center mb-1"
>
<code class="truncate">{{ derivationPath }}</code>
@ -375,6 +376,12 @@
</button>
<span v-show="showDerCopy">Copied</span>
</div>
<div
v-else
class="text-sm text-slate-500 flex justify-start items-center mb-1"
>
(none)
</div>
</div>
<!-- id used by puppeteer test script -->
@ -646,13 +653,16 @@
<script lang="ts">
import { AxiosError } from "axios";
import { Buffer } from "buffer/";
import Dexie from "dexie";
import "dexie-export-import";
import { ImportProgress } from "dexie-export-import/dist/import";
import { IIdentifier } from "@veramo/core";
import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import EntityIcon from "@/components/EntityIcon.vue";
import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
@ -664,9 +674,9 @@ import {
NotificationIface,
} from "@/constants/app";
import { db, accountsDB } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core";
import {
ErrorResponse,
EndorserRateLimits,
@ -674,15 +684,7 @@ import {
fetchEndorserRateLimits,
fetchImageRateLimits,
} from "@/libs/endorserServer";
import { Buffer } from "buffer/";
import EntityIcon from "@/components/EntityIcon.vue";
interface IAccount {
did: string;
publicKeyHex: string;
privateHex?: string;
derivationPath: string;
}
import { getAccount } from "@/libs/util";
const inputImportFileNameRef = ref<Blob>();
@ -705,6 +707,7 @@ export default class AccountViewView extends Vue {
givenName = "";
hideRegisterPromptOnNewContact = false;
imageLimits: ImageRateLimits | null = null;
imageServer = "";
isRegistered = false;
isSubscribed = false;
limitsMessage = "";
@ -738,18 +741,9 @@ export default class AccountViewView extends Vue {
*/
async mounted() {
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
// Initialize component state with values from the database or defaults
this.initializeState(settings);
// Get and process the identity
const identity = await this.getIdentity(this.activeDid);
if (identity) {
this.processIdentity(identity);
}
await this.initializeState();
await this.processIdentity();
const registration = await navigator.serviceWorker.ready;
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.
* @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.apiServer = (settings?.apiServer as string) || "";
this.apiServerInput = (settings?.apiServer as string) || "";
@ -778,6 +775,7 @@ export default class AccountViewView extends Vue {
(settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.isRegistered = !!settings?.isRegistered;
this.imageServer = (settings?.imageServer as string) || "";
this.profileImageUrl = settings?.profileImageUrl as string;
this.showContactGives = !!settings?.showContactGivesInline;
this.hideRegisterPromptOnNewContact =
@ -790,49 +788,6 @@ export default class AccountViewView extends Vue {
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
doCopyTwoSecRedo(text: string, fn: () => void) {
fn();
@ -872,21 +827,19 @@ export default class AccountViewView extends Vue {
/**
* Processes the identity and updates the component's state.
* @param {IdentityType} identity - Object containing identity information.
*/
processIdentity(identity: IIdentifier) {
if (
identity &&
identity.keys &&
identity.keys.length > 0 &&
identity.keys[0].meta
) {
async processIdentity() {
const account: Account | undefined = await getAccount(this.activeDid);
if (account?.identity) {
const identity = JSON.parse(account.identity as string) as IIdentifier;
this.publicHex = identity.keys[0].publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
this.derivationPath = identity.keys[0].meta?.derivationPath as string;
this.checkLimitsFor(identity);
} else {
// Handle the case where any of these are null or undefined
this.checkLimitsFor(this.activeDid);
} else if (account?.publicKeyHex) {
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() {
const identity = await this.getIdentity(this.activeDid);
if (identity) {
this.checkLimitsFor(identity);
if (this.activeDid) {
this.checkLimitsFor(this.activeDid);
} else {
this.limitsMessage =
"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`.
*/
public async checkLimitsFor(identity: IIdentifier) {
public async checkLimitsFor(did: string) {
this.loadingLimits = true;
this.limitsMessage = "";
@ -1260,7 +1212,7 @@ export default class AccountViewView extends Vue {
const resp = await fetchEndorserRateLimits(
this.apiServer,
this.axios,
identity,
did,
);
if (resp.status === 200) {
this.endorserLimits = resp.data;
@ -1285,11 +1237,7 @@ export default class AccountViewView extends Vue {
);
}
}
const imageResp = await fetchImageRateLimits(
this.apiServer,
this.axios,
identity,
);
const imageResp = await fetchImageRateLimits(this.axios, did);
if (imageResp.status === 200) {
this.imageLimits = imageResp.data;
}
@ -1386,9 +1334,9 @@ export default class AccountViewView extends Vue {
*
* @param {AccountType} account - The account object.
*/
private updateActiveAccountProperties(account: IAccount) {
private updateActiveAccountProperties(account: Account) {
this.activeDid = account.did;
this.derivationPath = account.derivationPath;
this.derivationPath = account.derivationPath || "";
this.publicHex = account.publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
}
@ -1459,11 +1407,7 @@ export default class AccountViewView extends Vue {
return;
}
try {
const identity = await this.getIdentity(this.activeDid);
if (!identity) {
throw Error("No identity found.");
}
const token = await accessToken(identity);
const token = await accessToken(this.activeDid);
const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER +
"/image/" +

30
src/views/ClaimAddRawView.vue

@ -29,7 +29,6 @@
</template>
<script lang="ts">
import { RawAxiosRequestHeaders } from "axios";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator";
@ -37,7 +36,6 @@ import GiftedDialog from "@/components/GiftedDialog.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import * as serverUtil from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue";
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() {
const fullClaim = JSON.parse(this.claimStr);
const result = await serverUtil.createAndSubmitClaim(
fullClaim,
await this.getIdentity(this.activeDid),
this.activeDid,
this.apiServer,
this.axios,
);

58
src/views/ClaimView.vue

@ -407,7 +407,7 @@
</template>
<script lang="ts">
import { AxiosError, RawAxiosRequestHeaders } from "axios";
import { AxiosError } from "axios";
import * as yaml from "js-yaml";
import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
@ -419,7 +419,6 @@ import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import * as serverUtil from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
import QuickNav from "@/components/QuickNav.vue";
@ -432,7 +431,6 @@ import { GiverReceiverInputInfo } from "@/libs/endorserServer";
export default class ClaimView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
accountIdentityStr: string = "null";
activeDid = "";
allMyDids: Array<string> = [];
allContacts: Array<Contact> = [];
@ -485,15 +483,12 @@ export default class ClaimView extends Vue {
const accounts = accountsDB.accounts;
const accountsArr: Array<Account> = await accounts?.toArray();
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);
let claimId;
if (pathParam) {
claimId = decodeURIComponent(pathParam);
await this.loadClaim(claimId, identity);
await this.loadClaim(claimId, this.activeDid);
} else {
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?
didInfo(did: string) {
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)
? "/api/claim/byHandle/"
: "/api/claim/";
const url = this.apiServer + urlPath + encodeURIComponent(claimId);
const headers = await this.getHeaders(identity);
const headers = await serverUtil.getHeaders(userDid);
try {
const resp = await this.axios.get(url, { headers });
@ -601,7 +569,7 @@ export default class ClaimView extends Vue {
this.apiServer +
"/api/v2/report/gives?handleId=" +
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, {
headers: giveHeaders,
});
@ -615,7 +583,7 @@ export default class ClaimView extends Vue {
this.apiServer +
"/api/v2/report/offers?handleId=" +
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, {
headers: offerHeaders,
});
@ -631,7 +599,7 @@ export default class ClaimView extends Vue {
this.apiServer +
"/api/report/issuersWhoClaimedOrConfirmed?claimId=" +
encodeURIComponent(serverUtil.stripEndorserPrefix(claimId));
const confirmHeaders = await this.getHeaders(identity);
const confirmHeaders = await serverUtil.getHeaders(userDid);
const response = await this.axios.get(confirmUrl, {
headers: confirmHeaders,
});
@ -673,15 +641,9 @@ export default class ClaimView extends Vue {
}
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 =
this.apiServer + "/api/claim/full/" + encodeURIComponent(claimId);
const headers = await this.getHeaders(identity);
const headers = await serverUtil.getHeaders(this.activeDid);
try {
const resp = await this.axios.get(url, { headers });
@ -760,7 +722,7 @@ export default class ClaimView extends Vue {
};
const result = await serverUtil.createAndSubmitClaim(
confirmationClaim,
await this.getIdentity(this.activeDid),
this.activeDid,
this.apiServer,
this.axios,
);
@ -794,7 +756,7 @@ export default class ClaimView extends Vue {
};
this.$router.push(route).then(async () => {
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>
<script lang="ts">
import { AxiosError, RawAxiosRequestHeaders } from "axios";
import { AxiosError } from "axios";
import * as yaml from "js-yaml";
import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
@ -407,7 +407,6 @@ import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import * as serverUtil from "@/libs/endorserServer";
import { displayAmount, GiverReceiverInputInfo } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
@ -420,7 +419,6 @@ import { isGiveAction } from "@/libs/util";
export default class ClaimView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
accountIdentityStr: string = "null";
activeDid = "";
allMyDids: Array<string> = [];
allContacts: Array<Contact> = [];
@ -471,9 +469,6 @@ export default class ClaimView extends Vue {
const accounts = accountsDB.accounts;
const accountsArr: Array<Account> = await accounts?.toArray();
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(
"/confirm-gift/".length,
@ -481,7 +476,7 @@ export default class ClaimView extends Vue {
let claimId;
if (pathParam) {
claimId = decodeURIComponent(pathParam);
await this.loadClaim(claimId, identity);
await this.loadClaim(claimId, this.activeDid);
} else {
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?
didInfo(did: string | undefined) {
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)
? "/api/claim/byHandle/"
: "/api/claim/";
const url = this.apiServer + urlPath + encodeURIComponent(claimId);
try {
const headers = await this.getHeaders(identity);
const headers = await serverUtil.getHeaders(userDid);
const resp = await this.axios.get(url, { headers });
// resp.data is:
// - a Jwt from https://api.endorser.ch/api-docs/
@ -614,7 +582,7 @@ export default class ClaimView extends Vue {
this.apiServer +
"/api/v2/report/gives?handleId=" +
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, {
headers: giveHeaders,
});
@ -685,7 +653,7 @@ export default class ClaimView extends Vue {
this.apiServer +
"/api/report/issuersWhoClaimedOrConfirmed?claimId=" +
encodeURIComponent(serverUtil.stripEndorserPrefix(claimId));
const confirmHeaders = await this.getHeaders(identity);
const confirmHeaders = await serverUtil.getHeaders(userDid);
const response = await this.axios.get(confirmUrl, {
headers: confirmHeaders,
});
@ -760,7 +728,7 @@ export default class ClaimView extends Vue {
};
const result = await serverUtil.createAndSubmitClaim(
confirmationClaim,
await this.getIdentity(this.activeDid),
this.activeDid,
this.apiServer,
this.axios,
);
@ -794,7 +762,7 @@ export default class ClaimView extends Vue {
};
this.$router.push(route).then(async () => {
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">
import { AxiosError } from "axios";
import * as didJwt from "did-jwt";
import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
@ -116,14 +114,17 @@ import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import { accessToken } from "@/libs/crypto";
import {
AgreeVerifiableCredential,
createEndorserJwtVcFromClaim,
displayAmount,
getHeaders,
GiveSummaryRecord,
GiveVerifiableCredential,
SCHEMA_ORG_CONTEXT,
} from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
@Component({ components: { QuickNav } })
export default class ContactAmountssView extends Vue {
@ -142,31 +143,6 @@ export default class ContactAmountssView extends Vue {
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() {
try {
await db.open();
@ -174,8 +150,8 @@ export default class ContactAmountssView extends Vue {
this.contact = (await db.contacts.get(contactDid)) || null;
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || "";
if (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) {
try {
const identity = await this.getIdentity(this.activeDid);
const identity = await libsUtil.getIdentity(this.activeDid);
let result: Array<GiveSummaryRecord> = [];
const url =
this.apiServer +
@ -207,7 +183,7 @@ export default class ContactAmountssView extends Vue {
encodeURIComponent(identity.did) +
"&recipientDid=" +
encodeURIComponent(contact.did);
const headers = await this.getHeaders(identity);
const headers = await getHeaders(activeDid);
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
result = resp.data.data;
@ -234,7 +210,7 @@ export default class ContactAmountssView extends Vue {
encodeURIComponent(contact.did) +
"&recipientDid=" +
encodeURIComponent(identity.did);
const headers2 = await this.getHeaders(identity);
const headers2 = await getHeaders(activeDid);
const resp2 = await this.axios.get(url2, { headers: headers2 });
if (resp2.status === 200) {
result = R.concat(result, resp2.data.data);
@ -289,33 +265,15 @@ export default class ContactAmountssView extends Vue {
object: origClaim,
};
// Make a payload for the claim
const vcPayload = {
vc: {
"@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,
});
const vcJwt: string = await createEndorserJwtVcFromClaim(
this.activeDid,
vcClaim,
);
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = this.apiServer + "/api/v2/claim";
const token = await accessToken(identity);
const token = await accessToken(this.activeDid);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
@ -324,7 +282,8 @@ export default class ContactAmountssView extends Vue {
try {
const resp = await this.axios.post(url, payload, { headers });
if (resp.data?.success) {
record.amountConfirmed = origClaim.object?.amountOfThisGood || 1;
record.amountConfirmed =
(origClaim.object?.amountOfThisGood as number) || 1;
}
} catch (error) {
let userMessage = "There was an error. See logs for more info.";
@ -350,7 +309,6 @@ export default class ContactAmountssView extends Vue {
);
}
}
}
cannotConfirmMessage() {
this.$notify(

31
src/views/ContactGiftingView.vue

@ -72,17 +72,15 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { IIdentifier } from "@veramo/core";
import GiftedDialog from "@/components/GiftedDialog.vue";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import { NotificationIface } from "@/constants/app";
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 { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { GiverReceiverInputInfo } from "@/libs/endorserServer";
@Component({
@ -134,32 +132,7 @@ export default class ContactGiftingView extends Vue {
}
}
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");
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) {
openDialog(giver?: GiverReceiverInputInfo) {
const recipient = this.projectId
? undefined
: { did: this.activeDid, name: "you" };

44
src/views/ContactQRScanShowView.vue

@ -24,6 +24,7 @@
>
<span class="text-red">Beware!</span>
You aren't sharing your name, so quickly
<br />
<router-link
: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"
@ -72,7 +73,7 @@
<script lang="ts">
import { AxiosError } from "axios";
import * as didJwt from "did-jwt";
import { Buffer } from "buffer/";
import { sha256 } from "ethereum-cryptography/sha256.js";
import QRCodeVue3 from "qr-code-generator-vue3";
import * as R from "ramda";
@ -90,17 +91,16 @@ import {
deriveAddress,
getContactPayloadFromJwtUrl,
nextDerivationPath,
SimpleSigner,
} from "@/libs/crypto";
import {
CONTACT_URL_PREFIX,
createEndorserJwt,
ENDORSER_JWT_URL_LOCATION,
isDid,
register,
setVisibilityUtil,
} from "@/libs/endorserServer";
import { Buffer } from "buffer/";
import * as libsUtil from "@/libs/util";
@Component({
components: {
@ -133,12 +133,15 @@ export default class ContactQRScanShow extends Vue {
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
if (account) {
const identity = await this.getIdentity(this.activeDid);
const identity = await libsUtil.getIdentity(this.activeDid);
const publicKeyHex = identity.keys[0].publicKeyHex;
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
const newDerivPath = nextDerivationPath(account.derivationPath);
const nextPublicHex = deriveAddress(account.mnemonic, newDerivPath)[2];
const newDerivPath = nextDerivationPath(account.derivationPath as string);
const nextPublicHex = deriveAddress(
account.mnemonic as string,
newDerivPath,
)[2];
const nextPublicEncKey = Buffer.from(nextPublicHex, "hex");
const nextPublicEncKeyHash = sha256(nextPublicEncKey);
const nextPublicEncKeyHashBase64 =
@ -158,15 +161,7 @@ export default class ContactQRScanShow extends Vue {
},
};
const alg = undefined;
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 vcJwt: string = await createEndorserJwt(identity.did, contactInfo);
const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION;
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

54
src/views/ContactsView.vue

@ -303,20 +303,20 @@
import { AxiosError } from "axios";
import { IndexableType } from "dexie";
import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { AppString, NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
import { db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken, getContactPayloadFromJwtUrl } from "@/libs/crypto";
import { getContactPayloadFromJwtUrl } from "@/libs/crypto";
import {
CONTACT_CSV_HEADER,
CONTACT_URL_PREFIX,
GiverReceiverInputInfo,
GiveSummaryRecord,
getHeaders,
isDid,
register,
setVisibilityUtil,
@ -326,7 +326,6 @@ import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import GiftedDialog from "@/components/GiftedDialog.vue";
import OfferDialog from "@/components/OfferDialog.vue";
import { Account } from "@/db/tables/accounts";
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() {
if (!this.activeDid) {
return;
@ -481,7 +450,7 @@ export default class ContactsView extends Vue {
};
try {
const { headers } = await this.getHeadersAndIdentity(this.activeDid);
const headers = await getHeaders(this.activeDid);
const givenByUrl =
this.apiServer +
"/api/v2/report/gives?agentDid=" +
@ -954,8 +923,19 @@ export default class ContactsView extends Vue {
this.apiServer +
"/api/report/canDidExplicitlySeeMe?did=" +
encodeURIComponent(contact.did);
const identity = await this.getIdentity(this.activeDid);
const headers = await this.getHeaders(identity);
const headers = await getHeaders(this.activeDid);
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 {
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 { Contact } from "@/db/tables/contacts";
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import {
capitalizeAndInsertSpacesBeforeCaps,
didInfoForContact,
displayAmount,
getHeaders,
GenericCredWrapper,
GenericVerifiableCredential,
GiveVerifiableCredential,
@ -203,30 +203,6 @@ export default class DIDView extends Vue {
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
* @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,
{
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 { Contact } from "@/db/tables/contacts";
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { didInfo, PlanData } from "@/libs/endorserServer";
import { didInfo, getHeaders, PlanData } from "@/libs/endorserServer";
@Component({
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) {
this.resetCounts();
@ -247,7 +222,7 @@ export default class DiscoverView extends Vue {
this.apiServer + "/api/v2/report/plans?" + queryParams,
{
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,
{
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 TopMessage from "@/components/TopMessage.vue";
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 {
constructGive,
createAndSubmitGive, didInfo,
createAndSubmitGive,
didInfo,
getPlanFromCache,
} from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
import { accessToken } from "@/libs/crypto";
import {Contact} from "@/db/tables/contacts";
import { Contact } from "@/db/tables/contacts";
@Component({
components: {
@ -315,9 +316,9 @@ export default class GiftedDetails extends Vue {
const identity = await libsUtil.getIdentity(this.activeDid);
const project = await getPlanFromCache(
this.projectId,
identity,
this.axios,
this.apiServer,
identity.did,
);
this.projectName = project?.name
? "the project: " + project.name
@ -380,8 +381,7 @@ export default class GiftedDetails extends Vue {
return;
}
try {
const identity = await libsUtil.getIdentity(this.activeDid);
const token = await accessToken(identity);
const token = await accessToken(this.activeDid);
const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER +
"/image/" +
@ -549,7 +549,6 @@ export default class GiftedDetails extends Vue {
*/
public async recordGive() {
try {
const identity = await libsUtil.getIdentity(this.activeDid);
const recipientDid = this.givenToRecipient
? this.recipientDid
: undefined;
@ -557,7 +556,7 @@ export default class GiftedDetails extends Vue {
const result = await createAndSubmitGive(
this.axios,
this.apiServer,
identity,
this.activeDid,
this.giverDid,
recipientDid,
this.description,

118
src/views/HomeView.vue

@ -5,7 +5,7 @@
<!-- CONTENT -->
<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">
Time Safari
{{ AppString.APP_NAME }}
</h1>
<!-- prompt to install notifications -->
@ -79,27 +79,37 @@
<!-- !isCreatingIdentifier -->
<div
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">
Want to connect with your contacts, or share contributions or
projects?
To recognize giving, have someone register you:
</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
: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"
>
Create An Identifier
Share your contact info.
</router-link>
</div>
</div>
<div v-else class="mb-4">
<!-- activeDid -->
<div
v-else-if="!isRegistered"
v-if="!isRegistered"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
>
<!-- activeDid && !isRegistered -->
Someone must register you before you can give kudos or make offers or
create projects... basically before doing anything.
Someone must register you before you can give kudos or make offers
or create projects... basically before doing anything.
<router-link
: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"
@ -166,6 +176,7 @@
</div>
</div>
</div>
</div>
<GiftedDialog ref="customDialog" />
<GiftedPrompts ref="giftedPrompts" />
@ -305,10 +316,10 @@
<script lang="ts">
import { UAParser } from "ua-parser-js";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import App from "../App.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import GiftedDialog from "@/components/GiftedDialog.vue";
import GiftedPrompts from "@/components/GiftedPrompts.vue";
@ -316,9 +327,8 @@ import FeedFilters from "@/components/FeedFilters.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.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 { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import {
BoundingBox,
@ -326,17 +336,17 @@ import {
MASTER_SETTINGS_KEY,
Settings,
} from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import {
contactForDid,
containsNonHiddenDid,
didInfoForContact,
fetchEndorserRateLimits,
getHeaders,
getPlanFromCache,
GiverReceiverInputInfo,
GiveSummaryRecord,
} from "@/libs/endorserServer";
import { generateSaveAndActivateIdentity } from "@/libs/util";
import { registerSaveAndActivatePasskey } from "@/libs/util";
interface GiveRecordWithContactInfo extends GiveSummaryRecord {
giver: {
@ -354,6 +364,11 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord {
}
@Component({
computed: {
App() {
return App;
},
},
components: {
GiftedDialog,
GiftedPrompts,
@ -367,6 +382,8 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord {
export default class HomeView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
AppString = AppString;
activeDid = "";
allContacts: Array<Contact> = [];
allMyDids: Array<string> = [];
@ -374,6 +391,7 @@ export default class HomeView extends Vue {
feedData: GiveRecordWithContactInfo[] = [];
feedPreviousOldestId?: string;
feedLastViewedClaimId?: string;
givenName = "";
isAnyFeedFilterOn: boolean;
isCreatingIdentifier = false;
isFeedFilteredByVisible = false;
@ -387,25 +405,6 @@ export default class HomeView extends Vue {
showShortcutBvc = false;
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() {
try {
await accountsDB.open();
@ -418,6 +417,7 @@ export default class HomeView extends Vue {
this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray();
this.feedLastViewedClaimId = settings?.lastViewedClaimId;
this.givenName = settings?.firstName || "";
this.isFeedFilteredByVisible = !!settings?.filterFeedByVisible;
this.isFeedFilteredByNearby = !!settings?.filterFeedByNearby;
this.isRegistered = !!settings?.isRegistered;
@ -426,21 +426,13 @@ export default class HomeView extends Vue {
this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings);
if (this.allMyDids.length === 0) {
this.isCreatingIdentifier = true;
this.activeDid = await generateSaveAndActivateIdentity();
this.allMyDids = [this.activeDid];
this.isCreatingIdentifier = false;
}
// someone may have have registered after sharing contact info
// someone may have have registered after sharing contact info, so recheck
if (!this.isRegistered && this.activeDid) {
const identity = await this.getIdentity(this.activeDid);
try {
const resp = await fetchEndorserRateLimits(
this.apiServer,
this.axios,
identity as IIdentifier,
this.activeDid,
);
if (resp.status === 200) {
// 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() {
return this.isFeedFilteredByVisible || this.isFeedFilteredByNearby;
}
@ -483,26 +484,6 @@ export default class HomeView extends Vue {
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
async reloadFeedOnChange() {
await db.open();
@ -520,7 +501,7 @@ export default class HomeView extends Vue {
* Data loader used by infinite scroller
* @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
// and the InfiniteScroll component triggers a load before finished.
// 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;
let endOfResults = true;
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId)
@ -550,7 +531,6 @@ export default class HomeView extends Vue {
if (results.data.length > 0) {
endOfResults = false;
// include the descriptions of the giver and receiver
const identity = await this.getIdentity(this.activeDid);
for (const record: GiveSummaryRecord of results.data) {
// similar code is in endorser-mobile utility.ts
// 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.
const plan = await getPlanFromCache(
record.fulfillsPlanHandleId,
identity,
this.axios,
this.apiServer,
this.activeDid,
);
// 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
* @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 response = await fetch(
endorserApiServer +
@ -658,7 +638,7 @@ export default class HomeView extends Vue {
beforeQuery,
{
method: "GET",
headers: await this.buildHeaders(),
headers: await getHeaders(this.activeDid),
},
);

80
src/views/NewEditProjectView.vue

@ -174,21 +174,23 @@
<script lang="ts">
import "leaflet/dist/leaflet.css";
import { AxiosError } from "axios";
import * as didJwt from "did-jwt";
import { DateTime } from "luxon";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
import QuickNav from "@/components/QuickNav.vue";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
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 { useAppStore } from "@/store/app";
import { PlanVerifiableCredential } from "@/libs/endorserServer";
import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
@Component({
components: { ImageMethodDialog, LMap, LMarker, LTileLayer, QuickNav },
@ -229,31 +231,6 @@ export default class NewEditProjectView extends Vue {
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() {
await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count();
@ -267,23 +244,17 @@ export default class NewEditProjectView extends Vue {
if (this.numAccounts === 0) {
this.errNote("There was a problem loading your account info.");
} else {
const identity = await this.getIdentity(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);
this.loadProject(this.activeDid);
}
}
}
async loadProject(identity: IIdentifier) {
async loadProject(userDid: string) {
const url =
this.apiServer +
"/api/claim/byHandle/" +
encodeURIComponent(this.projectId);
const token = await accessToken(identity);
const token = await accessToken(userDid);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
@ -342,8 +313,7 @@ export default class NewEditProjectView extends Vue {
return;
}
try {
const identity = await libsUtil.getIdentity(this.activeDid);
const token = await accessToken(identity);
const token = await accessToken(this.activeDid);
const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER +
"/image/" +
@ -446,31 +416,13 @@ export default class NewEditProjectView extends Vue {
} else {
delete vcClaim.startTime;
}
// Make a payload for the claim
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,
});
const vcJwt = await createEndorserJwtVcFromClaim(identity.did, vcClaim);
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = this.apiServer + "/api/v2/claim";
const token = await accessToken(identity);
const token = await accessToken(identity.did);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
@ -533,10 +485,7 @@ export default class NewEditProjectView extends Vue {
);
}
} else {
console.error(
"Here's the full error trying to save the claim:",
error,
);
console.error("Here's the full error trying to save the claim:", error);
this.$notify(
{
group: "alert",
@ -551,7 +500,6 @@ export default class NewEditProjectView extends Vue {
this.errorMessage = userMessage;
}
}
}
public async onSaveProjectClick() {
this.isHiddenSave = true;
@ -560,7 +508,7 @@ export default class NewEditProjectView extends Vue {
if (this.numAccounts === 0) {
console.error("Error: there is no account.");
} else {
const identity = await this.getIdentity(this.activeDid);
const identity = await libsUtil.getIdentity(this.activeDid);
this.saveProject(identity);
}
}

76
src/views/ProjectViewView.vue

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

22
src/views/ProjectsView.vue

@ -281,7 +281,7 @@ export default class ProjectsView extends Vue {
console.error("No accounts found.");
this.errNote("You need an identifier to load your projects.");
} else {
this.currentIid = await this.getIdentity(activeDid);
this.currentIid = await libsUtil.getIdentity(activeDid);
await this.loadOffers();
}
} catch (err) {
@ -356,26 +356,10 @@ export default class ProjectsView extends Vue {
async loadProjects(identifier?: IIdentifier, urlExtra: string = "") {
const identity = identifier || this.currentIid;
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);
}
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
* @param id of the project
@ -474,7 +458,7 @@ export default class ProjectsView extends Vue {
async loadOffers(identifier?: IIdentifier, urlExtra: string = "") {
const identity = identifier || this.currentIid;
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);
}

5
src/views/QuickActionBvcBeginView.vue

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

25
src/views/QuickActionBvcEndView.vue

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

4
src/views/SharedPhotoView.vue

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

73
src/views/StartView.vue

@ -17,7 +17,7 @@
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Start Here
Generate an Identity
</h1>
</div>
@ -25,33 +25,57 @@
<div id="start-question" class="mt-8">
<div class="max-w-3xl mx-auto">
<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 class="text-center font-light">
If you haven't used this before, click "Yes" to generate a new
identifier.
<p class="text-center font-light mt-6">
A <strong>passkey</strong> is easy to manage, though it is less
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 class="text-center mb-4 font-light">
Only click "No" if you have a seed of 12 or 24 words generated
elsewhere.
<p class="text-center font-light mt-4">
A <strong>new seed</strong> allows you full control over the keys,
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>
<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
@click="onClickYes()"
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"
@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 cursor-pointer"
>
Yes, generate one
Generate one with a new seed
</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
@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
v-if="numAccounts > 0"
@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
</a>
@ -64,23 +88,38 @@
<script lang="ts">
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({
components: {},
})
export default class StartView extends Vue {
givenName = "";
numAccounts = 0;
async mounted() {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.givenName = settings?.firstName || "";
await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count();
}
public onClickYes() {
public onClickNewSeed() {
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() {
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 QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app";
import { AppString, NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
import {
createPeerDid,
PeerSetup,
registerCredential,
verifyJwtP256,
verifyJwtSimplewebauthn,
verifyJwtWebCrypto,
} from "@/libs/didPeer";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { registerAndSavePasskey } from "@/libs/util";
const inputFileNameRef = ref<Blob>();
@ -333,14 +332,14 @@ export default class Help extends Vue {
}
public async register() {
const DEFAULT_USERNAME = "Time Safari Tester";
const DEFAULT_USERNAME = AppString.APP_NAME + " Tester";
if (!this.userName) {
this.$notify(
{
group: "modal",
type: "confirm",
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 () => {
this.userName = DEFAULT_USERNAME;
},
@ -353,18 +352,11 @@ export default class Help extends Vue {
);
return;
}
const cred = await registerCredential("Time Safari - " + this.userName);
const publicKeyBytes = cred.publicKeyBytes;
this.activeDid = createPeerDid(publicKeyBytes as Uint8Array);
this.credIdHex = cred.credIdHex as string;
await accountsDB.open();
await accountsDB.accounts.add({
dateCreated: new Date().toISOString(),
did: this.activeDid,
passkeyCredIdHex: this.credIdHex,
publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"),
});
const account = await registerAndSavePasskey(
AppString.APP_NAME + " - " + this.userName,
);
this.activeDid = account.did;
this.credIdHex = account.passkeyCredIdHex;
}
public async createJwtSimplewebauthn() {

Loading…
Cancel
Save