From 3f01ab8fcaf7aa94460215418315f56190e4f4b0 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 17 Jan 2025 21:12:31 +0100 Subject: [PATCH] feat(offline): split online and offline flow with dedicated buttons, remove export in sign, all counterparties can decrypt --- src/components/MarkFormField/index.tsx | 48 ++- .../MarkTypeStrategy/Signature/index.tsx | 26 +- src/components/PDFView/PdfMarking.tsx | 42 +- src/pages/create/index.tsx | 16 +- src/pages/home/index.tsx | 34 +- src/pages/sign/index.tsx | 383 ++++-------------- src/pages/verify/index.tsx | 117 +++--- src/utils/sign.ts | 37 +- 8 files changed, 237 insertions(+), 466 deletions(-) diff --git a/src/components/MarkFormField/index.tsx b/src/components/MarkFormField/index.tsx index fde0922..43c94a9 100644 --- a/src/components/MarkFormField/index.tsx +++ b/src/components/MarkFormField/index.tsx @@ -8,15 +8,19 @@ import { import React, { useState } from 'react' import { MarkInput } from '../MarkTypeStrategy/MarkInput.tsx' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faCheck } from '@fortawesome/free-solid-svg-icons' +import { faCheck, faDownload } from '@fortawesome/free-solid-svg-icons' import { Button } from '@mui/material' import styles from './style.module.scss' +import { ButtonUnderline } from '../ButtonUnderline/index.tsx' interface MarkFormFieldProps { currentUserMarks: CurrentUserMark[] handleCurrentUserMarkChange: (mark: CurrentUserMark) => void handleSelectedMarkValueChange: (value: string) => void - handleSubmit: (event: React.MouseEvent) => void + handleSubmit: ( + event: React.MouseEvent, + type: 'online' | 'offline' + ) => void selectedMark: CurrentUserMark | null selectedMarkValue: string } @@ -73,11 +77,11 @@ const MarkFormField = ({ setComplete(true) } - const handleSignAndComplete = ( - event: React.MouseEvent - ) => { - handleSubmit(event) - } + const handleSignAndComplete = + (type: 'online' | 'offline') => + (event: React.MouseEvent) => { + handleSubmit(event, type) + } return (
@@ -129,18 +133,28 @@ const MarkFormField = ({
) : ( -
- +
+ - SIGN AND COMPLETE - - + + Sign and export locally instead + + )}
diff --git a/src/components/MarkTypeStrategy/Signature/index.tsx b/src/components/MarkTypeStrategy/Signature/index.tsx index 5915ab2..aa3429d 100644 --- a/src/components/MarkTypeStrategy/Signature/index.tsx +++ b/src/components/MarkTypeStrategy/Signature/index.tsx @@ -3,7 +3,6 @@ import { decryptArrayBuffer, encryptArrayBuffer, getHash, - isOnline, uploadToFileStorage } from '../../../utils' import { MarkStrategy } from '../MarkStrategy' @@ -37,21 +36,17 @@ export const SignatureStrategy: MarkStrategy = { // Create the encrypted json file from array buffer and hash const file = new File([encryptedArrayBuffer], `${hash}.json`) - if (await isOnline()) { - try { - const url = await uploadToFileStorage(file) - console.info(`${file.name} uploaded to file storage`) - return url - } catch (error) { - if (error instanceof Error) { - console.error( - `Error occurred in uploading file ${file.name}`, - error.message - ) - } + try { + const url = await uploadToFileStorage(file) + console.info(`${file.name} uploaded to file storage`) + return url + } catch (error) { + if (error instanceof Error) { + console.error( + `Error occurred in uploading file ${file.name}`, + error.message + ) } - } else { - // TOOD: offline } return value @@ -89,7 +84,6 @@ export const SignatureStrategy: MarkStrategy = { return json } - // TOOD: offline return value } } diff --git a/src/components/PDFView/PdfMarking.tsx b/src/components/PDFView/PdfMarking.tsx index cbd3907..94e3333 100644 --- a/src/components/PDFView/PdfMarking.tsx +++ b/src/components/PDFView/PdfMarking.tsx @@ -24,9 +24,8 @@ import { interface PdfMarkingProps { currentUserMarks: CurrentUserMark[] files: CurrentUserFile[] - handleExport: () => void - handleEncryptedExport: () => void handleSign: () => void + handleSignOffline: () => void meta: Meta | null otherUserMarks: Mark[] setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void @@ -39,18 +38,16 @@ interface PdfMarkingProps { * @param props * @constructor */ -const PdfMarking = (props: PdfMarkingProps) => { - const { - files, - currentUserMarks, - setCurrentUserMarks, - setUpdatedMarks, - handleExport, - handleEncryptedExport, - handleSign, - meta, - otherUserMarks - } = props +const PdfMarking = ({ + files, + currentUserMarks, + setCurrentUserMarks, + setUpdatedMarks, + handleSign, + handleSignOffline, + meta, + otherUserMarks +}: PdfMarkingProps) => { const [selectedMark, setSelectedMark] = useState(null) const [selectedMarkValue, setSelectedMarkValue] = useState('') const [currentFile, setCurrentFile] = useState(null) @@ -99,7 +96,10 @@ const PdfMarking = (props: PdfMarkingProps) => { /** * Sign and Complete */ - const handleSubmit = (event: React.MouseEvent) => { + const handleSubmit = ( + event: React.MouseEvent, + type: 'online' | 'offline' + ) => { event.preventDefault() if (selectedMarkValue && selectedMark) { const updatedMark: CurrentUserMark = getUpdatedMark( @@ -117,16 +117,10 @@ const PdfMarking = (props: PdfMarkingProps) => { setUpdatedMarks(updatedMark.mark) } - handleSign() + if (type === 'online') handleSign() + else if (type === 'offline') handleSignOffline() } - // const updateCurrentUserMarkValues = () => { - // const updatedMark: CurrentUserMark = getUpdatedMark(selectedMark!, selectedMarkValue) - // const updatedCurrentUserMarks = updateCurrentUserMarks(currentUserMarks, updatedMark) - // setSelectedMarkValue(EMPTY) - // setCurrentUserMarks(updatedCurrentUserMarks) - // } - const handleChange = (value: string) => { setSelectedMarkValue(value) } @@ -142,8 +136,6 @@ const PdfMarking = (props: PdfMarkingProps) => { files={files} currentFile={currentFile} setCurrentFile={setCurrentFile} - handleExport={handleExport} - handleEncryptedExport={handleEncryptedExport} /> )}
diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 4b7989c..cd287b6 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -693,10 +693,18 @@ export const CreatePage = () => { type: 'application/sigit' }) - const firstSigner = users.filter((user) => user.role === UserRole.signer)[0] - + const userSet = new Set() + const nostrController = NostrController.getInstance() + const pubkey = await nostrController.capturePublicKey() + userSet.add(pubkey) + signers.forEach((signer) => { + userSet.add(signer.pubkey) + }) + viewers.forEach((viewer) => { + userSet.add(viewer.pubkey) + }) const keysFileContent = await generateKeysFile( - [firstSigner.pubkey], + Array.from(userSet), encryptionKey ) if (!keysFileContent) return null @@ -998,7 +1006,7 @@ export const CreatePage = () => { // If user is the next signer, we can navigate directly to sign page if (signers[0].pubkey === usersPubkey) { navigate(appPrivateRoutes.sign, { - state: { uploadedZip: finalZipFile } + state: { arrayBuffer } }) } else { navigate(appPrivateRoutes.homePage) diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index d81dd1b..abd3b4e 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -1,10 +1,9 @@ import { Button, TextField } from '@mui/material' -import JSZip from 'jszip' import { useCallback, useEffect, useState } from 'react' import { useNavigate, useSearchParams } from 'react-router-dom' import { toast } from 'react-toastify' import { useAppSelector } from '../../hooks' -import { appPrivateRoutes, appPublicRoutes } from '../../routes' +import { appPrivateRoutes } from '../../routes' import { Meta } from '../../types' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faSearch } from '@fortawesome/free-solid-svg-icons' @@ -15,6 +14,7 @@ import { Container } from '../../components/Container' import styles from './style.module.scss' import { extractSigitCardDisplayInfo, + navigateFromZip, SigitCardDisplayInfo, SigitStatus } from '../../utils' @@ -56,6 +56,7 @@ export const HomePage = () => { [key: string]: SigitCardDisplayInfo }>({}) const usersAppData = useAppSelector((state) => state.userAppData) + const usersPubkey = useAppSelector((state) => state.auth.usersPubkey) useEffect(() => { if (usersAppData?.sigits) { @@ -63,7 +64,7 @@ export const HomePage = () => { const parsedSigits: { [key: string]: SigitCardDisplayInfo } = {} for (const key in usersAppData.sigits) { if (Object.prototype.hasOwnProperty.call(usersAppData.sigits, key)) { - const sigitInfo = await extractSigitCardDisplayInfo( + const sigitInfo = extractSigitCardDisplayInfo( usersAppData.sigits[key] ) if (sigitInfo) { @@ -92,27 +93,12 @@ export const HomePage = () => { const fileName = file.name const fileExtension = fileName.slice(-10) // ".sigit.zip" has 10 characters if (fileExtension === '.sigit.zip') { - const zip = await JSZip.loadAsync(file).catch((err) => { - console.log('err in loading zip file :>> ', err) - toast.error(err.message || 'An error occurred in loading zip file.') - return null - }) + const nav = await navigateFromZip( + file, + usersPubkey as `npub1${string}` + ) - if (!zip) return - - // navigate to sign page if zip contains keys.json - if ('keys.json' in zip.files) { - return navigate(appPrivateRoutes.sign, { - state: { uploadedZip: file } - }) - } - - // navigate to verify page if zip contains meta.json - if ('meta.json' in zip.files) { - return navigate(appPublicRoutes.verify, { - state: { uploadedZip: file } - }) - } + if (nav) return navigate(nav.to, nav.options) toast.error('Invalid SiGit zip file') return @@ -124,7 +110,7 @@ export const HomePage = () => { state: { uploadedFiles: acceptedFiles } }) }, - [navigate] + [navigate, usersPubkey] ) const { getRootProps, getInputProps, isDragActive, open } = useDropzone({ diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index aa5bf74..07ffd4d 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -1,54 +1,41 @@ import axios from 'axios' -import saveAs from 'file-saver' import JSZip from 'jszip' import _ from 'lodash' import { Event, verifyEvent } from 'nostr-tools' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { useAppSelector } from '../../hooks' import { useLocation, useNavigate, useParams } from 'react-router-dom' import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' import { NostrController } from '../../controllers' -import { appPrivateRoutes, appPublicRoutes } from '../../routes' +import { appPublicRoutes } from '../../routes' import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types' import { - ARRAY_BUFFER, decryptArrayBuffer, - DEFLATE, - encryptArrayBuffer, extractMarksFromSignedMeta, extractZipUrlAndEncryptionKey, filterMarksByPubkey, findOtherUserMarks, - generateEncryptionKey, - generateKeysFile, getCurrentUserFiles, getCurrentUserMarks, getHash, hexToNpub, - isOnline, loadZip, npubToHex, parseJson, - processMarks, + encryptAndUploadMarks, readContentOfZipEntry, signEventForMetaFile, - timeout, unixNow, updateMarks, uploadMetaToFileStorage } from '../../utils' import { CurrentUserMark, Mark } from '../../types/mark.ts' import PdfMarking from '../../components/PDFView/PdfMarking.tsx' -import { - convertToSigitFile, - getZipWithFiles, - SigitFile -} from '../../utils/file.ts' +import { convertToSigitFile, SigitFile } from '../../utils/file.ts' import { generateTimestamp } from '../../utils/opentimestamps.ts' import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx' import { useNDK } from '../../hooks/useNDK.ts' -import { getLastSignersSig } from '../../utils/sign.ts' export const SignPage = () => { const navigate = useNavigate() @@ -81,12 +68,10 @@ export const SignPage = () => { /** * Received from `location.state` * - * uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains keys.json * arrayBuffer (decryptedArrayBuffer) will be received in navigation from create page in offline mode */ const { arrayBuffer: decryptedArrayBuffer, uploadedZip } = location.state || { - decryptedArrayBuffer: undefined, - uploadedZip: undefined + decryptedArrayBuffer: undefined } const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({}) @@ -218,63 +203,6 @@ export const SignPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [meta, usersPubkey]) - const decrypt = useCallback( - async (file: File) => { - setLoadingSpinnerDesc('Decrypting file') - - const zip = await loadZip(file) - if (!zip) return - - const parsedKeysJson = await parseKeysJson(zip) - if (!parsedKeysJson) return - - const encryptedArrayBuffer = await readContentOfZipEntry( - zip, - 'compressed.sigit', - 'arraybuffer' - ) - - if (!encryptedArrayBuffer) return - - const { keys, sender } = parsedKeysJson - - for (const key of keys) { - // decrypt the encryptionKey, with timeout (duration = 60 seconds) - const encryptionKey = await Promise.race([ - nostrController.nip04Decrypt(sender, key), - timeout(60000) - ]) - .then((res) => { - return res - }) - .catch((err) => { - console.log('err :>> ', err) - return null - }) - - // Return if encryption failed - if (!encryptionKey) continue - - const arrayBuffer = await decryptArrayBuffer( - encryptedArrayBuffer, - encryptionKey - ) - .catch((err) => { - console.log('err in decryption:>> ', err) - return null - }) - .finally(() => { - setIsLoading(false) - }) - - if (arrayBuffer) return arrayBuffer - } - - return null - }, - [nostrController] - ) - useEffect(() => { if (metaInNavState) { const processSigit = async () => { @@ -316,28 +244,14 @@ export const SignPage = () => { }, []) useEffect(() => { - // online mode - from create and home page views - - if (decryptedArrayBuffer) { - handleDecryptedArrayBuffer(decryptedArrayBuffer).finally(() => - setIsLoading(false) + if (decryptedArrayBuffer || uploadedZip) { + handleDecryptedArrayBuffer(decryptedArrayBuffer || uploadedZip).finally( + () => setIsLoading(false) ) - } else if (uploadedZip) { - decrypt(uploadedZip) - .then((arrayBuffer) => { - if (arrayBuffer) handleDecryptedArrayBuffer(arrayBuffer) - }) - .catch((err) => { - console.error(`error occurred in decryption`, err) - toast.error(err.message || `error occurred in decryption`) - }) - .finally(() => { - setIsLoading(false) - }) } else { setIsLoading(false) } - }, [decryptedArrayBuffer, uploadedZip, decrypt]) + }, [decryptedArrayBuffer, uploadedZip]) const handleArrayBufferFromBlossom = async ( arrayBuffer: ArrayBuffer, @@ -396,30 +310,12 @@ export const SignPage = () => { setMarks(updatedMarks) } - const parseKeysJson = async (zip: JSZip) => { - const keysFileContent = await readContentOfZipEntry( - zip, - 'keys.json', - 'string' - ) - - if (!keysFileContent) return null - - return await parseJson<{ sender: string; keys: string[] }>( - keysFileContent - ).catch((err) => { - console.log(`Error parsing content of keys.json:`, err) - toast.error(err.message || `Error parsing content of keys.json`) - return null - }) - } - - const handleDecryptedArrayBuffer = async (arrayBuffer: ArrayBuffer) => { - const decryptedZipFile = new File([arrayBuffer], 'decrypted.zip') - + const handleDecryptedArrayBuffer = async ( + decryptedArrayBuffer: ArrayBuffer + ) => { setLoadingSpinnerDesc('Parsing zip file') - const zip = await loadZip(decryptedZipFile) + const zip = await loadZip(decryptedArrayBuffer) if (!zip) return const files: { [filename: string]: SigitFile } = {} @@ -479,16 +375,15 @@ export const SignPage = () => { setMeta(parsedMetaJson) } - const handleSign = async () => { + const initializeSigning = async (type: 'online' | 'offline') => { if (Object.entries(files).length === 0 || !meta) return setIsLoading(true) - setLoadingSpinnerDesc('Signing nostr event') + const usersNpub = hexToNpub(usersPubkey!) const prevSig = getPrevSignersSig(usersNpub) if (!prevSig) { - setIsLoading(false) toast.error('Previous signature is invalid') return } @@ -508,7 +403,10 @@ export const SignPage = () => { }) } - const processedMarks = await processMarks(marks, encryptionKey) + const processedMarks = + type === 'online' + ? await encryptAndUploadMarks(marks, encryptionKey) + : marks const signedEvent = await signEventForMeta({ prevSig, @@ -519,6 +417,22 @@ export const SignPage = () => { const updatedMeta = updateMetaSignatures(meta, signedEvent) + return { + encryptionKey, + updatedMeta, + signedEvent + } + } + + const handleSign = async () => { + const result = await initializeSigning('online') + if (!result) { + setIsLoading(false) + return + } + + const { encryptionKey, updatedMeta, signedEvent } = result + setLoadingSpinnerDesc('Generating an open timestamp.') const timestamp = await generateTimestamp(signedEvent.id) @@ -527,19 +441,62 @@ export const SignPage = () => { updatedMeta.modifiedAt = unixNow() } - if (await isOnline()) { - await handleOnlineFlow(updatedMeta, encryptionKey) - } else { - setMeta(updatedMeta) + await handleOnlineFlow(updatedMeta, encryptionKey) + + const createSignature = JSON.parse(updatedMeta.createSignature) + navigate(`${appPublicRoutes.verify}/${createSignature.id}`) + } + + const handleSignOffline = async () => { + const result = await initializeSigning('offline') + if (!result) { setIsLoading(false) + return } - if (metaInNavState) { - const createSignature = JSON.parse(metaInNavState.createSignature) - navigate(`${appPublicRoutes.verify}/${createSignature.id}`) - } else { - navigate(appPrivateRoutes.homePage) + const { updatedMeta } = result + + const zip = new JSZip() + for (const [filename, value] of Object.entries(files)) { + zip.file(`files/${filename}`, await value.arrayBuffer()) } + const stringifiedMeta = JSON.stringify(updatedMeta, null, 2) + zip.file('meta.json', stringifiedMeta) + + // Handle errors during zip file generation + const handleZipError = (err: unknown) => { + console.log('Error in zip:>> ', err) + setIsLoading(false) + if (err instanceof Error) { + toast.error(err.message || 'Error occurred in generating zip file') + } + return null + } + + setLoadingSpinnerDesc('Generating zip file') + + const arrayBuffer = await zip + .generateAsync({ + type: 'arraybuffer', + compression: 'DEFLATE', + compressionOptions: { level: 6 } + }) + .catch(handleZipError) + + if (!arrayBuffer) { + setIsLoading(false) + return + } + + // Create a File object with the Blob data + const blob = new Blob([arrayBuffer]) + const file = new File([blob], `request-${unixNow()}.sigit.zip`, { + type: 'application/zip' + }) + + setIsLoading(false) + + navigate(`${appPublicRoutes.verify}`, { state: { uploadedZip: file } }) } // Sign the event for the meta file @@ -570,66 +527,6 @@ export const SignPage = () => { return metaCopy } - // create final zip file - const createFinalZipFile = async ( - encryptedArrayBuffer: ArrayBuffer, - encryptionKey: string - ): Promise => { - // Get the current timestamp in seconds - const blob = new Blob([encryptedArrayBuffer]) - // Create a File object with the Blob data - const file = new File([blob], `compressed.sigit`, { - type: 'application/sigit' - }) - - const isLastSigner = checkIsLastSigner(signers) - - const userSet = new Set() - - if (isLastSigner) { - if (submittedBy) { - userSet.add(submittedBy) - } - - signers.forEach((signer) => { - userSet.add(npubToHex(signer)!) - }) - - viewers.forEach((viewer) => { - userSet.add(npubToHex(viewer)!) - }) - } else { - const usersNpub = hexToNpub(usersPubkey!) - const signerIndex = signers.indexOf(usersNpub) - const nextSigner = signers[signerIndex + 1] - userSet.add(npubToHex(nextSigner)!) - } - - const keysFileContent = await generateKeysFile( - Array.from(userSet), - encryptionKey - ) - if (!keysFileContent) return null - - const zip = new JSZip() - zip.file(`compressed.sigit`, file) - zip.file('keys.json', keysFileContent) - - const arraybuffer = await zip - .generateAsync({ - type: 'arraybuffer', - compression: 'DEFLATE', - compressionOptions: { level: 6 } - }) - .catch(handleZipError) - - if (!arraybuffer) return null - - return new File([new Blob([arraybuffer])], `${unixNow()}.sigit.zip`, { - type: 'application/zip' - }) - } - // Check if the current user is the last signer const checkIsLastSigner = (signers: string[]): boolean => { const usersNpub = hexToNpub(usersPubkey!) @@ -638,16 +535,6 @@ export const SignPage = () => { return signerIndex === lastSignerIndex } - // Handle errors during zip file generation - const handleZipError = (err: unknown) => { - console.log('Error in zip:>> ', err) - setIsLoading(false) - if (err instanceof Error) { - toast.error(err.message || 'Error occurred in generating zip file') - } - return null - } - // Handle the online flow: update users app data and send notifications const handleOnlineFlow = async ( meta: Meta, @@ -718,99 +605,6 @@ export const SignPage = () => { setIsLoading(false) } - const handleExport = async () => { - const arrayBuffer = await prepareZipExport() - if (!arrayBuffer) return - - const blob = new Blob([arrayBuffer]) - saveAs(blob, `exported-${unixNow()}.sigit.zip`) - - setIsLoading(false) - - navigate(appPublicRoutes.verify) - } - - const handleEncryptedExport = async () => { - const arrayBuffer = await prepareZipExport() - if (!arrayBuffer) return - - const key = await generateEncryptionKey() - - setLoadingSpinnerDesc('Encrypting zip file') - const encryptedArrayBuffer = await encryptArrayBuffer(arrayBuffer, key) - - const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key) - - if (!finalZipFile) return - saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`) - - setIsLoading(false) - } - - const prepareZipExport = async (): Promise => { - if (Object.entries(files).length === 0 || !meta || !usersPubkey) - return Promise.resolve(null) - - const usersNpub = hexToNpub(usersPubkey) - if ( - !signers.includes(usersNpub) && - !viewers.includes(usersNpub) && - submittedBy !== usersNpub - ) - return Promise.resolve(null) - - setIsLoading(true) - setLoadingSpinnerDesc('Signing nostr event') - - if (!meta) return Promise.resolve(null) - - const prevSig = getLastSignersSig(meta, signers) - if (!prevSig) return Promise.resolve(null) - - const signedEvent = await signEventForMetaFile( - JSON.stringify({ - prevSig - }), - nostrController, - setIsLoading - ) - - if (!signedEvent) return Promise.resolve(null) - - const exportSignature = JSON.stringify(signedEvent, null, 2) - - const stringifiedMeta = JSON.stringify( - { - ...meta, - exportSignature - }, - null, - 2 - ) - - const zip = await getZipWithFiles(meta, files) - zip.file('meta.json', stringifiedMeta) - - const arrayBuffer = await zip - .generateAsync({ - type: ARRAY_BUFFER, - 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 Promise.resolve(null) - - return Promise.resolve(arrayBuffer) - } - /** * This function accepts an npub of a signer and return the signature of its previous signer. * This prevSig will be used in the content of the provided signer's signedEvent @@ -855,8 +649,7 @@ export const SignPage = () => { setCurrentUserMarks={setCurrentUserMarks} setUpdatedMarks={setUpdatedMarks} handleSign={handleSign} - handleExport={handleExport} - handleEncryptedExport={handleEncryptedExport} + handleSignOffline={handleSignOffline} otherUserMarks={otherUserMarks} meta={meta} /> diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index e39ca44..402decb 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -1,7 +1,7 @@ import { Box, Button, Typography } from '@mui/material' import JSZip from 'jszip' import { MuiFileInput } from 'mui-file-input' -import { useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' import { NostrController } from '../../controllers' @@ -27,14 +27,14 @@ import { generateKeysFile, ARRAY_BUFFER, DEFLATE, - uploadMetaToFileStorage + uploadMetaToFileStorage, + decrypt } from '../../utils' import styles from './style.module.scss' import { useLocation, useParams } from 'react-router-dom' import axios from 'axios' import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts' import { useAppSelector, useNDK } from '../../hooks' -import { getLastSignersSig } from '../../utils/sign.ts' import { saveAs } from 'file-saver' import { Container } from '../../components/Container' import { useSigitMeta } from '../../hooks/useSigitMeta.tsx' @@ -60,6 +60,7 @@ import { import { upgradeAndVerifyTimestamp } from '../../utils/opentimestamps.ts' import _ from 'lodash' import { MarkRender } from '../../components/MarkTypeStrategy/MarkRender.tsx' +import { SignerService } from '../../services/index.ts' interface PdfViewProps { files: CurrentUserFile[] @@ -185,7 +186,7 @@ export const VerifyPage = () => { * meta will be received in navigation from create & home page in online mode */ let metaInNavState = location?.state?.meta || undefined - const { uploadedZip } = location.state || {} + const uploadedZip = location?.state?.uploadedZip || undefined const [selectedFile, setSelectedFile] = useState(null) /** @@ -205,12 +206,6 @@ export const VerifyPage = () => { } } - useEffect(() => { - if (uploadedZip) { - setSelectedFile(uploadedZip) - } - }, [uploadedZip]) - const [meta, setMeta] = useState(metaInNavState) const { @@ -480,17 +475,35 @@ export const VerifyPage = () => { } }, [encryptionKey, metaInNavState, zipUrl]) - const handleVerify = async () => { - if (!selectedFile) return + const handleVerify = useCallback(async (selectedFile: File) => { setIsLoading(true) + setLoadingSpinnerDesc('Loading zip file') - const zip = await JSZip.loadAsync(selectedFile).catch((err) => { + let 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 + if (!zip) { + return setIsLoading(false) + } + + if ('keys.json' in zip.files) { + // Decrypt + setLoadingSpinnerDesc('Decrypting zip file content') + const arrayBuffer = await decrypt(selectedFile).catch((err) => { + console.error(`error occurred in decryption`, err) + toast.error(err.message || `error occurred in decryption`) + }) + + if (arrayBuffer) { + // Replace the zip and continue processing + zip = await JSZip.loadAsync(arrayBuffer) + } + } + + setLoadingSpinnerDesc('Opening zip file content') const files: { [filename: string]: SigitFile } = {} const fileHashes: { [key: string]: string | null } = {} @@ -547,12 +560,21 @@ export const VerifyPage = () => { } ) - if (!parsedMetaJson) return + if (!parsedMetaJson) { + setIsLoading(false) + return + } setMeta(parsedMetaJson) setIsLoading(false) - } + }, []) + + useEffect(() => { + if (uploadedZip) { + handleVerify(uploadedZip) + } + }, [handleVerify, uploadedZip]) // Handle errors during zip file generation const handleZipError = (err: unknown) => { @@ -564,14 +586,6 @@ export const VerifyPage = () => { return null } - // Check if the current user is the last signer - const checkIsLastSigner = (signers: string[]): boolean => { - const usersNpub = hexToNpub(usersPubkey!) - const lastSignerIndex = signers.length - 1 - const signerIndex = signers.indexOf(usersNpub) - return signerIndex === lastSignerIndex - } - // create final zip file const createFinalZipFile = async ( encryptedArrayBuffer: ArrayBuffer, @@ -584,28 +598,16 @@ export const VerifyPage = () => { type: 'application/sigit' }) - const isLastSigner = checkIsLastSigner(signers) - const userSet = new Set() - - if (isLastSigner) { - if (submittedBy) { - userSet.add(submittedBy) - } - - signers.forEach((signer) => { - userSet.add(npubToHex(signer)!) - }) - - viewers.forEach((viewer) => { - userSet.add(npubToHex(viewer)!) - }) - } else { - const usersNpub = hexToNpub(usersPubkey!) - const signerIndex = signers.indexOf(usersNpub) - const nextSigner = signers[signerIndex + 1] - userSet.add(npubToHex(nextSigner)!) + if (submittedBy) { + userSet.add(submittedBy) } + signers.forEach((signer) => { + userSet.add(npubToHex(signer)!) + }) + viewers.forEach((viewer) => { + userSet.add(npubToHex(viewer)!) + }) const keysFileContent = await generateKeysFile( Array.from(userSet), @@ -634,7 +636,10 @@ export const VerifyPage = () => { const handleExport = async () => { const arrayBuffer = await prepareZipExport() - if (!arrayBuffer) return + if (!arrayBuffer) { + setIsLoading(false) + return + } const blob = new Blob([arrayBuffer]) saveAs(blob, `exported-${unixNow()}.sigit.zip`) @@ -644,7 +649,10 @@ export const VerifyPage = () => { const handleEncryptedExport = async () => { const arrayBuffer = await prepareZipExport() - if (!arrayBuffer) return + if (!arrayBuffer) { + setIsLoading(false) + return + } const key = await generateEncryptionKey() @@ -653,7 +661,11 @@ export const VerifyPage = () => { const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key) - if (!finalZipFile) return + if (!finalZipFile) { + setIsLoading(false) + return + } + saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`) setIsLoading(false) @@ -675,7 +687,11 @@ export const VerifyPage = () => { setIsLoading(true) setLoadingSpinnerDesc('Signing nostr event') - const prevSig = getLastSignersSig(meta, signers) + if (!meta) return Promise.resolve(null) + + const signerService = new SignerService(meta) + const prevSig = signerService.getLastSignerSig() + if (!prevSig) return Promise.resolve(null) const signedEvent = await signEventForMetaFile( @@ -736,7 +752,10 @@ export const VerifyPage = () => { {selectedFile && ( - diff --git a/src/utils/sign.ts b/src/utils/sign.ts index ff67e44..dc3410c 100644 --- a/src/utils/sign.ts +++ b/src/utils/sign.ts @@ -1,46 +1,11 @@ -import { Event } from 'nostr-tools' -import { Meta } from '../types' - -/** - * This function returns the signature of last signer - * It will be used in the content of export signature's signedEvent - */ -const getLastSignersSig = ( - meta: Meta, - signers: `npub1${string}`[] -): string | null => { - // if there're no signers then use creator's signature - if (signers.length === 0) { - try { - const createSignatureEvent: Event = JSON.parse(meta.createSignature) - return createSignatureEvent.sig - } catch (error) { - return null - } - } - - // get last signer - const lastSigner = signers[signers.length - 1] - - // get the signature of last signer - try { - const lastSignatureEvent: Event = JSON.parse(meta.docSignatures[lastSigner]) - return lastSignatureEvent.sig - } catch (error) { - return null - } -} - /** * Checks if all signers have signed the sigit * @param signers - an array of npubs of all signers from the Sigit * @param signedBy - an array of npubs that have signed it already */ -const isFullySigned = ( +export const isFullySigned = ( signers: `npub1${string}`[], signedBy: `npub1${string}`[] ): boolean => { return signers.every((signer) => signedBy.includes(signer)) } - -export { getLastSignersSig, isFullySigned }