From 62c24f2d3ad552e66ef0fa3921319258850fb414 Mon Sep 17 00:00:00 2001 From: Oleksandr Raspopov Date: Fri, 20 Dec 2024 16:51:38 +0100 Subject: [PATCH] chore: update auth credentials table view --- ui/package-lock.json | 7 ++ ui/package.json | 1 + ui/src/adapters/api/credentials.ts | 72 +++++++++-- .../identities/IdentityAuthCredentials.tsx | 119 ++++++++++-------- ui/src/domain/credential.ts | 11 ++ ui/src/domain/index.ts | 1 + 6 files changed, 152 insertions(+), 59 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 6427b3cda..b76a8a5a0 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8,6 +8,7 @@ "name": "issuer-node-ui", "version": "1.0.0", "dependencies": { + "@iden3/js-crypto": "^1.1.0", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "ajv-formats-draft2019": "^1.6.1", @@ -1240,6 +1241,12 @@ "typescript": ">=5" } }, + "node_modules/@iden3/js-crypto": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@iden3/js-crypto/-/js-crypto-1.1.0.tgz", + "integrity": "sha512-MbL7OpOxBoCybAPoorxrp+fwjDVESyDe6giIWxErjEIJy0Q2n1DU4VmKh4vDoCyhJx/RdVgT8Dkb59lKwISqsw==", + "license": "AGPL-3.0" + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", diff --git a/ui/package.json b/ui/package.json index ad9f459d1..6bf0d357e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -2,6 +2,7 @@ "name": "issuer-node-ui", "version": "1.0.0", "dependencies": { + "@iden3/js-crypto": "^1.1.0", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "ajv-formats-draft2019": "^1.6.1", diff --git a/ui/src/adapters/api/credentials.ts b/ui/src/adapters/api/credentials.ts index dde2379b1..1082dc0b5 100644 --- a/ui/src/adapters/api/credentials.ts +++ b/ui/src/adapters/api/credentials.ts @@ -12,12 +12,14 @@ import { serializeSorters, } from "src/adapters/api"; import { + buildAppError, datetimeParser, getListParser, getResourceParser, getStrictParser, } from "src/adapters/parsers"; import { + AuthCredential, Credential, CredentialDisplayMethod, CredentialStatusType, @@ -50,6 +52,7 @@ type CredentialInput = Pick & { expirationDate?: string | null; issuanceDate: string; issuer: string; + proof?: Array<{ type: ProofType }> | null; refreshService?: RefreshService | null; type: [string, string]; }; @@ -82,6 +85,14 @@ export const credentialParser = getStrictParser()( expirationDate: datetimeParser.nullable().default(null), issuanceDate: datetimeParser, issuer: z.string(), + proof: z + .array( + z.object({ + type: z.nativeEnum(ProofType), + }) + ) + .nullable() + .default(null), refreshService: z .object({ id: z.string(), type: z.literal("Iden3RefreshService2023") }) .nullable() @@ -103,6 +114,7 @@ export const credentialParser = getStrictParser()( expirationDate, issuanceDate, issuer, + proof, refreshService, type, }, @@ -118,6 +130,7 @@ export const credentialParser = getStrictParser()( expired, id, issuanceDate, + proof, proofTypes, refreshService, revNonce: credentialStatus.revocationNonce, @@ -137,6 +150,27 @@ export const credentialStatusParser = getStrictParser()( z.union([z.literal("all"), z.literal("revoked"), z.literal("expired")]) ); +export type AuthCredentialSubjectInput = { + x: string; + y: string; +}; +export type AuthCredentialSubject = { + x: bigint; + y: bigint; +}; + +export const authCredentialSubjectParser = getStrictParser< + AuthCredentialSubjectInput, + AuthCredentialSubject +>()( + z + .object({ + x: z.string().regex(/^\d+$/, "x must be a numeric string"), + y: z.string().regex(/^\d+$/, "y must be a numeric string"), + }) + .transform(({ x, y }) => ({ x: BigInt(x), y: BigInt(y) })) +); + export async function getCredential({ credentialID, env, @@ -206,7 +240,7 @@ export async function getCredentials({ } } -export async function getCredentialsByIDs({ +export async function getAuthCredentialsByIDs({ env, identifier, IDs, @@ -216,17 +250,41 @@ export async function getCredentialsByIDs({ env: Env; identifier: string; signal?: AbortSignal; -}): Promise>> { +}): Promise>> { try { const promises = IDs.map((id) => getCredential({ credentialID: id, env, identifier, signal })); const credentials = await Promise.all(promises); - const { failed, successful } = credentials.reduce>( + const { failed, successful } = credentials.reduce>( (acc, credential) => { - if (credential.success) { - return { ...acc, successful: [...acc.successful, credential.data] }; - } else { - return { ...acc, failed: [...acc.failed, credential.error] }; + try { + if (credential.success) { + const parsedCredentialSubject = authCredentialSubjectParser.parse({ + x: credential.data.credentialSubject.x, + y: credential.data.credentialSubject.y, + }); + + const published = + credential.data.proof?.some( + ({ type }) => type === ProofType.Iden3SparseMerkleTreeProof + ) || false; + + return { + ...acc, + successful: [ + ...acc.successful, + { + ...credential.data, + credentialSubject: { ...parsedCredentialSubject }, + published, + }, + ], + }; + } else { + return { ...acc, failed: [...acc.failed, credential.error] }; + } + } catch (error) { + return { ...acc, failed: [...acc.failed, buildAppError(error)] }; } }, { failed: [], successful: [] } diff --git a/ui/src/components/identities/IdentityAuthCredentials.tsx b/ui/src/components/identities/IdentityAuthCredentials.tsx index f74641a19..b4cf048cd 100644 --- a/ui/src/components/identities/IdentityAuthCredentials.tsx +++ b/ui/src/components/identities/IdentityAuthCredentials.tsx @@ -1,3 +1,4 @@ +import { PublicKey } from "@iden3/js-crypto"; import { Avatar, Button, @@ -9,13 +10,16 @@ import { Table, TableColumnsType, Tag, + Tooltip, Typography, } from "antd"; import { useCallback, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { getCredentialsByIDs } from "src/adapters/api/credentials"; +import { getAuthCredentialsByIDs } from "src/adapters/api/credentials"; import { notifyErrors } from "src/adapters/parsers"; +import IconCheckMark from "src/assets/icons/check.svg?react"; +import IconCopy from "src/assets/icons/copy-01.svg?react"; import IconCreditCardRefresh from "src/assets/icons/credit-card-refresh.svg?react"; import IconDots from "src/assets/icons/dots-vertical.svg?react"; import IconPlus from "src/assets/icons/plus.svg?react"; @@ -25,7 +29,7 @@ import { TableCard } from "src/components/shared/TableCard"; import { useEnvContext } from "src/contexts/Env"; import { useIdentityContext } from "src/contexts/Identity"; -import { AppError, Credential } from "src/domain"; +import { AppError, AuthCredential } from "src/domain"; import { ROUTES } from "src/routes"; import { AsyncTask, isAsyncTaskDataAvailable, isAsyncTaskStarting } from "src/utils/async"; import { isAbortedError, makeRequestAbortable } from "src/utils/browser"; @@ -37,11 +41,11 @@ export function IdentityAuthCredentials({ IDs }: { IDs: Array }) { const { identifier } = useIdentityContext(); const navigate = useNavigate(); - const [credentials, setCredentials] = useState>({ + const [credentials, setCredentials] = useState>({ status: "pending", }); - const [credentialToRevoke, setCredentialToRevoke] = useState(); + const [credentialToRevoke, setCredentialToRevoke] = useState(); const fetchAuthCredentials = useCallback( async (signal?: AbortSignal) => { @@ -51,7 +55,7 @@ export function IdentityAuthCredentials({ IDs }: { IDs: Array }) { : { status: "loading" } ); - const response = await getCredentialsByIDs({ + const response = await getAuthCredentialsByIDs({ env, identifier, IDs, @@ -73,85 +77,96 @@ export function IdentityAuthCredentials({ IDs }: { IDs: Array }) { [env, identifier, IDs] ); - const tableColumns: TableColumnsType = [ - { - dataIndex: "schemaType", - ellipsis: { showTitle: false }, - key: "schemaType", - render: (schemaType: Credential["schemaType"]) => ( - {schemaType} - ), - sorter: { - compare: ({ schemaType: a }, { schemaType: b }) => (a && b ? a.localeCompare(b) : 0), - multiple: 1, - }, - title: "Type", - }, - { - dataIndex: "createdAt", - key: "createdAt", - render: (issuanceDate: Credential["issuanceDate"]) => ( - {formatDate(issuanceDate)} - ), - sorter: ({ issuanceDate: a }, { issuanceDate: b }) => b.getTime() - a.getTime(), - title: ISSUE_DATE, - }, - + const tableColumns: TableColumnsType = [ { dataIndex: "credentialSubject", ellipsis: { showTitle: false }, key: "credentialSubject", - render: (credentialSubject: Credential["credentialSubject"]) => ( - - {typeof credentialSubject.x === "string" ? credentialSubject.x : "-"} - - ), + render: (credentialSubject: AuthCredential["credentialSubject"]) => { + const { x, y } = credentialSubject; + const pKey = new PublicKey([x, y]); + const pKeyHex = pKey.hex(); + return ( + + , ], + text: pKeyHex, + }} + ellipsis={{ + suffix: pKeyHex.slice(-5), + }} + > + {pKeyHex} + + + ); + }, - title: "Credential Subject x", - }, - { - dataIndex: "credentialSubject", - ellipsis: { showTitle: false }, - key: "credentialSubject", - render: (credentialSubject: Credential["credentialSubject"]) => ( - - {typeof credentialSubject.y === "string" ? credentialSubject.y : "-"} - - ), - title: "Credential Subject y", + title: "Public key", }, { dataIndex: "credentialStatus", ellipsis: { showTitle: false }, key: "credentialStatus", - render: (credentialStatus: Credential["credentialStatus"]) => ( + render: (credentialStatus: AuthCredential["credentialStatus"]) => ( {credentialStatus.revocationNonce} ), title: "Revocation nonce", }, + { + dataIndex: "schemaType", + ellipsis: { showTitle: false }, + key: "schemaType", + render: (schemaType: AuthCredential["schemaType"]) => ( + {schemaType} + ), + sorter: { + compare: ({ schemaType: a }, { schemaType: b }) => (a && b ? a.localeCompare(b) : 0), + multiple: 1, + }, + title: "Type", + }, { dataIndex: "credentialStatus", ellipsis: { showTitle: false }, key: "credentialStatus", - render: (credentialStatus: Credential["credentialStatus"]) => ( + render: (credentialStatus: AuthCredential["credentialStatus"]) => ( {credentialStatus.type} ), - title: "Revocation status type", + title: "Revocation status", + }, + { + dataIndex: "createdAt", + key: "createdAt", + render: (issuanceDate: AuthCredential["issuanceDate"]) => ( + {formatDate(issuanceDate)} + ), + sorter: ({ issuanceDate: a }, { issuanceDate: b }) => b.getTime() - a.getTime(), + title: ISSUE_DATE, }, { dataIndex: "revoked", key: "revoked", - render: (revoked: Credential["revoked"]) => ( + render: (revoked: AuthCredential["revoked"]) => ( {revoked ? "Revoked" : "-"} ), responsive: ["sm"], title: REVOCATION, }, - + { + dataIndex: "published", + key: "published", + render: (published: AuthCredential["published"]) => ( + {published ? "Published" : "Pending"} + ), + responsive: ["sm"], + title: "Published", + }, { dataIndex: "id", key: "id", - render: (id: Credential["id"], credential: Credential) => ( + render: (id: AuthCredential["id"], credential: AuthCredential) => ( | null; proofTypes: ProofType[]; refreshService: RefreshService | null; revNonce: number; @@ -40,6 +43,14 @@ export type Credential = { userID: string; }; +export type AuthCredential = Omit & { + credentialSubject: { + x: bigint; + y: bigint; + }; + published: boolean; +}; + export type IssuedMessage = { schemaType: string; universalLink: string; diff --git a/ui/src/domain/index.ts b/ui/src/domain/index.ts index f3f11e471..5be033699 100644 --- a/ui/src/domain/index.ts +++ b/ui/src/domain/index.ts @@ -3,6 +3,7 @@ export type { AppError } from "src/domain/error"; export type { Connection } from "src/domain/connection"; export type { + AuthCredential, Credential, CredentialsTabIDs, IssuedMessage,