From 3d6bf9976866ea3198b707f4c3137882a290049b Mon Sep 17 00:00:00 2001 From: Fernando Gonzalez Goncharov Date: Tue, 17 Dec 2024 23:04:08 +0200 Subject: [PATCH] feat(dashboard-for-dapps): stress test credential decryption and filtering --- apps/dashboard-for-dapps/src/routeTree.gen.ts | 31 +- .../src/routes/credentials.tsx | 408 ++++++++++++++++++ 2 files changed, 435 insertions(+), 4 deletions(-) create mode 100644 apps/dashboard-for-dapps/src/routes/credentials.tsx diff --git a/apps/dashboard-for-dapps/src/routeTree.gen.ts b/apps/dashboard-for-dapps/src/routeTree.gen.ts index fd01c2cb6..97c918796 100644 --- a/apps/dashboard-for-dapps/src/routeTree.gen.ts +++ b/apps/dashboard-for-dapps/src/routeTree.gen.ts @@ -11,10 +11,17 @@ // Import Routes import { Route as rootRoute } from "./routes/__root"; +import { Route as CredentialsImport } from "./routes/credentials"; import { Route as IndexImport } from "./routes/index"; // Create/Update Routes +const CredentialsRoute = CredentialsImport.update({ + id: "/credentials", + path: "/credentials", + getParentRoute: () => rootRoute, +} as any); + const IndexRoute = IndexImport.update({ id: "/", path: "/", @@ -32,6 +39,13 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof IndexImport; parentRoute: typeof rootRoute; }; + "/credentials": { + id: "/credentials"; + path: "/credentials"; + fullPath: "/credentials"; + preLoaderRoute: typeof CredentialsImport; + parentRoute: typeof rootRoute; + }; } } @@ -39,32 +53,37 @@ declare module "@tanstack/react-router" { export interface FileRoutesByFullPath { "/": typeof IndexRoute; + "/credentials": typeof CredentialsRoute; } export interface FileRoutesByTo { "/": typeof IndexRoute; + "/credentials": typeof CredentialsRoute; } export interface FileRoutesById { __root__: typeof rootRoute; "/": typeof IndexRoute; + "/credentials": typeof CredentialsRoute; } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath; - fullPaths: "/"; + fullPaths: "/" | "/credentials"; fileRoutesByTo: FileRoutesByTo; - to: "/"; - id: "__root__" | "/"; + to: "/" | "/credentials"; + id: "__root__" | "/" | "/credentials"; fileRoutesById: FileRoutesById; } export interface RootRouteChildren { IndexRoute: typeof IndexRoute; + CredentialsRoute: typeof CredentialsRoute; } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + CredentialsRoute: CredentialsRoute, }; export const routeTree = rootRoute @@ -77,11 +96,15 @@ export const routeTree = rootRoute "__root__": { "filePath": "__root.tsx", "children": [ - "/" + "/", + "/credentials" ] }, "/": { "filePath": "index.tsx" + }, + "/credentials": { + "filePath": "credentials.tsx" } } } diff --git a/apps/dashboard-for-dapps/src/routes/credentials.tsx b/apps/dashboard-for-dapps/src/routes/credentials.tsx new file mode 100644 index 000000000..b91451444 --- /dev/null +++ b/apps/dashboard-for-dapps/src/routes/credentials.tsx @@ -0,0 +1,408 @@ +import { + Center, + Container, + HStack, + Image, + List, + Spinner, + Stack, + Text, + chakra, +} from "@chakra-ui/react"; +import type { idOSCredential } from "@idos-network/idos-sdk"; +import { + Button, + DrawerActionTrigger, + DrawerBackdrop, + DrawerBody, + DrawerCloseTrigger, + DrawerContent, + DrawerFooter, + DrawerHeader, + DrawerRoot, + DrawerTitle, + RefreshButton, + SearchField, +} from "@idos-network/ui-kit"; +import { DataListItem, DataListRoot, EmptyState } from "@idos-network/ui-kit"; +import { useQuery } from "@tanstack/react-query"; +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { useDebounce, useToggle } from "@uidotdev/usehooks"; +import { matchSorter } from "match-sorter"; +import { useMemo, useState } from "react"; + +import { SecretKeyPrompt } from "@/components/secret-key-prompt"; +import { useSecretKey } from "@/hooks"; +import { useIdOS } from "@/idOS.provider"; +import { changeCase, decrypt, openImageInNewTab } from "@/utils"; + +export const Route = createFileRoute("/credentials")({ + component: Credentials, + validateSearch: (search): { filter?: string } => { + return { + filter: (search.filter as string) ?? undefined, + }; + }, +}); + +export const useFetchAllCredentials = ({ + enabled, + secretKey, +}: { enabled: boolean; secretKey: string }) => { + const idOS = useIdOS(); + + return useQuery({ + queryKey: ["credentials", secretKey], + queryFn: async () => { + const credentials = await idOS.data.listAllCredentials(); + const promiseList = credentials.map(async (credential) => { + const fullCredential = (await idOS.data.get( + "credentials", + credential.id, + false, + )) as idOSCredential; + + if (!fullCredential) return null; + + try { + const decrypted = decrypt( + fullCredential.content, + fullCredential.encryption_public_key, + secretKey, + ); + + return { + ...fullCredential, + content: decrypted, + }; + } catch (error) { + console.error(`Failed to decrypt/parse credential ${credential.id}:`, error); + return { + ...fullCredential, + content: null, + }; + } + }); + + const stressPromiseList = Array.from({ length: 300 }, () => promiseList).flat(); + const results = await Promise.all(stressPromiseList); + return results.filter((credential): credential is idOSCredential => credential !== null); + }, + enabled, + }); +}; + +function CredentialDetails({ + credential, + open, + toggle, +}: { credential: idOSCredential | null; open: boolean; toggle: (value?: boolean) => void }) { + if (!credential) return null; + + const content = JSON.parse(credential.content); + + const subject = Object.entries(content.credentialSubject).filter( + ([key]) => !["emails", "wallets"].includes(key) && !key.endsWith("_file"), + ) as [string, string][]; + + const emails = content.credentialSubject.emails; + const wallets = content.credentialSubject.wallets; + const files = ( + Object.entries(content.credentialSubject).filter(([key]) => key.endsWith("_file")) as [ + string, + string, + ][] + ).map(([key, value]) => [key, value]); + + return ( + { + toggle(false); + }} + > + + + + Credential details + + + + + {subject.map(([key, value]) => ( + + ))} + + + {emails.map(({ address, verified }: { address: string; verified: boolean }) => ( + + {address} + {verified ? " (verified)" : ""} + + ))} + + } + /> + + {wallets.map( + ({ + address, + currency, + }: { address: string; currency: string; verified: boolean }) => ( + + {address} ({currency}) + + ), + )} + + } + /> + {files.length > 0 ? ( + + {files.map(([key, value]) => ( + openImageInNewTab(value)} + > + + Identification document front + + + ))} + + } + /> + ) : null} + + + + + + + + + + + + ); +} + +function SearchResults({ results }: { results: idOSCredential[] }) { + const [credential, setCredential] = useState(null); + const [openSecretKeyPrompt, toggleSecretKeyPrompt] = useToggle(); + const [openCredentialDetails, toggleCredentialDetails] = useToggle(); + const [secretKey, setSecretKey] = useSecretKey(); + + if (!results.length) { + return ; + } + + const handleOpenCredentialDetails = async (credential: idOSCredential) => { + setCredential(credential); + + if (!secretKey) { + toggleSecretKeyPrompt(); + return; + } + + toggleCredentialDetails(); + }; + + const onKeySubmit = async (secretKey: string) => { + setSecretKey(secretKey); + toggleCredentialDetails(); + }; + + return ( + <> + {results.map((credential) => { + const publicFields = Object.entries(JSON.parse(credential.public_notes)) as [ + string, + string, + ][]; + + return ( + + + {publicFields.map(([key, value]) => ( + + ))} + + + + ); + })} + + + + + ); +} + +function Credentials() { + const navigate = useNavigate({ from: Route.fullPath }); + const { filter = "" } = Route.useSearch(); + const debouncedSearchTerm = useDebounce(filter, 300); + const [secretKey] = useSecretKey(); + const credentials = useFetchAllCredentials({ enabled: !!secretKey, secretKey }); + + const handleSearchChange = (e: React.ChangeEvent) => { + const search = e.target.value; + navigate({ + search: { + filter: search, + }, + }); + }; + + const results = useMemo(() => { + if (!credentials.data) return []; + if (!debouncedSearchTerm) return credentials.data; + + return matchSorter(credentials.data, debouncedSearchTerm, { + keys: ["public_notes", "content"], + threshold: matchSorter.rankings.CONTAINS, + }); + }, [debouncedSearchTerm, credentials.data]); + + return ( + + + {credentials.isFetching ? ( +
+ + Fetching credentials... +
+ ) : ( + + + + navigate({ + search: {}, + }) + } + /> + + credentials.refetch()} + /> + + + + )} +
+
+ ); +}