import { Box, Button, Tooltip, Typography, useTheme } from '@mui/material' import JSZip from 'jszip' import { MuiFileInput } from 'mui-file-input' import { Event, verifyEvent } from 'nostr-tools' import { useEffect, useState } from 'react' import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' import { NostrController } from '../../controllers' import { CreateSignatureEventContent, Meta } from '../../types' import { decryptArrayBuffer, extractMarksFromSignedMeta, getHash, hexToNpub, unixNow, parseJson, readContentOfZipEntry, signEventForMetaFile, shorten } from '../../utils' import styles from './style.module.scss' import { Cancel, CheckCircle } from '@mui/icons-material' import { useLocation } from 'react-router-dom' import axios from 'axios' import { PdfFile } from '../../types/drawing.ts' import { addMarks, convertToPdfBlob, convertToPdfFile, groupMarksByPage } from '../../utils/pdf.ts' import { State } from '../../store/rootReducer.ts' 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 { StickySideColumns } from '../../layouts/StickySideColumns.tsx' import { UsersDetails } from '../../components/UsersDetails.tsx/index.tsx' import { UserAvatar } from '../../components/UserAvatar/index.tsx' import { useSigitProfiles } from '../../hooks/useSigitProfiles.tsx' import { TooltipChild } from '../../components/TooltipChild.tsx' export const VerifyPage = () => { const theme = useTheme() const textColor = theme.palette.getContrastText( theme.palette.background.paper ) const location = useLocation() /** * 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 } = location.state || {} const { submittedBy, zipUrl, encryptionKey, signers, viewers, fileHashes, parsedSignatureEvents, createdAt, signedStatus, completedAt, signersStatus } = useSigitMeta(meta) const profiles = useSigitProfiles([ ...(submittedBy ? [submittedBy] : []), ...signers, ...viewers ]) const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [selectedFile, setSelectedFile] = useState(null) const [currentFileHashes, setCurrentFileHashes] = useState<{ [key: string]: string | null }>(fileHashes) const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({}) const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) const nostrController = NostrController.getInstance() useEffect(() => { if (uploadedZip) { setSelectedFile(uploadedZip) } else if (meta && encryptionKey) { const processSigit = async () => { setIsLoading(true) setLoadingSpinnerDesc('Fetching file from file server') axios .get(zipUrl, { responseType: 'arraybuffer' }) .then(async (res) => { const fileName = zipUrl.split('/').pop() const file = new File([res.data], fileName!) const encryptedArrayBuffer = await file.arrayBuffer() const arrayBuffer = await decryptArrayBuffer( encryptedArrayBuffer, encryptionKey ).catch((err) => { console.log('err in decryption:>> ', err) toast.error( err.message || 'An error occurred in decrypting file.' ) return null }) if (arrayBuffer) { const zip = await JSZip.loadAsync(arrayBuffer).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 const files: { [filename: string]: PdfFile } = {} const fileHashes: { [key: string]: string | null } = {} const fileNames = Object.values(zip.files).map( (entry) => entry.name ) // generate hashes for all entries in files folder of zipArchive // these hashes can be used to verify the originality of files for (const fileName of fileNames) { const arrayBuffer = await readContentOfZipEntry( zip, fileName, 'arraybuffer' ) if (arrayBuffer) { files[fileName] = await convertToPdfFile( arrayBuffer, fileName! ) const hash = await getHash(arrayBuffer) if (hash) { fileHashes[fileName.replace(/^files\//, '')] = hash } } else { fileHashes[fileName.replace(/^files\//, '')] = null } } setCurrentFileHashes(fileHashes) setFiles(files) setIsLoading(false) } }) .catch((err) => { console.error(`error occurred in getting file from ${zipUrl}`, err) toast.error( err.message || `error occurred in getting file from ${zipUrl}` ) }) .finally(() => { setIsLoading(false) }) } processSigit() } }, [encryptionKey, meta, uploadedZip, zipUrl]) 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 const fileHashes: { [key: string]: string | null } = {} const fileNames = Object.values(zip.files) .filter((entry) => entry.name.startsWith('files/') && !entry.dir) .map((entry) => entry.name) // generate hashes for all entries in files folder of zipArchive // these hashes can be used to verify the originality of files for (const fileName of fileNames) { const arrayBuffer = await readContentOfZipEntry( zip, fileName, 'arraybuffer' ) if (arrayBuffer) { const hash = await getHash(arrayBuffer) if (hash) { fileHashes[fileName.replace(/^files\//, '')] = hash } } else { fileHashes[fileName.replace(/^files\//, '')] = null } } console.log('fileHashes :>> ', fileHashes) setCurrentFileHashes(fileHashes) 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 } ) if (!parsedMetaJson) return const createSignatureEvent = await parseJson( parsedMetaJson.createSignature ).catch((err) => { console.log('err in parsing the createSignature event:>> ', err) toast.error( err.message || 'error occurred in parsing the create signature event' ) setIsLoading(false) return null }) if (!createSignatureEvent) return const isValidCreateSignature = verifyEvent(createSignatureEvent) if (!isValidCreateSignature) { toast.error('Create signature is invalid') setIsLoading(false) return } const createSignatureContent = await parseJson( createSignatureEvent.content ).catch((err) => { console.log( `err in parsing the createSignature event's content :>> `, err ) toast.error( err.message || `error occurred in parsing the create signature event's content` ) setIsLoading(false) return null }) if (!createSignatureContent) return setIsLoading(false) } const handleExport = async () => { if (Object.entries(files).length === 0 || !meta || !usersPubkey) return const usersNpub = hexToNpub(usersPubkey) if ( !signers.includes(usersNpub) && !viewers.includes(usersNpub) && submittedBy !== usersNpub ) { return } setIsLoading(true) setLoadingSpinnerDesc('Signing nostr event') if (!meta) return const prevSig = getLastSignersSig(meta, signers) if (!prevSig) return const signedEvent = await signEventForMetaFile( JSON.stringify({ prevSig }), nostrController, setIsLoading ) if (!signedEvent) return const exportSignature = JSON.stringify(signedEvent, null, 2) const updatedMeta = { ...meta, exportSignature } const stringifiedMeta = JSON.stringify(updatedMeta, null, 2) const zip = new JSZip() zip.file('meta.json', stringifiedMeta) const marks = extractMarksFromSignedMeta(updatedMeta) const marksByPage = groupMarksByPage(marks) for (const [fileName, pdf] of Object.entries(files)) { const pages = await addMarks(pdf.file, marksByPage) const blob = await convertToPdfBlob(pages) zip.file(`/files/${fileName}`, blob) } const arrayBuffer = await zip .generateAsync({ type: 'arraybuffer', compression: 'DEFLATE', compressionOptions: { level: 6 } }) .catch((err) => { console.log('err in zip:>> ', err) setIsLoading(false) toast.error(err.message || 'Error occurred in generating zip file') return null }) if (!arrayBuffer) return const blob = new Blob([arrayBuffer]) saveAs(blob, `exported-${unixNow()}.sigit.zip`) setIsLoading(false) } const displayExportedBy = () => { if (!meta || !meta.exportSignature) return null const exportSignatureString = meta.exportSignature try { const exportSignatureEvent = JSON.parse(exportSignatureString) as Event if (verifyEvent(exportSignatureEvent)) { const exportedBy = exportSignatureEvent.pubkey const profile = profiles[exportedBy] return ( ) } 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: '.sigit.zip' } }} /> {selectedFile && ( )} )} {meta && ( {Object.entries(currentFileHashes).map( ([filename, hash], index) => { const isValidHash = fileHashes[filename] === hash return ( {filename} {isValidHash && ( )} {!isValidHash && ( )} ) } )} {displayExportedBy()} } right={ } /> )} ) }