From b18e5548862de7a425bd00193ad90d842787a803 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 8 Dec 2023 14:10:01 -0700 Subject: [PATCH] add ability to confirm a claim --- project.task.yaml | 3 +- src/libs/endorserServer.ts | 112 +++++++++++++++++++++++++++++++--- src/views/AccountViewView.vue | 6 +- src/views/ClaimView.vue | 89 ++++++++++++++++++++++----- src/views/ContactsView.vue | 2 +- 5 files changed, 184 insertions(+), 28 deletions(-) diff --git a/project.task.yaml b/project.task.yaml index 40ff1fb1..43569bb0 100644 --- a/project.task.yaml +++ b/project.task.yaml @@ -17,7 +17,7 @@ tasks: - .5 If notifications are not enabled, add message to front page with link/button to enable - show VC details... somehow: - - .5 make a VC details page, or link to endorser.ch (including confirmations) + - 01 show my VCs - most interesting, or via search - 01 allow download of each VC (& confirmations, to show that they actually own their data) - 04 allow user to download VCs, mine + ones I can see about me from others - add VC confirmation? @@ -33,6 +33,7 @@ tasks: - Deploy to a server. - Ensure public server has limits that work for group adoption. - Test PWA features on Android and iOS. + - Other features - donation vs give, show offers, show give & outstanding totals, show network view, restrict registration, connect to contacts blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time - make identicons for contacts into more-memorable faces (and maybe change project identicons, too) diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index 167f2113..f80f88d6 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -40,20 +40,22 @@ export interface ClaimResult { error: { code: string; message: string }; } -export interface GenericClaim { +export interface GenericVerifiableCredential { "@context": string; "@type": string; - issuedAt: string; - issuer: string; - // "any" because arbitrary objects can be subject of agreement +} + +export interface GenericServerRecord extends GenericVerifiableCredential { + handleId?: string; + id?: string; + issuedAt?: string; + issuer?: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any claim: Record; } -export const BLANK_GENERIC_CLAIM: GenericClaim = { +export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = { "@context": SCHEMA_ORG_CONTEXT, "@type": "", - issuedAt: "", - issuer: "", claim: {}, }; @@ -153,6 +155,42 @@ export function isHiddenDid(did: string) { return did === HIDDEN_DID; } +/** + * @return true for any nested string where func(input) === true + * + * Similar logic is found in endorser-mobile. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function testRecursivelyOnString(func: (arg0: any) => boolean, input: any) { + if (Object.prototype.toString.call(input) === "[object String]") { + return func(input); + } else if (input instanceof Object) { + if (!Array.isArray(input)) { + // it's an object + for (const key in input) { + if (testRecursivelyOnString(func, input[key])) { + return true; + } + } + } else { + // it's an array + for (const value of input) { + if (testRecursivelyOnString(func, value)) { + return true; + } + } + } + return false; + } else { + return false; + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function containsHiddenDid(obj: any) { + return testRecursivelyOnString(isHiddenDid, obj); +} + export function stripEndorserPrefix(claimId: string) { if (claimId && claimId.startsWith(ENDORSER_CH_HANDLE_PREFIX)) { return claimId.substring(ENDORSER_CH_HANDLE_PREFIX.length); @@ -161,6 +199,60 @@ export function stripEndorserPrefix(claimId: string) { } } +// similar logic is found in endorser-mobile +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function removeSchemaContext(obj: any) { + return obj["@context"] === SCHEMA_ORG_CONTEXT + ? R.omit(["@context"], obj) + : obj; +} + +// similar logic is found in endorser-mobile +export function addLastClaimOrHandleAsIdIfMissing( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + obj: any, + lastClaimId?: string, + handleId?: string, +) { + if (!obj.identifier && lastClaimId) { + const result = R.clone(obj); + result.lastClaimId = lastClaimId; + return result; + } else if (!obj.identifier && handleId) { + const result = R.clone(obj); + result.identifier = handleId; + return result; + } else { + return obj; + } +} + +// return clone of object without any nested *VisibleToDids keys +// similar logic is found in endorser-mobile +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function removeVisibleToDids(input: any): any { + if (input instanceof Object) { + if (!Array.isArray(input)) { + // it's an object + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result: Record = {}; + for (const key in input) { + if (!key.endsWith("VisibleToDids")) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + result[key] = removeVisibleToDids(R.clone(input[key])); + } + } + return result; + } else { + // it's an array + return R.map(removeVisibleToDids, input); + } + return false; + } else { + return input; + } +} + /** always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY @@ -232,7 +324,7 @@ export async function createAndSubmitGive( : undefined, }; return createAndSubmitClaim( - vcClaim as GenericClaim, + vcClaim as GenericServerRecord, identity, apiServer, axios, @@ -280,7 +372,7 @@ export async function createAndSubmitOffer( }; } return createAndSubmitClaim( - vcClaim as GenericClaim, + vcClaim as GenericServerRecord, identity, apiServer, axios, @@ -288,7 +380,7 @@ export async function createAndSubmitOffer( } export async function createAndSubmitClaim( - vcClaim: GenericClaim, + vcClaim: GenericVerifiableCredential, identity: IIdentifier, apiServer: string, axios: Axios, diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index cdd8d1e6..e07f9f4e 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -202,8 +202,12 @@ > Advanced -
+

+ Beware: the features here can be confusing and even change data in ways + you do not expect. But we support your freedoms! +

+

Deep Identity Details diff --git a/src/views/ClaimView.vue b/src/views/ClaimView.vue index 5dcfa6a1..42e6d35a 100644 --- a/src/views/ClaimView.vue +++ b/src/views/ClaimView.vue @@ -115,6 +115,24 @@

+ +
+
+ You have confirmed this claim. +
+
+ You cannot confirm this claim because it contains a DID that is hidden + from you. +
+
+ +
+
@@ -164,12 +182,7 @@ 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 { - BLANK_GENERIC_CLAIM, - didInfo, - isHiddenDid, - stripEndorserPrefix, -} from "@/libs/endorserServer"; +import * as serverUtil from "@/libs/endorserServer"; import QuickNav from "@/components/QuickNav.vue"; import EntityIcon from "@/components/EntityIcon.vue"; import { Account } from "@/db/tables/accounts"; @@ -197,9 +210,10 @@ export default class ClaimView extends Vue { fullClaim = null; fullClaimMessage = ""; numConfsNotVisible = 0; // number of hidden DIDs in the confirmerIdList, minus the issuer if they aren't visible - veriClaim = BLANK_GENERIC_CLAIM; + veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD; util = util; + containsHiddenDid = serverUtil.containsHiddenDid; async created() { await db.open(); @@ -241,7 +255,7 @@ export default class ClaimView extends Vue { ); } - public async getIdentity(activeDid: string) { + public async getIdentity(activeDid: string): Promise { await accountsDB.open(); const account = (await accountsDB.accounts .where("did") @@ -275,7 +289,7 @@ export default class ClaimView extends Vue { dids: Array, contacts: Array, ) { - return didInfo(did, activeDid, dids, contacts); + return serverUtil.didInfo(did, activeDid, dids, contacts); } async loadClaim(claimId: string, identity: IIdentifier) { @@ -317,21 +331,19 @@ export default class ClaimView extends Vue { const confirmUrl = this.apiServer + "/api/report/issuersWhoClaimedOrConfirmed?claimId=" + - encodeURIComponent(stripEndorserPrefix(claimId)); + encodeURIComponent(serverUtil.stripEndorserPrefix(claimId)); const confirmHeaders = await this.getHeaders(identity); try { const response = await this.axios.get(confirmUrl, { headers: confirmHeaders, }); if (response.status === 200) { - console.log("response:", response); const resultList1 = response.data.result || []; - const resultList2 = R.reject(isHiddenDid, resultList1); + const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1); const resultList3 = R.reject( (did: string) => did === this.veriClaim.issuer, resultList2, ); - console.log("all result lists:", resultList1, resultList2, resultList3); this.confirmerIdList = resultList3; this.numConfsNotVisible = resultList1.length - resultList2.length; if (resultList3.length === resultList2.length) { @@ -351,8 +363,6 @@ export default class ClaimView extends Vue { this.confsVisibleErrorMessage = "Had problems retrieving confirmations. See logs for more info."; } - console.log("confirmerIdList:", this.confirmerIdList); - console.log("confsVisibleToIdList:", this.confsVisibleToIdList); } async showFullClaim(claimId: string) { @@ -407,5 +417,54 @@ export default class ClaimView extends Vue { } } } + + async confirmClaim() { + // similar logic is found in endorser-mobile + const goodClaim = serverUtil.removeSchemaContext( + serverUtil.removeVisibleToDids( + serverUtil.addLastClaimOrHandleAsIdIfMissing( + this.veriClaim.claim, + this.veriClaim.id, + this.veriClaim.handleId, + ), + ), + ); + const confirmationClaim: serverUtil.GenericVerifiableCredential & { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + object: any; + } = { + "@context": "https://schema.org", + "@type": "AgreeAction", + object: goodClaim, + }; + const result = await serverUtil.createAndSubmitClaim( + confirmationClaim, + await this.getIdentity(this.activeDid), + this.apiServer, + this.axios, + ); + if (result.type === "success") { + this.$notify( + { + group: "alert", + type: "success", + title: "Success", + text: "Confirmation submitted.", + }, + 5000, + ); + } else { + console.log("Got error submitting the confirmation:", result); + this.$notify( + { + group: "alert", + type: "danger", + title: "Error", + text: "There was a problem submitting the confirmation. See logs for more info.", + }, + -1, + ); + } + } } diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index cd9db39e..01df405b 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -335,7 +335,7 @@ export default class ContactsView extends Vue { } } - public async getIdentity(activeDid: string) { + public async getIdentity(activeDid: string): Promise { await accountsDB.open(); const accounts = await accountsDB.accounts.toArray(); const account = R.find((acc) => acc.did === activeDid, accounts) as Account;