From 5c1440244cbd3e6d9b80e557dc1a511c1a067871 Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Thu, 16 May 2024 10:40:56 +0500 Subject: [PATCH] feat: add verify page --- src/pages/home/index.tsx | 6 + src/pages/verify/index.tsx | 332 +++++++++++++++++++++++++++++ src/pages/verify/style.module.scss | 39 ++++ src/routes/index.tsx | 8 +- src/types/core.ts | 1 + 5 files changed, 385 insertions(+), 1 deletion(-) create mode 100644 src/pages/verify/index.tsx create mode 100644 src/pages/verify/style.module.scss diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index 79b1261..4f9d477 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -20,6 +20,12 @@ export const HomePage = () => { > Sign + ) } diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx new file mode 100644 index 0000000..d808294 --- /dev/null +++ b/src/pages/verify/index.tsx @@ -0,0 +1,332 @@ +import { + Box, + Button, + List, + ListItem, + ListSubheader, + Typography, + useTheme +} from '@mui/material' +import JSZip from 'jszip' +import { MuiFileInput } from 'mui-file-input' +import { useEffect, useState } from 'react' +import { Link } from 'react-router-dom' +import { toast } from 'react-toastify' +import placeholderAvatar from '../../assets/images/nostr-logo.jpg' +import { LoadingSpinner } from '../../components/LoadingSpinner' +import { MetadataController } from '../../controllers' +import { getProfileRoute } from '../../routes' +import { Meta, ProfileMetadata } from '../../types' +import { + hexToNpub, + parseJson, + readContentOfZipEntry, + shorten +} from '../../utils' +import styles from './style.module.scss' +import { Event, verifyEvent } from 'nostr-tools' + +export const VerifyPage = () => { + const theme = useTheme() + + const textColor = theme.palette.getContrastText( + theme.palette.background.paper + ) + + const [isLoading, setIsLoading] = useState(false) + const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') + + const [selectedFile, setSelectedFile] = useState(null) + const [meta, setMeta] = useState(null) + + const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>( + {} + ) + + useEffect(() => { + if (meta) { + const metadataController = new MetadataController() + + const users = [meta.submittedBy, ...meta.signers, ...meta.viewers] + + users.forEach((user) => { + if (!(user in metadata)) { + metadataController + .findMetadata(user) + .then((metadataEvent) => { + const metadataContent = + metadataController.extractProfileMetadataContent(metadataEvent) + if (metadataContent) + setMetadata((prev) => ({ + ...prev, + [user]: metadataContent + })) + }) + .catch((err) => { + console.error( + `error occurred in finding metadata for: ${user}`, + err + ) + }) + } + }) + } + }, [meta]) + + const handleVerify = async () => { + if (!selectedFile) return + setIsLoading(true) + + const zip = await JSZip.loadAsync(selectedFile).catch((err) => { + console.log('err in loading zip file :>> ', err) + toast.error(err.message || 'An error occurred in loading zip file.') + return null + }) + + if (!zip) return + + setLoadingSpinnerDesc('Parsing meta.json') + + const metaFileContent = await readContentOfZipEntry( + zip, + 'meta.json', + 'string' + ) + + if (!metaFileContent) { + setIsLoading(false) + return + } + + const parsedMetaJson = await parseJson(metaFileContent).catch( + (err) => { + console.log('err in parsing the content of meta.json :>> ', err) + toast.error( + err.message || 'error occurred in parsing the content of meta.json' + ) + setIsLoading(false) + return null + } + ) + + setMeta(parsedMetaJson) + setIsLoading(false) + } + + const imageLoadError = (event: any) => { + event.target.src = placeholderAvatar + } + + const getRoboImageUrl = (pubkey: string) => { + const npub = hexToNpub(pubkey) + return `https://robohash.org/${npub}.png?set=set3` + } + + const displayUser = (pubkey: string, verifySignature = false) => { + const profile = metadata[pubkey] + + let isValidSignature = false + + if (verifySignature) { + const signedEventString = meta ? meta.signedEvents[pubkey] : null + if (signedEventString) { + try { + const signedEvent = JSON.parse(signedEventString) + isValidSignature = verifyEvent(signedEvent) + } catch (error) { + console.error( + `An error occurred in parsing and verifying the signature event for ${pubkey}`, + error + ) + } + } + } + + return ( + + Profile Image + + + {profile?.display_name || + profile?.name || + shorten(hexToNpub(pubkey))} + + + {verifySignature && ( + + ({isValidSignature ? 'Valid' : 'Not Valid'} Signature) + + )} + + ) + } + + const displayExportedBy = () => { + if (!meta || !meta.exportSignature) return null + + const exportSignatureString = meta.exportSignature + + try { + const exportSignatureEvent = JSON.parse(exportSignatureString) as Event + + if (verifyEvent(exportSignatureEvent)) { + return displayUser(exportSignatureEvent.pubkey) + } else { + toast.error(`Invalid export signature!`) + return ( + + Invalid export signature + + ) + } + } catch (error) { + console.error(`An error occurred wile parsing exportSignature`, error) + return null + } + } + + return ( + <> + {isLoading && } + + {!meta && ( + <> + + Select exported zip file + + + setSelectedFile(value)} + InputProps={{ + inputProps: { + accept: '.zip' + } + }} + /> + + {selectedFile && ( + + + + )} + + )} + + {meta && ( + <> + + Meta Info + + } + > + + + Submitted By + + {displayUser(meta.submittedBy)} + + + + + Exported By + + {displayExportedBy()} + + + {meta.signers.length > 0 && ( + + + Signers + +
    + {meta.signers.map((signer) => ( +
  • + {displayUser(signer, true)} +
  • + ))} +
+
+ )} + + {meta.viewers.length > 0 && ( + + + Viewers + +
    + {meta.viewers.map((viewer) => ( +
  • + {displayUser(viewer)} +
  • + ))} +
+
+ )} + + + + Files + +
    + {Object.keys(meta.fileHashes).map((file, index) => ( +
  • + {file} +
  • + ))} +
