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) => { |
|||
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