diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 3789411..98525a4 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -221,57 +221,67 @@ export const CreatePage = () => { ) } - const handleCreate = async () => { + // Validate inputs before proceeding + const validateInputs = (): boolean => { if (!title.trim()) { toast.error('Title can not be empty') - return + return false } if (users.length === 0) { toast.error( - 'No signer/viewer is provided. At least add one signer or viewer.' + 'No signer/viewer is provided. At least add one signer or viewer.' ) - return + return false } if (selectedFiles.length === 0) { toast.error('No file is selected. Select at least 1 file') - return + return false } - setIsLoading(true) - setLoadingSpinnerDesc('Generating hashes for files') + return true + } + // Handle errors during file arrayBuffer conversion + const handleFileError = (file: File) => (err: any) => { + console.log( + `Error while getting arrayBuffer of file ${file.name} :>> `, + err + ) + toast.error( + err.message || `Error while getting arrayBuffer of file ${file.name}` + ) + return null + } + + // Generate hash for each selected file + const generateFileHashes = async (): Promise<{ + [key: string]: string + } | null> => { const fileHashes: { [key: string]: string } = {} - // generating file hashes for (const file of selectedFiles) { - const arraybuffer = await file.arrayBuffer().catch((err) => { - console.log( - `err while getting arrayBuffer of file ${file.name} :>> `, - err - ) - toast.error( - err.message || `err while getting arrayBuffer of file ${file.name}` - ) - return null - }) - - if (!arraybuffer) return + const arraybuffer = await file.arrayBuffer().catch(handleFileError(file)) + if (!arraybuffer) return null const hash = await getHash(arraybuffer) - if (!hash) { - setIsLoading(false) - return + return null } fileHashes[file.name] = hash } + return fileHashes + } + + // Create a zip file with the selected files and sign the event + const createZipFile = async (fileHashes: { + [key: string]: string + }): Promise<{ zip: JSZip; createSignature: string } | null> => { const zip = new JSZip() - // zipping files selectedFiles.forEach((file) => { zip.file(`files/${file.name}`, file) }) @@ -280,6 +290,7 @@ export const CreatePage = () => { const viewers = users.filter((user) => user.role === UserRole.viewer) setLoadingSpinnerDesc('Signing nostr event') + const createSignature = await signEventForMetaFile( JSON.stringify({ signers: signers.map((signer) => hexToNpub(signer.pubkey)), @@ -290,12 +301,27 @@ export const CreatePage = () => { setIsLoading ) - if (!createSignature) return + if (!createSignature) return null + try { + return { + zip, + createSignature: JSON.stringify(createSignature, null, 2) + } + } catch (error) { + return null + } + } + + // Add metadata and file hashes to the zip file + const addMetaToZip = async ( + zip: JSZip, + createSignature: string + ): Promise => { // create content for meta file const meta: Meta = { title, - createSignature: JSON.stringify(createSignature, null, 2), + createSignature, docSignatures: {} } @@ -304,112 +330,176 @@ export const CreatePage = () => { zip.file('meta.json', stringifiedMeta) const metaHash = await getHash(stringifiedMeta) - if (!metaHash) return + if (!metaHash) return null const metaHashJson = { [usersPubkey!]: metaHash } zip.file('hashes.json', JSON.stringify(metaHashJson, null, 2)) + return metaHash } catch (err) { console.error(err) toast.error('An error occurred in converting meta json to string') - return + return null } + } + // Handle errors during zip file generation + const handleZipError = (err: any) => { + console.log('Error in zip:>> ', err) + setIsLoading(false) + toast.error(err.message || 'Error occurred in generating zip file') + return null + } + + // Generate the zip file + const generateZipFile = async (zip: JSZip): Promise => { setLoadingSpinnerDesc('Generating zip file') 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 + compressionOptions: { level: 6 } }) + .catch(handleZipError) + return arraybuffer + } + + // Encrypt the zip file with the generated encryption key + const encryptZipFile = async ( + arraybuffer: ArrayBuffer, + encryptionKey: string + ): Promise => { + setLoadingSpinnerDesc('Encrypting zip file') + return encryptArrayBuffer(arraybuffer, encryptionKey).finally(() => + setIsLoading(false) + ) + } + + // Handle file upload and further actions based on online/offline status + const handleFileUpload = async (blob: Blob, encryptionKey: string) => { + if (await isOnline()) { + const fileUrl = await uploadFile(blob) + + if (!fileUrl) return + + await sendDMs(fileUrl, encryptionKey) + setIsLoading(false) + + navigate( + `${appPrivateRoutes.sign}?file=${encodeURIComponent(fileUrl)}&key=${encodeURIComponent(encryptionKey)}` + ) + } else { + handleOffline(blob, encryptionKey) + } + } + + // Handle errors during file upload + const handleUploadError = (err: any) => { + console.log('Error in upload:>> ', err) + setIsLoading(false) + toast.error(err.message || 'Error occurred in uploading zip file') + return null + } + + // Upload the file to the storage and send DMs to signers/viewers + const uploadFile = async (blob: Blob): Promise => { + setIsLoading(true) + 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(handleUploadError) + + return fileUrl + } + + // Send DMs to signers and viewers with the file URL and encryption key + const sendDMs = async (fileUrl: string, encryptionKey: string) => { + setLoadingSpinnerDesc('Sending DM to signers/viewers') + + const signers = users.filter((user) => user.role === UserRole.signer) + const viewers = users.filter((user) => user.role === UserRole.viewer) + + if (signers.length > 0) { + await sendDM( + fileUrl, + encryptionKey, + signers[0].pubkey, + nostrController, + true, + setAuthUrl + ) + } else { + for (const viewer of viewers) { + await sendDM( + fileUrl, + encryptionKey, + viewer.pubkey, + nostrController, + false, + setAuthUrl + ) + } + } + } + + // Manage offline scenarios for signing or viewing the file + const handleOffline = (blob: Blob, encryptionKey: string) => { + const signers = users.filter((user) => user.role === UserRole.signer) + + if (signers[0] && signers[0].pubkey === usersPubkey) { + // Create a File object with the Blob data for offline signing + const file = new File([blob], `compressed.sigit`, { + type: 'application/sigit' + }) + navigate(appPrivateRoutes.sign, { state: { file, encryptionKey } }) + } else { + // Save the file and show encryption key for offline viewing + saveAs(blob, 'request.sigit') + setTextToCopy(encryptionKey) + setOpenCopyModel(true) + } + } + + const handleCreate = async () => { + if (!validateInputs()) return + + setIsLoading(true) + setLoadingSpinnerDesc('Generating hashes for files') + + const fileHashes = await generateFileHashes() + if (!fileHashes) return + + const createZipResponse = await createZipFile(fileHashes) + if (!createZipResponse) return + + const { zip, createSignature } = createZipResponse + + const metaHash = await addMetaToZip(zip, createSignature) + if (!metaHash) return + + setLoadingSpinnerDesc('Generating zip file') + + const arraybuffer = await generateZipFile(zip) if (!arraybuffer) return const encryptionKey = await generateEncryptionKey() setLoadingSpinnerDesc('Encrypting zip file') - const encryptedArrayBuffer = await encryptArrayBuffer( + const encryptedArrayBuffer = await encryptZipFile( arraybuffer, encryptionKey - ).finally(() => setIsLoading(false)) - + ) const blob = new Blob([encryptedArrayBuffer]) - if (await isOnline()) { - setIsLoading(true) - 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 - - setLoadingSpinnerDesc('Sending DM to signers/viewers') - - // send DM to first signer if exists - if (signers.length > 0) { - await sendDM( - fileUrl, - encryptionKey, - signers[0].pubkey, - nostrController, - true, - setAuthUrl - ) - } else { - // send DM to all viewers if no signer - for (const viewer of viewers) { - // todo: execute in parallel - await sendDM( - fileUrl, - encryptionKey, - viewer.pubkey, - nostrController, - false, - setAuthUrl - ) - } - } - setIsLoading(false) - - navigate( - `${appPrivateRoutes.sign}?file=${encodeURIComponent( - fileUrl - )}&key=${encodeURIComponent(encryptionKey)}` - ) - } else { - if (signers[0] && signers[0].pubkey === usersPubkey) { - // Create a File object with the Blob data - const file = new File([blob], `compressed.sigit`, { - type: 'application/sigit' - }) - - navigate(appPrivateRoutes.sign, { state: { file, encryptionKey } }) - } else { - saveAs(blob, 'request.sigit') - setTextToCopy(encryptionKey) - setOpenCopyModel(true) - } - } + return await handleFileUpload(blob, encryptionKey) } if (authUrl) {