import { Box, Button, List, ListItem, ListSubheader, Table, TableBody, TableCell, TableHead, TableRow, TextField, Typography, useTheme } from '@mui/material' import axios from 'axios' import saveAs from 'file-saver' import JSZip from 'jszip' import _ from 'lodash' import { MuiFileInput } from 'mui-file-input' import { EventTemplate } from 'nostr-tools' import { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { Link, useSearchParams } from 'react-router-dom' import { toast } from 'react-toastify' import placeholderAvatar from '../../assets/images/nostr-logo.jpg' import { LoadingSpinner } from '../../components/LoadingSpinner' import { MetadataController, NostrController } from '../../controllers' import { getProfileRoute } from '../../routes' import { State } from '../../store/rootReducer' import { Meta, ProfileMetadata, User, UserRole } from '../../types' import { decryptArrayBuffer, encryptArrayBuffer, generateEncryptionKey, getHash, hexToNpub, parseJson, readContentOfZipEntry, sendDM, shorten, signEventForMetaFile, uploadToFileStorage } from '../../utils' import styles from './style.module.scss' enum SignedStatus { Fully_Signed, User_Is_Next_Signer, User_Is_Not_Next_Signer } export const VerifyPage = () => { const [searchParams] = useSearchParams() const [displayInput, setDisplayInput] = useState(false) const [selectedFile, setSelectedFile] = useState(null) const [encryptionKey, setEncryptionKey] = useState('') const [zip, setZip] = useState() const [isLoading, setIsLoading] = useState(true) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [meta, setMeta] = useState(null) const [signedStatus, setSignedStatus] = useState() const [nextSinger, setNextSinger] = useState() const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) const [authUrl, setAuthUrl] = useState() const nostrController = NostrController.getInstance() useEffect(() => { if (meta) { setDisplayInput(false) // get list of users who have signed const signedBy = Object.keys(meta.signedEvents) if (meta.signers.length > 0) { // check if all signers have signed then its fully signed if (meta.signers.every((signer) => signedBy.includes(signer))) { setSignedStatus(SignedStatus.Fully_Signed) } else { for (const signer of meta.signers) { if (!signedBy.includes(signer)) { setNextSinger(signer) if (signer === usersPubkey) { // logged in user is the next signer setSignedStatus(SignedStatus.User_Is_Next_Signer) } else { setSignedStatus(SignedStatus.User_Is_Not_Next_Signer) } break } } } } else { // there's no signer just viewers. So its fully signed setSignedStatus(SignedStatus.Fully_Signed) } } }, [meta, usersPubkey]) useEffect(() => { const fileUrl = searchParams.get('file') const key = searchParams.get('key') if (fileUrl && key) { setIsLoading(true) setLoadingSpinnerDesc('Fetching file from file server') axios .get(fileUrl, { responseType: 'arraybuffer' }) .then((res) => { const fileName = fileUrl.split('/').pop() const file = new File([res.data], fileName!) decrypt(file, key).then((arrayBuffer) => { if (arrayBuffer) handleDecryptedArrayBuffer(arrayBuffer) }) }) .catch((err) => { console.error(`error occurred in getting file from ${fileUrl}`, err) toast.error( err.message || `error occurred in getting file from ${fileUrl}` ) }) .finally(() => { setIsLoading(false) }) } else { setIsLoading(false) setDisplayInput(true) } }, [searchParams]) const decrypt = async (file: File, key: string) => { setLoadingSpinnerDesc('Decrypting file') const encryptedArrayBuffer = await file.arrayBuffer() const arrayBuffer = await decryptArrayBuffer(encryptedArrayBuffer, key) .catch((err) => { console.log('err in decryption:>> ', err) toast.error(err.message || 'An error occurred in decrypting file.') return null }) .finally(() => { setIsLoading(false) }) return arrayBuffer } const handleDecryptedArrayBuffer = async (arrayBuffer: ArrayBuffer) => { const decryptedZipFile = new File([arrayBuffer], 'decrypted.zip') setLoadingSpinnerDesc('Parsing zip file') const zip = await JSZip.loadAsync(decryptedZipFile).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 setZip(zip) 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) } const handleDecrypt = async () => { if (!selectedFile || !encryptionKey) return setIsLoading(true) const arrayBuffer = await decrypt(selectedFile, encryptionKey) if (!arrayBuffer) return handleDecryptedArrayBuffer(arrayBuffer) } const handleSign = async () => { if (!zip || !meta) return setIsLoading(true) setLoadingSpinnerDesc('parsing hashes.json file') const hashesFileContent = await readContentOfZipEntry( zip, 'hashes.json', 'string' ) if (!hashesFileContent) { setIsLoading(false) return } let hashes = await parseJson(hashesFileContent).catch((err) => { console.log('err in parsing the content of hashes.json :>> ', err) toast.error( err.message || 'error occurred in parsing the content of hashes.json' ) setIsLoading(false) return null }) if (!hashes) return setLoadingSpinnerDesc('Generating hashes for files') const fileHashes: { [key: string]: string } = {} const fileNames = Object.keys(meta.fileHashes) // 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 filePath = `files/${fileName}` const arrayBuffer = await readContentOfZipEntry( zip, filePath, 'arraybuffer' ) if (!arrayBuffer) { setIsLoading(false) return } const hash = await getHash(arrayBuffer) if (!hash) { setIsLoading(false) return } fileHashes[fileName] = hash } setLoadingSpinnerDesc('Signing nostr event') const signedEvent = await signEventForMetaFile( fileHashes, nostrController, setIsLoading ) if (!signedEvent) return const metaCopy = _.cloneDeep(meta) metaCopy.signedEvents = { ...metaCopy.signedEvents, [signedEvent.pubkey]: JSON.stringify(signedEvent, null, 2) } const stringifiedMeta = JSON.stringify(metaCopy, null, 2) zip.file('meta.json', stringifiedMeta) const metaHash = await getHash(stringifiedMeta) if (!metaHash) return hashes = { ...hashes, [usersPubkey!]: metaHash } zip.file('hashes.json', JSON.stringify(hashes, null, 2)) 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 encryptionKey = await generateEncryptionKey() setLoadingSpinnerDesc('Encrypting zip file') const encryptedArrayBuffer = await encryptArrayBuffer( arrayBuffer, encryptionKey ) const blob = new Blob([encryptedArrayBuffer]) setLoadingSpinnerDesc('Uploading zip file to file storage.') const fileUrl = await uploadToFileStorage(blob, nostrController) .then((url) => { toast.success('zip file uploaded to file storage') return url }) .catch((err) => { console.log('err in upload:>> ', err) setIsLoading(false) toast.error(err.message || 'Error occurred in uploading zip file') return null }) if (!fileUrl) return // check if the current user is the last signer const lastSignerIndex = meta.signers.length - 1 const signerIndex = meta.signers.indexOf(usersPubkey!) const isLastSigner = signerIndex === lastSignerIndex // if current user is the last signer, then send DMs to all signers and viewers if (isLastSigner) { const userSet = new Set() userSet.add(meta.submittedBy) meta.signers.forEach((signer) => { userSet.add(signer) }) meta.viewers.forEach((viewer) => { userSet.add(viewer) }) const users = Array.from(userSet) for (const user of users) { // todo: execute in parallel await sendDM( fileUrl, encryptionKey, user, nostrController, false, setAuthUrl ) } } else { const nextSigner = meta.signers[signerIndex + 1] await sendDM( fileUrl, encryptionKey, nextSigner, nostrController, false, setAuthUrl ) } setIsLoading(false) } const handleExport = async () => { if (!meta || !zip || !usersPubkey) return if ( !meta.signers.includes(usersPubkey) && !meta.viewers.includes(usersPubkey) && meta.submittedBy !== usersPubkey ) return setIsLoading(true) setLoadingSpinnerDesc('Signing nostr event') const event: EventTemplate = { kind: 1, content: '', created_at: Math.floor(Date.now() / 1000), // Current timestamp tags: [] } // Sign the event const signedEvent = await nostrController.signEvent(event).catch((err) => { console.error(err) toast.error(err.message || 'Error occurred in signing nostr event') setIsLoading(false) // Set loading state to false return null }) if (!signedEvent) return const exportSignature = JSON.stringify(signedEvent, null, 2) const stringifiedMeta = JSON.stringify( { ...meta, exportSignature }, null, 2 ) zip.file('meta.json', stringifiedMeta) 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.zip') setIsLoading(false) } if (authUrl) { return (