Browse Source

add ability to confirm a claim

kb/add-usage-guide
Trent Larson 12 months ago
parent
commit
b18e554886
  1. 3
      project.task.yaml
  2. 112
      src/libs/endorserServer.ts
  3. 6
      src/views/AccountViewView.vue
  4. 89
      src/views/ClaimView.vue
  5. 2
      src/views/ContactsView.vue

3
project.task.yaml

@ -17,7 +17,7 @@ tasks:
- .5 If notifications are not enabled, add message to front page with link/button to enable - .5 If notifications are not enabled, add message to front page with link/button to enable
- show VC details... somehow: - 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) - 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 - 04 allow user to download VCs, mine + ones I can see about me from others
- add VC confirmation? - add VC confirmation?
@ -33,6 +33,7 @@ tasks:
- Deploy to a server. - Deploy to a server.
- Ensure public server has limits that work for group adoption. - Ensure public server has limits that work for group adoption.
- Test PWA features on Android and iOS. - 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 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) - make identicons for contacts into more-memorable faces (and maybe change project identicons, too)

112
src/libs/endorserServer.ts

@ -40,20 +40,22 @@ export interface ClaimResult {
error: { code: string; message: string }; error: { code: string; message: string };
} }
export interface GenericClaim { export interface GenericVerifiableCredential {
"@context": string; "@context": string;
"@type": 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 // eslint-disable-next-line @typescript-eslint/no-explicit-any
claim: Record<any, any>; claim: Record<any, any>;
} }
export const BLANK_GENERIC_CLAIM: GenericClaim = { export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = {
"@context": SCHEMA_ORG_CONTEXT, "@context": SCHEMA_ORG_CONTEXT,
"@type": "", "@type": "",
issuedAt: "",
issuer: "",
claim: {}, claim: {},
}; };
@ -153,6 +155,42 @@ export function isHiddenDid(did: string) {
return did === HIDDEN_DID; 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) { export function stripEndorserPrefix(claimId: string) {
if (claimId && claimId.startsWith(ENDORSER_CH_HANDLE_PREFIX)) { if (claimId && claimId.startsWith(ENDORSER_CH_HANDLE_PREFIX)) {
return claimId.substring(ENDORSER_CH_HANDLE_PREFIX.length); 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<string, any> = {};
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 always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
@ -232,7 +324,7 @@ export async function createAndSubmitGive(
: undefined, : undefined,
}; };
return createAndSubmitClaim( return createAndSubmitClaim(
vcClaim as GenericClaim, vcClaim as GenericServerRecord,
identity, identity,
apiServer, apiServer,
axios, axios,
@ -280,7 +372,7 @@ export async function createAndSubmitOffer(
}; };
} }
return createAndSubmitClaim( return createAndSubmitClaim(
vcClaim as GenericClaim, vcClaim as GenericServerRecord,
identity, identity,
apiServer, apiServer,
axios, axios,
@ -288,7 +380,7 @@ export async function createAndSubmitOffer(
} }
export async function createAndSubmitClaim( export async function createAndSubmitClaim(
vcClaim: GenericClaim, vcClaim: GenericVerifiableCredential,
identity: IIdentifier, identity: IIdentifier,
apiServer: string, apiServer: string,
axios: Axios, axios: Axios,

6
src/views/AccountViewView.vue

@ -202,8 +202,12 @@
> >
Advanced Advanced
</h3> </h3>
<div v-if="showAdvanced"> <div v-if="showAdvanced">
<p>
Beware: the features here can be confusing and even change data in ways
you do not expect. But we support your freedoms!
</p>
<!-- Deep Identity Details --> <!-- Deep Identity Details -->
<h2 class="text-slate-500 text-sm font-bold mb-2 py-2"> <h2 class="text-slate-500 text-sm font-bold mb-2 py-2">
Deep Identity Details Deep Identity Details

89
src/views/ClaimView.vue

@ -115,6 +115,24 @@
</ul> </ul>
</div> </div>
</div> </div>
<div class="mt-4">
<div v-if="confirmerIdList.includes(activeDid)">
You have confirmed this claim.
</div>
<div v-else-if="containsHiddenDid(veriClaim.claim)">
You cannot confirm this claim because it contains a DID that is hidden
from you.
</div>
<div v-else>
<button
class="bg-blue-600 text-white mt-4 px-4 py-2 rounded-md mb-4"
@click="confirmClaim(veriClaim.id)"
>
Confirm Claim
</button>
</div>
</div>
</div> </div>
<div> <div>
@ -164,12 +182,7 @@ import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { import * as serverUtil from "@/libs/endorserServer";
BLANK_GENERIC_CLAIM,
didInfo,
isHiddenDid,
stripEndorserPrefix,
} from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
@ -197,9 +210,10 @@ export default class ClaimView extends Vue {
fullClaim = null; fullClaim = null;
fullClaimMessage = ""; fullClaimMessage = "";
numConfsNotVisible = 0; // number of hidden DIDs in the confirmerIdList, minus the issuer if they aren't visible 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; util = util;
containsHiddenDid = serverUtil.containsHiddenDid;
async created() { async created() {
await db.open(); await db.open();
@ -241,7 +255,7 @@ export default class ClaimView extends Vue {
); );
} }
public async getIdentity(activeDid: string) { public async getIdentity(activeDid: string): Promise<IIdentifier> {
await accountsDB.open(); await accountsDB.open();
const account = (await accountsDB.accounts const account = (await accountsDB.accounts
.where("did") .where("did")
@ -275,7 +289,7 @@ export default class ClaimView extends Vue {
dids: Array<string>, dids: Array<string>,
contacts: Array<Contact>, contacts: Array<Contact>,
) { ) {
return didInfo(did, activeDid, dids, contacts); return serverUtil.didInfo(did, activeDid, dids, contacts);
} }
async loadClaim(claimId: string, identity: IIdentifier) { async loadClaim(claimId: string, identity: IIdentifier) {
@ -317,21 +331,19 @@ export default class ClaimView extends Vue {
const confirmUrl = const confirmUrl =
this.apiServer + this.apiServer +
"/api/report/issuersWhoClaimedOrConfirmed?claimId=" + "/api/report/issuersWhoClaimedOrConfirmed?claimId=" +
encodeURIComponent(stripEndorserPrefix(claimId)); encodeURIComponent(serverUtil.stripEndorserPrefix(claimId));
const confirmHeaders = await this.getHeaders(identity); const confirmHeaders = await this.getHeaders(identity);
try { try {
const response = await this.axios.get(confirmUrl, { const response = await this.axios.get(confirmUrl, {
headers: confirmHeaders, headers: confirmHeaders,
}); });
if (response.status === 200) { if (response.status === 200) {
console.log("response:", response);
const resultList1 = response.data.result || []; const resultList1 = response.data.result || [];
const resultList2 = R.reject(isHiddenDid, resultList1); const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1);
const resultList3 = R.reject( const resultList3 = R.reject(
(did: string) => did === this.veriClaim.issuer, (did: string) => did === this.veriClaim.issuer,
resultList2, resultList2,
); );
console.log("all result lists:", resultList1, resultList2, resultList3);
this.confirmerIdList = resultList3; this.confirmerIdList = resultList3;
this.numConfsNotVisible = resultList1.length - resultList2.length; this.numConfsNotVisible = resultList1.length - resultList2.length;
if (resultList3.length === resultList2.length) { if (resultList3.length === resultList2.length) {
@ -351,8 +363,6 @@ export default class ClaimView extends Vue {
this.confsVisibleErrorMessage = this.confsVisibleErrorMessage =
"Had problems retrieving confirmations. See logs for more info."; "Had problems retrieving confirmations. See logs for more info.";
} }
console.log("confirmerIdList:", this.confirmerIdList);
console.log("confsVisibleToIdList:", this.confsVisibleToIdList);
} }
async showFullClaim(claimId: string) { 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,
);
}
}
} }
</script> </script>

2
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<IIdentifier> {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts) as Account; const account = R.find((acc) => acc.did === activeDid, accounts) as Account;

Loading…
Cancel
Save