+
+
+ + )} +
+ + ) +} diff --git a/src/pages/verify/style.module.scss b/src/pages/verify/style.module.scss new file mode 100644 index 0000000..61fe63d --- /dev/null +++ b/src/pages/verify/style.module.scss @@ -0,0 +1,39 @@ +@import '../../colors.scss'; + +.container { + color: $text-color; + display: flex; + flex-direction: column; + + .subHeader { + border-bottom: 0.5px solid; + font-size: 1.5rem; + } + + .usersList { + display: flex; + flex-direction: column; + gap: 10px; + list-style: none; + margin-top: 10px; + } + + .user { + display: flex; + align-items: center; + gap: 10px; + + .name { + text-align: center; + cursor: pointer; + } + } + + .tableCell { + border-right: 1px solid rgba(224, 224, 224, 1); + + .user { + @extend .user; + } + } +} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 93eb430..1796e19 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -5,11 +5,13 @@ import { Login } from '../pages/login' import { ProfilePage } from '../pages/profile' import { hexToNpub } from '../utils' import { SignPage } from '../pages/sign' +import { VerifyPage } from '../pages/verify' export const appPrivateRoutes = { homePage: '/', create: '/create', - sign: '/sign' + sign: '/sign', + verify: '/verify' } export const appPublicRoutes = { @@ -51,5 +53,9 @@ export const privateRoutes = [ { path: appPrivateRoutes.sign, element: + }, + { + path: appPrivateRoutes.verify, + element: } ] diff --git a/src/types/core.ts b/src/types/core.ts index 531e1fd..a328ac0 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -14,4 +14,5 @@ export interface Meta { fileHashes: { [key: string]: string } submittedBy: string signedEvents: { [key: string]: string } + exportSignature?: string }