Skip to content

Commit

Permalink
chore: update auth credentials table view
Browse files Browse the repository at this point in the history
  • Loading branch information
Oleksandr Raspopov committed Dec 20, 2024
1 parent e268d44 commit 62c24f2
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 59 deletions.
7 changes: 7 additions & 0 deletions ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
72 changes: 65 additions & 7 deletions ui/src/adapters/api/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -50,6 +52,7 @@ type CredentialInput = Pick<Credential, "id" | "revoked" | "schemaHash"> & {
expirationDate?: string | null;
issuanceDate: string;
issuer: string;
proof?: Array<{ type: ProofType }> | null;
refreshService?: RefreshService | null;
type: [string, string];
};
Expand Down Expand Up @@ -82,6 +85,14 @@ export const credentialParser = getStrictParser<CredentialInput, Credential>()(
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()
Expand All @@ -103,6 +114,7 @@ export const credentialParser = getStrictParser<CredentialInput, Credential>()(
expirationDate,
issuanceDate,
issuer,
proof,
refreshService,
type,
},
Expand All @@ -118,6 +130,7 @@ export const credentialParser = getStrictParser<CredentialInput, Credential>()(
expired,
id,
issuanceDate,
proof,
proofTypes,
refreshService,
revNonce: credentialStatus.revocationNonce,
Expand All @@ -137,6 +150,27 @@ export const credentialStatusParser = getStrictParser<CredentialStatus>()(
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,
Expand Down Expand Up @@ -206,7 +240,7 @@ export async function getCredentials({
}
}

export async function getCredentialsByIDs({
export async function getAuthCredentialsByIDs({
env,
identifier,
IDs,
Expand All @@ -216,17 +250,41 @@ export async function getCredentialsByIDs({
env: Env;
identifier: string;
signal?: AbortSignal;
}): Promise<Response<List<Credential>>> {
}): Promise<Response<List<AuthCredential>>> {
try {
const promises = IDs.map((id) => getCredential({ credentialID: id, env, identifier, signal }));
const credentials = await Promise.all(promises);

const { failed, successful } = credentials.reduce<List<Credential>>(
const { failed, successful } = credentials.reduce<List<AuthCredential>>(
(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: [] }
Expand Down
119 changes: 67 additions & 52 deletions ui/src/components/identities/IdentityAuthCredentials.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PublicKey } from "@iden3/js-crypto";
import {
Avatar,
Button,
Expand All @@ -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";
Expand All @@ -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";
Expand All @@ -37,11 +41,11 @@ export function IdentityAuthCredentials({ IDs }: { IDs: Array<string> }) {
const { identifier } = useIdentityContext();
const navigate = useNavigate();

const [credentials, setCredentials] = useState<AsyncTask<Credential[], AppError>>({
const [credentials, setCredentials] = useState<AsyncTask<AuthCredential[], AppError>>({
status: "pending",
});

const [credentialToRevoke, setCredentialToRevoke] = useState<Credential>();
const [credentialToRevoke, setCredentialToRevoke] = useState<AuthCredential>();

const fetchAuthCredentials = useCallback(
async (signal?: AbortSignal) => {
Expand All @@ -51,7 +55,7 @@ export function IdentityAuthCredentials({ IDs }: { IDs: Array<string> }) {
: { status: "loading" }
);

const response = await getCredentialsByIDs({
const response = await getAuthCredentialsByIDs({
env,
identifier,
IDs,
Expand All @@ -73,85 +77,96 @@ export function IdentityAuthCredentials({ IDs }: { IDs: Array<string> }) {
[env, identifier, IDs]
);

const tableColumns: TableColumnsType<Credential> = [
{
dataIndex: "schemaType",
ellipsis: { showTitle: false },
key: "schemaType",
render: (schemaType: Credential["schemaType"]) => (
<Typography.Text strong>{schemaType}</Typography.Text>
),
sorter: {
compare: ({ schemaType: a }, { schemaType: b }) => (a && b ? a.localeCompare(b) : 0),
multiple: 1,
},
title: "Type",
},
{
dataIndex: "createdAt",
key: "createdAt",
render: (issuanceDate: Credential["issuanceDate"]) => (
<Typography.Text>{formatDate(issuanceDate)}</Typography.Text>
),
sorter: ({ issuanceDate: a }, { issuanceDate: b }) => b.getTime() - a.getTime(),
title: ISSUE_DATE,
},

const tableColumns: TableColumnsType<AuthCredential> = [
{
dataIndex: "credentialSubject",
ellipsis: { showTitle: false },
key: "credentialSubject",
render: (credentialSubject: Credential["credentialSubject"]) => (
<Typography.Text>
{typeof credentialSubject.x === "string" ? credentialSubject.x : "-"}
</Typography.Text>
),
render: (credentialSubject: AuthCredential["credentialSubject"]) => {
const { x, y } = credentialSubject;
const pKey = new PublicKey([x, y]);
const pKeyHex = pKey.hex();
return (
<Tooltip title={pKeyHex}>
<Typography.Text
copyable={{
icon: [<IconCopy key={0} />, <IconCheckMark key={1} />],
text: pKeyHex,
}}
ellipsis={{
suffix: pKeyHex.slice(-5),
}}
>
{pKeyHex}
</Typography.Text>
</Tooltip>
);
},

title: "Credential Subject x",
},
{
dataIndex: "credentialSubject",
ellipsis: { showTitle: false },
key: "credentialSubject",
render: (credentialSubject: Credential["credentialSubject"]) => (
<Typography.Text>
{typeof credentialSubject.y === "string" ? credentialSubject.y : "-"}
</Typography.Text>
),
title: "Credential Subject y",
title: "Public key",
},
{
dataIndex: "credentialStatus",
ellipsis: { showTitle: false },
key: "credentialStatus",
render: (credentialStatus: Credential["credentialStatus"]) => (
render: (credentialStatus: AuthCredential["credentialStatus"]) => (
<Typography.Text>{credentialStatus.revocationNonce}</Typography.Text>
),
title: "Revocation nonce",
},
{
dataIndex: "schemaType",
ellipsis: { showTitle: false },
key: "schemaType",
render: (schemaType: AuthCredential["schemaType"]) => (
<Typography.Text strong>{schemaType}</Typography.Text>
),
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"]) => (
<Typography.Text>{credentialStatus.type}</Typography.Text>
),
title: "Revocation status type",
title: "Revocation status",
},
{
dataIndex: "createdAt",
key: "createdAt",
render: (issuanceDate: AuthCredential["issuanceDate"]) => (
<Typography.Text>{formatDate(issuanceDate)}</Typography.Text>
),
sorter: ({ issuanceDate: a }, { issuanceDate: b }) => b.getTime() - a.getTime(),
title: ISSUE_DATE,
},
{
dataIndex: "revoked",
key: "revoked",
render: (revoked: Credential["revoked"]) => (
render: (revoked: AuthCredential["revoked"]) => (
<Typography.Text>{revoked ? "Revoked" : "-"}</Typography.Text>
),
responsive: ["sm"],
title: REVOCATION,
},

{
dataIndex: "published",
key: "published",
render: (published: AuthCredential["published"]) => (
<Typography.Text>{published ? "Published" : "Pending"}</Typography.Text>
),
responsive: ["sm"],
title: "Published",
},
{
dataIndex: "id",
key: "id",
render: (id: Credential["id"], credential: Credential) => (
render: (id: AuthCredential["id"], credential: AuthCredential) => (
<Dropdown
menu={{
items: [
Expand Down
11 changes: 11 additions & 0 deletions ui/src/domain/credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export type Credential = {
expired: boolean;
id: string;
issuanceDate: Date;
proof: Array<{
type: ProofType;
}> | null;
proofTypes: ProofType[];
refreshService: RefreshService | null;
revNonce: number;
Expand All @@ -40,6 +43,14 @@ export type Credential = {
userID: string;
};

export type AuthCredential = Omit<Credential, "credentialSubject"> & {
credentialSubject: {
x: bigint;
y: bigint;
};
published: boolean;
};

export type IssuedMessage = {
schemaType: string;
universalLink: string;
Expand Down
1 change: 1 addition & 0 deletions ui/src/domain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export type { AppError } from "src/domain/error";
export type { Connection } from "src/domain/connection";

export type {
AuthCredential,
Credential,
CredentialsTabIDs,
IssuedMessage,
Expand Down

0 comments on commit 62c24f2

Please sign in to comment.