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?.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
}