diff --git a/src/components/DisplaySigit/index.tsx b/src/components/DisplaySigit/index.tsx index 0fe5c2f..ce82f47 100644 --- a/src/components/DisplaySigit/index.tsx +++ b/src/components/DisplaySigit/index.tsx @@ -1,9 +1,6 @@ -import { useEffect, useState } from 'react' -import { Meta, ProfileMetadata } from '../../types' +import { Meta } from '../../types' import { SigitCardDisplayInfo, SigitStatus } from '../../utils' -import { Event, kinds } from 'nostr-tools' import { Link } from 'react-router-dom' -import { MetadataController } from '../../controllers' import { formatTimestamp, hexToNpub, npubToHex, shorten } from '../../utils' import { appPublicRoutes, appPrivateRoutes } from '../../routes' import { Button, Divider, Tooltip } from '@mui/material' @@ -22,6 +19,7 @@ import { UserAvatarGroup } from '../UserAvatarGroup' import styles from './style.module.scss' import { TooltipChild } from '../TooltipChild' import { getExtensionIconLabel } from '../getExtensionIconLabel' +import { useSigitProfiles } from '../../hooks/useSigitProfiles' type SigitProps = { meta: Meta @@ -38,61 +36,10 @@ export const DisplaySigit = ({ meta, parsedMeta }: SigitProps) => { fileExtensions } = parsedMeta - const [profiles, setProfiles] = useState<{ [key: string]: ProfileMetadata }>( - {} - ) - - useEffect(() => { - const hexKeys = new Set([ - ...signers.map((signer) => npubToHex(signer)!) - ]) - - if (submittedBy) { - hexKeys.add(npubToHex(submittedBy)!) - } - - const metadataController = new MetadataController() - - const handleMetadataEvent = (key: string) => (event: Event) => { - const metadataContent = - metadataController.extractProfileMetadataContent(event) - - if (metadataContent) { - setProfiles((prev) => ({ - ...prev, - [key]: metadataContent - })) - } - } - - const handleEventListener = - (key: string) => (kind: number, event: Event) => { - if (kind === kinds.Metadata) { - handleMetadataEvent(key)(event) - } - } - - hexKeys.forEach((key) => { - if (!(key in profiles)) { - metadataController.on(key, handleEventListener(key)) - - metadataController - .findMetadata(key) - .then((metadataEvent) => { - if (metadataEvent) handleMetadataEvent(key)(metadataEvent) - }) - .catch((err) => { - console.error(`error occurred in finding metadata for: ${key}`, err) - }) - } - }) - - return () => { - hexKeys.forEach((key) => { - metadataController.off(key, handleEventListener(key)) - }) - } - }, [submittedBy, signers, profiles]) + const profiles = useSigitProfiles([ + ...(submittedBy ? [submittedBy] : []), + ...signers + ]) return (
diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx index aebd791..5460983 100644 --- a/src/hooks/useSigitMeta.tsx +++ b/src/hooks/useSigitMeta.tsx @@ -3,6 +3,7 @@ import { CreateSignatureEventContent, Meta } from '../types' import { Mark } from '../types/mark' import { fromUnixTimestamp, + hexToNpub, parseCreateSignatureEvent, parseCreateSignatureEventContent, SigitMetaParseError, @@ -12,11 +13,30 @@ import { import { toast } from 'react-toastify' import { verifyEvent } from 'nostr-tools' import { Event } from 'nostr-tools' +import store from '../store/store' +import { AuthState } from '../store/auth/types' +import { NostrController } from '../controllers' + +/** + * Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`, + * and `Event` (event's fields are made optional and pubkey and created_at replaced with our versions) + */ +interface FlatMeta + extends Meta, + CreateSignatureEventContent, + Partial> { + // Remove pubkey and use submittedBy as `npub1${string}` + submittedBy?: `npub1${string}` + + // Remove created_at and replace with createdAt + createdAt?: number -interface FlatMeta extends Meta, CreateSignatureEventContent, Partial { // Validated create signature event isValid: boolean + // Decryption + encryptionKey: string | null + // Calculated status fields signedStatus: SigitStatus signersStatus: { @@ -33,8 +53,8 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { const [isValid, setIsValid] = useState(false) const [kind, setKind] = useState() const [tags, setTags] = useState() - const [created_at, setCreatedAt] = useState() - const [pubkey, setPubkey] = useState() // submittedBy, pubkey from nostr event + const [createdAt, setCreatedAt] = useState() + const [submittedBy, setSubmittedBy] = useState<`npub1${string}`>() // submittedBy, pubkey from nostr event const [id, setId] = useState() const [sig, setSig] = useState() @@ -54,6 +74,8 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { [signer: `npub1${string}`]: SignStatus }>({}) + const [encryptionKey, setEncryptionKey] = useState(null) + useEffect(() => { if (!meta) return ;(async function () { @@ -70,7 +92,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { setTags(tags) // created_at in nostr events are stored in seconds setCreatedAt(fromUnixTimestamp(created_at)) - setPubkey(pubkey) + setSubmittedBy(pubkey as `npub1${string}`) setId(id) setSig(sig) @@ -84,6 +106,31 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { setMarkConfig(markConfig) setZipUrl(zipUrl) + if (meta.keys) { + const { sender, keys } = meta.keys + + // Retrieve the user's public key from the state + const usersPubkey = (store.getState().auth as AuthState).usersPubkey! + const usersNpub = hexToNpub(usersPubkey) + + // Check if the user's public key is in the keys object + if (usersNpub in keys) { + // Instantiate the NostrController to decrypt the encryption key + const nostrController = NostrController.getInstance() + const decrypted = await nostrController + .nip04Decrypt(sender, keys[usersNpub]) + .catch((err) => { + console.log( + 'An error occurred in decrypting encryption key', + err + ) + return null + }) + + setEncryptionKey(decrypted) + } + } + // Parse each signature event and set signer status for (const npub in meta.docSignatures) { try { @@ -125,15 +172,15 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { }, [meta]) return { - modifiedAt: meta.modifiedAt, - createSignature: meta.createSignature, - docSignatures: meta.docSignatures, - keys: meta.keys, + modifiedAt: meta?.modifiedAt, + createSignature: meta?.createSignature, + docSignatures: meta?.docSignatures, + keys: meta?.keys, isValid, kind, tags, - created_at, - pubkey, + createdAt, + submittedBy, id, sig, signers, @@ -143,6 +190,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { title, zipUrl, signedStatus, - signersStatus + signersStatus, + encryptionKey } } diff --git a/src/hooks/useSigitProfiles.tsx b/src/hooks/useSigitProfiles.tsx new file mode 100644 index 0000000..8178dd7 --- /dev/null +++ b/src/hooks/useSigitProfiles.tsx @@ -0,0 +1,70 @@ +import { useEffect, useState } from 'react' +import { ProfileMetadata } from '../types' +import { MetadataController } from '../controllers' +import { npubToHex } from '../utils' +import { Event, kinds } from 'nostr-tools' + +/** + * Extracts profiles from metadata events + * @param pubkeys Array of npubs to check + * @returns ProfileMetadata + */ +export const useSigitProfiles = ( + pubkeys: `npub1${string}`[] +): { [key: string]: ProfileMetadata } => { + const [profileMetadata, setProfileMetadata] = useState<{ + [key: string]: ProfileMetadata + }>({}) + + useEffect(() => { + if (pubkeys.length) { + const metadataController = new MetadataController() + + // Remove duplicate keys + const users = new Set([...pubkeys]) + + const handleMetadataEvent = (key: string) => (event: Event) => { + const metadataContent = + metadataController.extractProfileMetadataContent(event) + + if (metadataContent) { + setProfileMetadata((prev) => ({ + ...prev, + [key]: metadataContent + })) + } + } + + users.forEach((user) => { + const hexKey = npubToHex(user) + if (hexKey && !(hexKey in profileMetadata)) { + metadataController.on(hexKey, (kind: number, event: Event) => { + if (kind === kinds.Metadata) { + handleMetadataEvent(hexKey)(event) + } + }) + + metadataController + .findMetadata(hexKey) + .then((metadataEvent) => { + if (metadataEvent) handleMetadataEvent(hexKey)(metadataEvent) + }) + .catch((err) => { + console.error( + `error occurred in finding metadata for: ${user}`, + err + ) + }) + } + }) + + return () => { + users.forEach((key) => { + metadataController.off(key, handleMetadataEvent(key)) + }) + } + } + }, [pubkeys, profileMetadata]) + + return profileMetadata +} diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 1f6bee1..f77bd48 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -10,22 +10,20 @@ import { } from '@mui/material' import JSZip from 'jszip' import { MuiFileInput } from 'mui-file-input' -import { Event, kinds, verifyEvent } from 'nostr-tools' +import { Event, verifyEvent } from 'nostr-tools' import { useEffect, useState } from 'react' import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' import { UserAvatar } from '../../components/UserAvatar' -import { MetadataController, NostrController } from '../../controllers' +import { NostrController } from '../../controllers' import { CreateSignatureEventContent, Meta, - ProfileMetadata, SignedEventContent } from '../../types' import { decryptArrayBuffer, extractMarksFromSignedMeta, - extractZipUrlAndEncryptionKey, getHash, hexToNpub, unixNow, @@ -51,6 +49,8 @@ import { useSelector } from 'react-redux' import { getLastSignersSig } from '../../utils/sign.ts' import { saveAs } from 'file-saver' import { Container } from '../../components/Container' +import { useSigitMeta } from '../../hooks/useSigitMeta.tsx' +import { useSigitProfiles } from '../../hooks/useSigitProfiles.tsx' export const VerifyPage = () => { const theme = useTheme() @@ -63,52 +63,35 @@ export const VerifyPage = () => { * uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains meta.json * meta will be received in navigation from create & home page in online mode */ - const { uploadedZip, meta: metaInNavState } = location.state || {} + const { uploadedZip, meta } = location.state || {} + + const { submittedBy, zipUrl, encryptionKey, signers, viewers, fileHashes } = + useSigitMeta(meta) + const profiles = useSigitProfiles([ + ...(submittedBy ? [submittedBy] : []), + ...signers, + ...viewers + ]) const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [selectedFile, setSelectedFile] = useState(null) - const [meta, setMeta] = useState(null) - const [submittedBy, setSubmittedBy] = useState() - - const [signers, setSigners] = useState<`npub1${string}`[]>([]) - const [viewers, setViewers] = useState<`npub1${string}`[]>([]) - const [creatorFileHashes, setCreatorFileHashes] = useState<{ - [key: string]: string - }>({}) const [currentFileHashes, setCurrentFileHashes] = useState<{ [key: string]: string | null - }>({}) + }>(fileHashes) const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({}) - const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>( - {} - ) const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) const nostrController = NostrController.getInstance() useEffect(() => { if (uploadedZip) { setSelectedFile(uploadedZip) - } else if (metaInNavState) { + } else if (meta && encryptionKey) { const processSigit = async () => { setIsLoading(true) - setLoadingSpinnerDesc('Extracting zipUrl and encryption key from meta') - - const res = await extractZipUrlAndEncryptionKey(metaInNavState) - if (!res) { - setIsLoading(false) - return - } - - const { - zipUrl, - encryptionKey, - createSignatureEvent, - createSignatureContent - } = res setLoadingSpinnerDesc('Fetching file from file server') axios @@ -175,12 +158,6 @@ export const VerifyPage = () => { setCurrentFileHashes(fileHashes) setFiles(files) - setSigners(createSignatureContent.signers) - setViewers(createSignatureContent.viewers) - setCreatorFileHashes(createSignatureContent.fileHashes) - setSubmittedBy(createSignatureEvent.pubkey) - - setMeta(metaInNavState) setIsLoading(false) } }) @@ -197,49 +174,7 @@ export const VerifyPage = () => { processSigit() } - }, [uploadedZip, metaInNavState]) - - useEffect(() => { - if (submittedBy) { - const metadataController = new MetadataController() - - const users = [submittedBy, ...signers, ...viewers] - - users.forEach((user) => { - const pubkey = npubToHex(user)! - - if (!(pubkey in metadata)) { - const handleMetadataEvent = (event: Event) => { - const metadataContent = - metadataController.extractProfileMetadataContent(event) - if (metadataContent) - setMetadata((prev) => ({ - ...prev, - [pubkey]: metadataContent - })) - } - - metadataController.on(pubkey, (kind: number, event: Event) => { - if (kind === kinds.Metadata) { - handleMetadataEvent(event) - } - }) - - metadataController - .findMetadata(pubkey) - .then((metadataEvent) => { - if (metadataEvent) handleMetadataEvent(metadataEvent) - }) - .catch((err) => { - console.error( - `error occurred in finding metadata for: ${user}`, - err - ) - }) - } - }) - } - }, [submittedBy, signers, viewers, metadata]) + }, [encryptionKey, meta, uploadedZip, zipUrl]) const handleVerify = async () => { if (!selectedFile) return @@ -345,12 +280,6 @@ export const VerifyPage = () => { if (!createSignatureContent) return - setSigners(createSignatureContent.signers) - setViewers(createSignatureContent.viewers) - setCreatorFileHashes(createSignatureContent.fileHashes) - setSubmittedBy(createSignatureEvent.pubkey) - - setMeta(parsedMetaJson) setIsLoading(false) } @@ -451,7 +380,7 @@ export const VerifyPage = () => { } const displayUser = (pubkey: string, verifySignature = false) => { - const profile = metadata[pubkey] + const profile = profiles[pubkey] let isValidSignature = false @@ -682,7 +611,7 @@ export const VerifyPage = () => { {Object.entries(currentFileHashes).map( ([filename, hash], index) => { - const isValidHash = creatorFileHashes[filename] === hash + const isValidHash = fileHashes[filename] === hash return ( diff --git a/src/utils/meta.ts b/src/utils/meta.ts index b3c0c28..74d38b7 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -69,7 +69,7 @@ export enum SigitMetaParseErrorType { export interface SigitCardDisplayInfo { createdAt?: number title?: string - submittedBy?: string + submittedBy?: `npub1${string}` signers: `npub1${string}`[] fileExtensions: string[] signedStatus: SigitStatus @@ -161,7 +161,7 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => { ) sigitInfo.title = createSignatureContent.title - sigitInfo.submittedBy = createSignatureEvent.pubkey + sigitInfo.submittedBy = createSignatureEvent.pubkey as `npub1${string}` sigitInfo.signers = createSignatureContent.signers sigitInfo.fileExtensions = extensions