add ability to view specific details of a claim, and also confirm it #91
Merged
trentlarson
merged 3 commits from claim
into master
11 months ago
14 changed files with 2894 additions and 65 deletions
@ -1,3 +1,5 @@ |
|||||
|
// many of these are also found in endorser-mobile utility.ts
|
||||
|
|
||||
export const isGlobalUri = (uri: string) => { |
export const isGlobalUri = (uri: string) => { |
||||
return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/)); |
return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/)); |
||||
}; |
}; |
||||
|
File diff suppressed because it is too large
@ -0,0 +1,472 @@ |
|||||
|
<template> |
||||
|
<QuickNav /> |
||||
|
<!-- CONTENT --> |
||||
|
<section id="Content" class="p-6 pb-24"> |
||||
|
<!-- Breadcrumb --> |
||||
|
<div id="ViewBreadcrumb" class="mb-8"> |
||||
|
<h1 class="text-lg text-center font-light relative px-7"> |
||||
|
<!-- Back --> |
||||
|
<button |
||||
|
@click="$router.go(-1)" |
||||
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" |
||||
|
> |
||||
|
<fa icon="chevron-left" class="fa-fw"></fa> |
||||
|
</button> |
||||
|
Verifiable Claim Details |
||||
|
</h1> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Details --> |
||||
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"> |
||||
|
<div> |
||||
|
<div class="block pb-4 flex gap-4 overflow-hidden"> |
||||
|
<div class="overflow-hidden"> |
||||
|
<h2 class="text-xl">{{ veriClaim.id }}</h2> |
||||
|
<div class="text-sm"> |
||||
|
<div> |
||||
|
{{ veriClaim.claimType }} |
||||
|
</div> |
||||
|
<div> |
||||
|
<fa icon="message" class="fa-fw text-slate-400"></fa> |
||||
|
{{ veriClaim.claim?.description }} |
||||
|
</div> |
||||
|
<div> |
||||
|
<fa icon="user" class="fa-fw text-slate-400"></fa> |
||||
|
{{ veriClaim.issuer }} |
||||
|
</div> |
||||
|
<div> |
||||
|
<fa icon="calendar" class="fa-fw text-slate-400"></fa> |
||||
|
{{ veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") }} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div> |
||||
|
<h2 class="font-bold text-2xl">Confirmations</h2> |
||||
|
|
||||
|
<span v-if="totalConfirmers() === 0">Nobody has confirmed this.</span> |
||||
|
<span v-else-if="totalConfirmers() === 1"> |
||||
|
One person has confirmed this. |
||||
|
</span> |
||||
|
<span v-else> {{ totalConfirmers() }} people have confirmed this. </span> |
||||
|
|
||||
|
<div v-if="totalConfirmers() > 0"> |
||||
|
<div |
||||
|
v-if=" |
||||
|
confirmerIdList.length === 0 && confsVisibleToIdList.length === 0 |
||||
|
" |
||||
|
> |
||||
|
Nobody that you know confirmed this claim, nor do they have any |
||||
|
confirmers in their network. |
||||
|
</div> |
||||
|
|
||||
|
<div |
||||
|
v-if="confirmerIdList.length === 0 && confsVisibleToIdList.length > 0" |
||||
|
> |
||||
|
<!-- Only show if this person has links to confirmers (below). --> |
||||
|
Nobody that you know has issued or confirmed this claim. |
||||
|
</div> |
||||
|
<div v-if="confirmerIdList.length > 0"> |
||||
|
The following people have issued or confirmed this claim. |
||||
|
<ul> |
||||
|
<li |
||||
|
v-for="confirmerId in confirmerIdList" |
||||
|
:key="confirmerId" |
||||
|
class="list-disc" |
||||
|
> |
||||
|
<div class="flex gap-4"> |
||||
|
<div class="grow overflow-hidden"> |
||||
|
<div class="text-sm"> |
||||
|
{{ confirmerId }} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
|
||||
|
<!-- |
||||
|
Never need to show the following message. |
||||
|
If there is nobody in the confirmerIdList then we'll show the combined "nobody" message. |
||||
|
If there is somebody in the confirmerIdList then that's all they need to show. |
||||
|
--> |
||||
|
<!-- Nobody that you know can see someone who has confirmed this claim. --> |
||||
|
|
||||
|
<div v-if="confsVisibleToIdList.length > 0"> |
||||
|
The following people can connect you with people who have issued or |
||||
|
confirmed this claim. |
||||
|
<ul> |
||||
|
<li |
||||
|
v-for="confsVisibleTo in confsVisibleToIdList" |
||||
|
:key="confsVisibleTo" |
||||
|
class="list-disc" |
||||
|
> |
||||
|
<div class="flex gap-4"> |
||||
|
<div class="grow overflow-hidden"> |
||||
|
<div class="text-sm"> |
||||
|
{{ confsVisibleTo }} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</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 data 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> |
||||
|
<h2 class="font-bold text-2xl mt-8">Claim</h2> |
||||
|
<pre>{{ util.inspect(veriClaim, false, null) }}</pre> |
||||
|
</div> |
||||
|
|
||||
|
<h2 class="font-bold text-2xl mt-8">Full Claim</h2> |
||||
|
<p> |
||||
|
The full claim includes the claim as it was originally issued, including |
||||
|
the signature (ie. the proof of issuance by that person). |
||||
|
</p> |
||||
|
<div v-if="!fullClaim"> |
||||
|
<div v-if="fullClaimMessage"> |
||||
|
{{ fullClaimMessage }} |
||||
|
</div> |
||||
|
<button |
||||
|
v-else |
||||
|
class="bg-blue-600 text-white mt-4 px-4 py-2 rounded-md mb-4" |
||||
|
@click="showFullClaim(veriClaim.id)" |
||||
|
> |
||||
|
Load Full Claim Details |
||||
|
</button> |
||||
|
</div> |
||||
|
<div v-else> |
||||
|
<pre>{{ util.inspect(fullClaim, false, null) }}</pre> |
||||
|
</div> |
||||
|
|
||||
|
<a :href="apiServer + '/api/claim/' + veriClaim.id" target="_blank"> |
||||
|
<button class="bg-blue-600 text-white mt-4 px-4 py-2 rounded-md mb-4"> |
||||
|
View on the Public Server |
||||
|
</button> |
||||
|
</a> |
||||
|
</section> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { AxiosError, RawAxiosRequestHeaders } from "axios"; |
||||
|
import * as R from "ramda"; |
||||
|
import { IIdentifier } from "@veramo/core"; |
||||
|
import * as util from "util"; |
||||
|
import { Component, Vue } from "vue-facing-decorator"; |
||||
|
|
||||
|
import GiftedDialog from "@/components/GiftedDialog.vue"; |
||||
|
import OfferDialog from "@/components/OfferDialog.vue"; |
||||
|
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 QuickNav from "@/components/QuickNav.vue"; |
||||
|
import EntityIcon from "@/components/EntityIcon.vue"; |
||||
|
import { Account } from "@/db/tables/accounts"; |
||||
|
|
||||
|
interface Notification { |
||||
|
group: string; |
||||
|
type: string; |
||||
|
title: string; |
||||
|
text: string; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav }, |
||||
|
}) |
||||
|
export default class ClaimView extends Vue { |
||||
|
$notify!: (notification: Notification, timeout?: number) => void; |
||||
|
|
||||
|
activeDid = ""; |
||||
|
allMyDids: Array<string> = []; |
||||
|
allContacts: Array<Contact> = []; |
||||
|
apiServer = ""; |
||||
|
confirmerIdList = []; // list of DIDs that have confirmed this claim excluding the issuer |
||||
|
confsVisibleErrorMessage = ""; |
||||
|
confsVisibleToIdList = []; // list of DIDs that can see any confirmer |
||||
|
fullClaim = null; |
||||
|
fullClaimMessage = ""; |
||||
|
numConfsNotVisible = 0; // number of hidden DIDs in the confirmerIdList, minus the issuer if they aren't visible |
||||
|
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD; |
||||
|
|
||||
|
util = util; |
||||
|
containsHiddenDid = serverUtil.containsHiddenDid; |
||||
|
|
||||
|
async created() { |
||||
|
await db.open(); |
||||
|
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; |
||||
|
this.activeDid = settings?.activeDid || ""; |
||||
|
this.apiServer = settings?.apiServer || ""; |
||||
|
this.allContacts = await db.contacts.toArray(); |
||||
|
|
||||
|
await accountsDB.open(); |
||||
|
const accounts = accountsDB.accounts; |
||||
|
const accountsArr = 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 || "null"); |
||||
|
|
||||
|
const pathParam = window.location.pathname.substring("/claim/".length); |
||||
|
let claimId; |
||||
|
if (pathParam) { |
||||
|
claimId = decodeURIComponent(pathParam); |
||||
|
this.loadClaim(claimId, identity); |
||||
|
} else { |
||||
|
this.$notify( |
||||
|
{ |
||||
|
group: "alert", |
||||
|
type: "danger", |
||||
|
title: "Error", |
||||
|
text: "No claim ID was provided.", |
||||
|
}, |
||||
|
-1, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
totalConfirmers() { |
||||
|
return ( |
||||
|
this.numConfsNotVisible + |
||||
|
this.confirmerIdList.length + |
||||
|
this.confsVisibleToIdList.length |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
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 identity 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, |
||||
|
activeDid: string, |
||||
|
dids: Array<string>, |
||||
|
contacts: Array<Contact>, |
||||
|
) { |
||||
|
return serverUtil.didInfo(did, activeDid, dids, contacts); |
||||
|
} |
||||
|
|
||||
|
async loadClaim(claimId: string, identity: IIdentifier) { |
||||
|
const url = |
||||
|
this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(claimId); |
||||
|
const headers = await this.getHeaders(identity); |
||||
|
|
||||
|
try { |
||||
|
const resp = await this.axios.get(url, { headers }); |
||||
|
if (resp.status === 200) { |
||||
|
this.veriClaim = resp.data; |
||||
|
} else { |
||||
|
// actually, axios typically throws an error so we never get here |
||||
|
console.log("Error getting claim:", resp); |
||||
|
this.$notify( |
||||
|
{ |
||||
|
group: "alert", |
||||
|
type: "danger", |
||||
|
title: "Error", |
||||
|
text: "There was a problem getting that claim. See logs for more info.", |
||||
|
}, |
||||
|
-1, |
||||
|
); |
||||
|
} |
||||
|
} catch (error: unknown) { |
||||
|
const serverError = error as AxiosError; |
||||
|
console.error("Error retrieving claim:", serverError); |
||||
|
this.$notify( |
||||
|
{ |
||||
|
group: "alert", |
||||
|
type: "danger", |
||||
|
title: "Error", |
||||
|
text: "Something went wrong retrieving that claim. See logs for more info.", |
||||
|
}, |
||||
|
-1, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
const confirmUrl = |
||||
|
this.apiServer + |
||||
|
"/api/report/issuersWhoClaimedOrConfirmed?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) { |
||||
|
const resultList1 = response.data.result || []; |
||||
|
const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1); |
||||
|
const resultList3 = R.reject( |
||||
|
(did: string) => did === this.veriClaim.issuer, |
||||
|
resultList2, |
||||
|
); |
||||
|
this.confirmerIdList = resultList3; |
||||
|
this.numConfsNotVisible = resultList1.length - resultList2.length; |
||||
|
if (resultList3.length === resultList2.length) { |
||||
|
// the issuer was not in the "visible" list so they must be hidden |
||||
|
// so subtract them from the non-visible confirmers count |
||||
|
this.numConfsNotVisible = this.numConfsNotVisible - 1; |
||||
|
} |
||||
|
this.confsVisibleToIdList = |
||||
|
response.data.result.resultVisibleToDids || []; |
||||
|
} else { |
||||
|
this.confsVisibleErrorMessage = |
||||
|
"Had problems retrieving confirmations."; |
||||
|
} |
||||
|
} catch (error: unknown) { |
||||
|
const serverError = error as AxiosError; |
||||
|
console.error("Error retrieving confirmations:", serverError); |
||||
|
this.confsVisibleErrorMessage = |
||||
|
"Had problems retrieving confirmations. See logs for more info."; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async showFullClaim(claimId: string) { |
||||
|
await accountsDB.open(); |
||||
|
const accounts = accountsDB.accounts; |
||||
|
const accountsArr = await accounts?.toArray(); |
||||
|
const account = accountsArr.find((acc) => acc.did === this.activeDid); |
||||
|
const identity = JSON.parse(account?.identity || "null"); |
||||
|
|
||||
|
const url = |
||||
|
this.apiServer + "/api/claim/full/" + encodeURIComponent(claimId); |
||||
|
const headers = await this.getHeaders(identity); |
||||
|
|
||||
|
try { |
||||
|
const resp = await this.axios.get(url, { headers }); |
||||
|
if (resp.status === 200) { |
||||
|
this.fullClaim = resp.data; |
||||
|
} else { |
||||
|
// actually, axios typically throws an error so we never get here |
||||
|
console.log("Error getting full claim:", resp); |
||||
|
this.$notify( |
||||
|
{ |
||||
|
group: "alert", |
||||
|
type: "danger", |
||||
|
title: "Error", |
||||
|
text: "There was a problem getting that claim. See logs for more info.", |
||||
|
}, |
||||
|
-1, |
||||
|
); |
||||
|
} |
||||
|
} catch (error: unknown) { |
||||
|
console.error("Error retrieving full claim:", error); |
||||
|
const serverError = error as AxiosError; |
||||
|
if (serverError.response?.status === 403) { |
||||
|
this.fullClaimMessage = |
||||
|
"You are not authorized to view the full contents of this claim." + |
||||
|
" To see all the details, ask the issuer to allow you to see their claims." + |
||||
|
" If you cannot see the issuer's DID, ask someone in the Confirmations section above." + |
||||
|
" If there are no connections, you will have to ask people in your" + |
||||
|
" network for their help, some other way; send them to this page and" + |
||||
|
" see if they can make a connection for you."; |
||||
|
} else { |
||||
|
this.$notify( |
||||
|
{ |
||||
|
group: "alert", |
||||
|
type: "danger", |
||||
|
title: "Error", |
||||
|
text: "Something went wrong retrieving that claim. See logs for more info.", |
||||
|
}, |
||||
|
-1, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async confirmClaim() { |
||||
|
if (confirm("Do you personally confirm that this is true?")) { |
||||
|
// 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> |
Loading…
Reference in new issue