Offline flow separation #304

Merged
b merged 13 commits from 231-offline-flow into staging 2025-01-22 13:05:30 +00:00
8 changed files with 237 additions and 466 deletions
Showing only changes of commit 3f01ab8fca - Show all commits

View File

@ -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<HTMLButtonElement>) => void
handleSubmit: (
event: React.MouseEvent<HTMLButtonElement>,
type: 'online' | 'offline'
) => void
selectedMark: CurrentUserMark | null
selectedMarkValue: string
}
@ -73,11 +77,11 @@ const MarkFormField = ({
setComplete(true)
}
const handleSignAndComplete = (
event: React.MouseEvent<HTMLButtonElement>
) => {
handleSubmit(event)
}
const handleSignAndComplete =
(type: 'online' | 'offline') =>
(event: React.MouseEvent<HTMLButtonElement>) => {
handleSubmit(event, type)
}
return (
<div className={styles.container}>
@ -129,18 +133,28 @@ const MarkFormField = ({
</div>
</form>
) : (
<div className={styles.actionsBottom}>
<Button
onClick={handleSignAndComplete}
className={[styles.submitButton, styles.completeButton].join(
' '
)}
<>
<div className={styles.actionsBottom}>
<Button
onClick={handleSignAndComplete('online')}
className={[
styles.submitButton,
styles.completeButton
].join(' ')}
disabled={!isReadyToSign()}
autoFocus
>
SIGN AND BROADCAST
</Button>
</div>
<ButtonUnderline
onClick={handleSignAndComplete('offline')}
disabled={!isReadyToSign()}
autoFocus
>
SIGN AND COMPLETE
</Button>
</div>
<FontAwesomeIcon icon={faDownload} />
Sign and export locally instead
</ButtonUnderline>
</>
)}
<div className={styles.footerContainer}>

View File

@ -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
}
}

View File

@ -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<CurrentUserMark | null>(null)
const [selectedMarkValue, setSelectedMarkValue] = useState<string>('')
const [currentFile, setCurrentFile] = useState<CurrentUserFile | null>(null)
@ -99,7 +96,10 @@ const PdfMarking = (props: PdfMarkingProps) => {
/**
* Sign and Complete
*/
const handleSubmit = (event: React.MouseEvent<HTMLButtonElement>) => {
const handleSubmit = (
event: React.MouseEvent<HTMLButtonElement>,
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}
/>
)}
</div>

View File

@ -693,10 +693,18 @@ export const CreatePage = () => {
type: 'application/sigit'
})
const firstSigner = users.filter((user) => user.role === UserRole.signer)[0]
const userSet = new Set<string>()
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)

View File

@ -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({

View File

@ -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<File | null> => {
// 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<string>()
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<ArrayBuffer | null> => {
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}
/>

View File

@ -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<File | null>(null)
/**
@ -205,12 +206,6 @@ export const VerifyPage = () => {
}
}
useEffect(() => {
if (uploadedZip) {
setSelectedFile(uploadedZip)
}
}, [uploadedZip])
const [meta, setMeta] = useState<Meta>(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<string>()
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 && (
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleVerify} variant="contained">
<Button
onClick={() => handleVerify(selectedFile)}
variant="contained"
>
Verify
</Button>
</Box>

View File

@ -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 }