Offline flow separation #304
@ -8,15 +8,19 @@ import {
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { MarkInput } from '../MarkTypeStrategy/MarkInput.tsx'
|
import { MarkInput } from '../MarkTypeStrategy/MarkInput.tsx'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
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 { Button } from '@mui/material'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
|
import { ButtonUnderline } from '../ButtonUnderline/index.tsx'
|
||||||
|
|
||||||
interface MarkFormFieldProps {
|
interface MarkFormFieldProps {
|
||||||
currentUserMarks: CurrentUserMark[]
|
currentUserMarks: CurrentUserMark[]
|
||||||
handleCurrentUserMarkChange: (mark: CurrentUserMark) => void
|
handleCurrentUserMarkChange: (mark: CurrentUserMark) => void
|
||||||
handleSelectedMarkValueChange: (value: string) => void
|
handleSelectedMarkValueChange: (value: string) => void
|
||||||
handleSubmit: (event: React.MouseEvent<HTMLButtonElement>) => void
|
handleSubmit: (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement>,
|
||||||
|
type: 'online' | 'offline'
|
||||||
|
) => void
|
||||||
selectedMark: CurrentUserMark | null
|
selectedMark: CurrentUserMark | null
|
||||||
selectedMarkValue: string
|
selectedMarkValue: string
|
||||||
}
|
}
|
||||||
@ -73,11 +77,11 @@ const MarkFormField = ({
|
|||||||
setComplete(true)
|
setComplete(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSignAndComplete = (
|
const handleSignAndComplete =
|
||||||
event: React.MouseEvent<HTMLButtonElement>
|
(type: 'online' | 'offline') =>
|
||||||
) => {
|
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
handleSubmit(event)
|
handleSubmit(event, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
@ -129,18 +133,28 @@ const MarkFormField = ({
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.actionsBottom}>
|
<>
|
||||||
<Button
|
<div className={styles.actionsBottom}>
|
||||||
onClick={handleSignAndComplete}
|
<Button
|
||||||
className={[styles.submitButton, styles.completeButton].join(
|
onClick={handleSignAndComplete('online')}
|
||||||
' '
|
className={[
|
||||||
)}
|
styles.submitButton,
|
||||||
|
styles.completeButton
|
||||||
|
].join(' ')}
|
||||||
|
disabled={!isReadyToSign()}
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
SIGN AND BROADCAST
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ButtonUnderline
|
||||||
|
onClick={handleSignAndComplete('offline')}
|
||||||
disabled={!isReadyToSign()}
|
disabled={!isReadyToSign()}
|
||||||
autoFocus
|
|
||||||
>
|
>
|
||||||
SIGN AND COMPLETE
|
<FontAwesomeIcon icon={faDownload} />
|
||||||
</Button>
|
Sign and export locally instead
|
||||||
</div>
|
</ButtonUnderline>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.footerContainer}>
|
<div className={styles.footerContainer}>
|
||||||
|
@ -3,7 +3,6 @@ import {
|
|||||||
decryptArrayBuffer,
|
decryptArrayBuffer,
|
||||||
encryptArrayBuffer,
|
encryptArrayBuffer,
|
||||||
getHash,
|
getHash,
|
||||||
isOnline,
|
|
||||||
uploadToFileStorage
|
uploadToFileStorage
|
||||||
} from '../../../utils'
|
} from '../../../utils'
|
||||||
import { MarkStrategy } from '../MarkStrategy'
|
import { MarkStrategy } from '../MarkStrategy'
|
||||||
@ -37,21 +36,17 @@ export const SignatureStrategy: MarkStrategy = {
|
|||||||
// Create the encrypted json file from array buffer and hash
|
// Create the encrypted json file from array buffer and hash
|
||||||
const file = new File([encryptedArrayBuffer], `${hash}.json`)
|
const file = new File([encryptedArrayBuffer], `${hash}.json`)
|
||||||
|
|
||||||
if (await isOnline()) {
|
try {
|
||||||
try {
|
const url = await uploadToFileStorage(file)
|
||||||
const url = await uploadToFileStorage(file)
|
console.info(`${file.name} uploaded to file storage`)
|
||||||
console.info(`${file.name} uploaded to file storage`)
|
return url
|
||||||
return url
|
} catch (error) {
|
||||||
} catch (error) {
|
if (error instanceof Error) {
|
||||||
if (error instanceof Error) {
|
console.error(
|
||||||
console.error(
|
`Error occurred in uploading file ${file.name}`,
|
||||||
`Error occurred in uploading file ${file.name}`,
|
error.message
|
||||||
error.message
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// TOOD: offline
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return value
|
return value
|
||||||
@ -89,7 +84,6 @@ export const SignatureStrategy: MarkStrategy = {
|
|||||||
return json
|
return json
|
||||||
}
|
}
|
||||||
|
|
||||||
// TOOD: offline
|
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,9 +24,8 @@ import {
|
|||||||
interface PdfMarkingProps {
|
interface PdfMarkingProps {
|
||||||
currentUserMarks: CurrentUserMark[]
|
currentUserMarks: CurrentUserMark[]
|
||||||
files: CurrentUserFile[]
|
files: CurrentUserFile[]
|
||||||
handleExport: () => void
|
|
||||||
handleEncryptedExport: () => void
|
|
||||||
handleSign: () => void
|
handleSign: () => void
|
||||||
|
handleSignOffline: () => void
|
||||||
meta: Meta | null
|
meta: Meta | null
|
||||||
otherUserMarks: Mark[]
|
otherUserMarks: Mark[]
|
||||||
setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void
|
setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void
|
||||||
@ -39,18 +38,16 @@ interface PdfMarkingProps {
|
|||||||
* @param props
|
* @param props
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
const PdfMarking = (props: PdfMarkingProps) => {
|
const PdfMarking = ({
|
||||||
const {
|
files,
|
||||||
files,
|
currentUserMarks,
|
||||||
currentUserMarks,
|
setCurrentUserMarks,
|
||||||
setCurrentUserMarks,
|
setUpdatedMarks,
|
||||||
setUpdatedMarks,
|
handleSign,
|
||||||
handleExport,
|
handleSignOffline,
|
||||||
handleEncryptedExport,
|
meta,
|
||||||
handleSign,
|
otherUserMarks
|
||||||
meta,
|
}: PdfMarkingProps) => {
|
||||||
otherUserMarks
|
|
||||||
} = props
|
|
||||||
const [selectedMark, setSelectedMark] = useState<CurrentUserMark | null>(null)
|
const [selectedMark, setSelectedMark] = useState<CurrentUserMark | null>(null)
|
||||||
const [selectedMarkValue, setSelectedMarkValue] = useState<string>('')
|
const [selectedMarkValue, setSelectedMarkValue] = useState<string>('')
|
||||||
const [currentFile, setCurrentFile] = useState<CurrentUserFile | null>(null)
|
const [currentFile, setCurrentFile] = useState<CurrentUserFile | null>(null)
|
||||||
@ -99,7 +96,10 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
|||||||
/**
|
/**
|
||||||
* Sign and Complete
|
* Sign and Complete
|
||||||
*/
|
*/
|
||||||
const handleSubmit = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const handleSubmit = (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement>,
|
||||||
|
type: 'online' | 'offline'
|
||||||
|
) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (selectedMarkValue && selectedMark) {
|
if (selectedMarkValue && selectedMark) {
|
||||||
const updatedMark: CurrentUserMark = getUpdatedMark(
|
const updatedMark: CurrentUserMark = getUpdatedMark(
|
||||||
@ -117,16 +117,10 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
|||||||
setUpdatedMarks(updatedMark.mark)
|
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) => {
|
const handleChange = (value: string) => {
|
||||||
setSelectedMarkValue(value)
|
setSelectedMarkValue(value)
|
||||||
}
|
}
|
||||||
@ -142,8 +136,6 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
|||||||
files={files}
|
files={files}
|
||||||
currentFile={currentFile}
|
currentFile={currentFile}
|
||||||
setCurrentFile={setCurrentFile}
|
setCurrentFile={setCurrentFile}
|
||||||
handleExport={handleExport}
|
|
||||||
handleEncryptedExport={handleEncryptedExport}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -693,10 +693,18 @@ export const CreatePage = () => {
|
|||||||
type: 'application/sigit'
|
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(
|
const keysFileContent = await generateKeysFile(
|
||||||
[firstSigner.pubkey],
|
Array.from(userSet),
|
||||||
encryptionKey
|
encryptionKey
|
||||||
)
|
)
|
||||||
if (!keysFileContent) return null
|
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 user is the next signer, we can navigate directly to sign page
|
||||||
if (signers[0].pubkey === usersPubkey) {
|
if (signers[0].pubkey === usersPubkey) {
|
||||||
navigate(appPrivateRoutes.sign, {
|
navigate(appPrivateRoutes.sign, {
|
||||||
state: { uploadedZip: finalZipFile }
|
state: { arrayBuffer }
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
navigate(appPrivateRoutes.homePage)
|
navigate(appPrivateRoutes.homePage)
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { Button, TextField } from '@mui/material'
|
import { Button, TextField } from '@mui/material'
|
||||||
import JSZip from 'jszip'
|
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { useAppSelector } from '../../hooks'
|
import { useAppSelector } from '../../hooks'
|
||||||
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
|
import { appPrivateRoutes } from '../../routes'
|
||||||
import { Meta } from '../../types'
|
import { Meta } from '../../types'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faSearch } from '@fortawesome/free-solid-svg-icons'
|
import { faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||||
@ -15,6 +14,7 @@ import { Container } from '../../components/Container'
|
|||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import {
|
import {
|
||||||
extractSigitCardDisplayInfo,
|
extractSigitCardDisplayInfo,
|
||||||
|
navigateFromZip,
|
||||||
SigitCardDisplayInfo,
|
SigitCardDisplayInfo,
|
||||||
SigitStatus
|
SigitStatus
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
@ -56,6 +56,7 @@ export const HomePage = () => {
|
|||||||
[key: string]: SigitCardDisplayInfo
|
[key: string]: SigitCardDisplayInfo
|
||||||
}>({})
|
}>({})
|
||||||
const usersAppData = useAppSelector((state) => state.userAppData)
|
const usersAppData = useAppSelector((state) => state.userAppData)
|
||||||
|
const usersPubkey = useAppSelector((state) => state.auth.usersPubkey)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (usersAppData?.sigits) {
|
if (usersAppData?.sigits) {
|
||||||
@ -63,7 +64,7 @@ export const HomePage = () => {
|
|||||||
const parsedSigits: { [key: string]: SigitCardDisplayInfo } = {}
|
const parsedSigits: { [key: string]: SigitCardDisplayInfo } = {}
|
||||||
for (const key in usersAppData.sigits) {
|
for (const key in usersAppData.sigits) {
|
||||||
if (Object.prototype.hasOwnProperty.call(usersAppData.sigits, key)) {
|
if (Object.prototype.hasOwnProperty.call(usersAppData.sigits, key)) {
|
||||||
const sigitInfo = await extractSigitCardDisplayInfo(
|
const sigitInfo = extractSigitCardDisplayInfo(
|
||||||
usersAppData.sigits[key]
|
usersAppData.sigits[key]
|
||||||
)
|
)
|
||||||
if (sigitInfo) {
|
if (sigitInfo) {
|
||||||
@ -92,27 +93,12 @@ export const HomePage = () => {
|
|||||||
const fileName = file.name
|
const fileName = file.name
|
||||||
const fileExtension = fileName.slice(-10) // ".sigit.zip" has 10 characters
|
const fileExtension = fileName.slice(-10) // ".sigit.zip" has 10 characters
|
||||||
if (fileExtension === '.sigit.zip') {
|
if (fileExtension === '.sigit.zip') {
|
||||||
const zip = await JSZip.loadAsync(file).catch((err) => {
|
const nav = await navigateFromZip(
|
||||||
console.log('err in loading zip file :>> ', err)
|
file,
|
||||||
toast.error(err.message || 'An error occurred in loading zip file.')
|
usersPubkey as `npub1${string}`
|
||||||
return null
|
)
|
||||||
})
|
|
||||||
|
|
||||||
if (!zip) return
|
if (nav) return navigate(nav.to, nav.options)
|
||||||
|
|
||||||
// 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 }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.error('Invalid SiGit zip file')
|
toast.error('Invalid SiGit zip file')
|
||||||
return
|
return
|
||||||
@ -124,7 +110,7 @@ export const HomePage = () => {
|
|||||||
state: { uploadedFiles: acceptedFiles }
|
state: { uploadedFiles: acceptedFiles }
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[navigate]
|
[navigate, usersPubkey]
|
||||||
)
|
)
|
||||||
|
|
||||||
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
|
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
|
||||||
|
@ -1,54 +1,41 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import saveAs from 'file-saver'
|
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { Event, verifyEvent } from 'nostr-tools'
|
import { Event, verifyEvent } from 'nostr-tools'
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { useAppSelector } from '../../hooks'
|
import { useAppSelector } from '../../hooks'
|
||||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
import { NostrController } from '../../controllers'
|
import { NostrController } from '../../controllers'
|
||||||
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
|
import { appPublicRoutes } from '../../routes'
|
||||||
import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types'
|
import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types'
|
||||||
import {
|
import {
|
||||||
ARRAY_BUFFER,
|
|
||||||
decryptArrayBuffer,
|
decryptArrayBuffer,
|
||||||
DEFLATE,
|
|
||||||
encryptArrayBuffer,
|
|
||||||
extractMarksFromSignedMeta,
|
extractMarksFromSignedMeta,
|
||||||
extractZipUrlAndEncryptionKey,
|
extractZipUrlAndEncryptionKey,
|
||||||
filterMarksByPubkey,
|
filterMarksByPubkey,
|
||||||
findOtherUserMarks,
|
findOtherUserMarks,
|
||||||
generateEncryptionKey,
|
|
||||||
generateKeysFile,
|
|
||||||
getCurrentUserFiles,
|
getCurrentUserFiles,
|
||||||
getCurrentUserMarks,
|
getCurrentUserMarks,
|
||||||
getHash,
|
getHash,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
isOnline,
|
|
||||||
loadZip,
|
loadZip,
|
||||||
npubToHex,
|
npubToHex,
|
||||||
parseJson,
|
parseJson,
|
||||||
processMarks,
|
encryptAndUploadMarks,
|
||||||
readContentOfZipEntry,
|
readContentOfZipEntry,
|
||||||
signEventForMetaFile,
|
signEventForMetaFile,
|
||||||
timeout,
|
|
||||||
unixNow,
|
unixNow,
|
||||||
updateMarks,
|
updateMarks,
|
||||||
uploadMetaToFileStorage
|
uploadMetaToFileStorage
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import { CurrentUserMark, Mark } from '../../types/mark.ts'
|
import { CurrentUserMark, Mark } from '../../types/mark.ts'
|
||||||
import PdfMarking from '../../components/PDFView/PdfMarking.tsx'
|
import PdfMarking from '../../components/PDFView/PdfMarking.tsx'
|
||||||
import {
|
import { convertToSigitFile, SigitFile } from '../../utils/file.ts'
|
||||||
convertToSigitFile,
|
|
||||||
getZipWithFiles,
|
|
||||||
SigitFile
|
|
||||||
} from '../../utils/file.ts'
|
|
||||||
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
||||||
import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx'
|
import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx'
|
||||||
import { useNDK } from '../../hooks/useNDK.ts'
|
import { useNDK } from '../../hooks/useNDK.ts'
|
||||||
import { getLastSignersSig } from '../../utils/sign.ts'
|
|
||||||
|
|
||||||
export const SignPage = () => {
|
export const SignPage = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -81,12 +68,10 @@ export const SignPage = () => {
|
|||||||
/**
|
/**
|
||||||
* Received from `location.state`
|
* 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
|
* arrayBuffer (decryptedArrayBuffer) will be received in navigation from create page in offline mode
|
||||||
*/
|
*/
|
||||||
const { arrayBuffer: decryptedArrayBuffer, uploadedZip } = location.state || {
|
const { arrayBuffer: decryptedArrayBuffer, uploadedZip } = location.state || {
|
||||||
decryptedArrayBuffer: undefined,
|
decryptedArrayBuffer: undefined
|
||||||
uploadedZip: undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
|
const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
|
||||||
@ -218,63 +203,6 @@ export const SignPage = () => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [meta, usersPubkey])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (metaInNavState) {
|
if (metaInNavState) {
|
||||||
const processSigit = async () => {
|
const processSigit = async () => {
|
||||||
@ -316,28 +244,14 @@ export const SignPage = () => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// online mode - from create and home page views
|
if (decryptedArrayBuffer || uploadedZip) {
|
||||||
|
handleDecryptedArrayBuffer(decryptedArrayBuffer || uploadedZip).finally(
|
||||||
if (decryptedArrayBuffer) {
|
() => setIsLoading(false)
|
||||||
handleDecryptedArrayBuffer(decryptedArrayBuffer).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 {
|
} else {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}, [decryptedArrayBuffer, uploadedZip, decrypt])
|
}, [decryptedArrayBuffer, uploadedZip])
|
||||||
|
|
||||||
const handleArrayBufferFromBlossom = async (
|
const handleArrayBufferFromBlossom = async (
|
||||||
arrayBuffer: ArrayBuffer,
|
arrayBuffer: ArrayBuffer,
|
||||||
@ -396,30 +310,12 @@ export const SignPage = () => {
|
|||||||
setMarks(updatedMarks)
|
setMarks(updatedMarks)
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseKeysJson = async (zip: JSZip) => {
|
const handleDecryptedArrayBuffer = async (
|
||||||
const keysFileContent = await readContentOfZipEntry(
|
decryptedArrayBuffer: ArrayBuffer
|
||||||
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')
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Parsing zip file')
|
setLoadingSpinnerDesc('Parsing zip file')
|
||||||
|
|
||||||
const zip = await loadZip(decryptedZipFile)
|
const zip = await loadZip(decryptedArrayBuffer)
|
||||||
if (!zip) return
|
if (!zip) return
|
||||||
|
|
||||||
const files: { [filename: string]: SigitFile } = {}
|
const files: { [filename: string]: SigitFile } = {}
|
||||||
@ -479,16 +375,15 @@ export const SignPage = () => {
|
|||||||
setMeta(parsedMetaJson)
|
setMeta(parsedMetaJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSign = async () => {
|
const initializeSigning = async (type: 'online' | 'offline') => {
|
||||||
if (Object.entries(files).length === 0 || !meta) return
|
if (Object.entries(files).length === 0 || !meta) return
|
||||||
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Signing nostr event')
|
setLoadingSpinnerDesc('Signing nostr event')
|
||||||
|
|
||||||
const usersNpub = hexToNpub(usersPubkey!)
|
const usersNpub = hexToNpub(usersPubkey!)
|
||||||
const prevSig = getPrevSignersSig(usersNpub)
|
const prevSig = getPrevSignersSig(usersNpub)
|
||||||
if (!prevSig) {
|
if (!prevSig) {
|
||||||
setIsLoading(false)
|
|
||||||
toast.error('Previous signature is invalid')
|
toast.error('Previous signature is invalid')
|
||||||
return
|
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({
|
const signedEvent = await signEventForMeta({
|
||||||
prevSig,
|
prevSig,
|
||||||
@ -519,6 +417,22 @@ export const SignPage = () => {
|
|||||||
|
|
||||||
const updatedMeta = updateMetaSignatures(meta, signedEvent)
|
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.')
|
setLoadingSpinnerDesc('Generating an open timestamp.')
|
||||||
|
|
||||||
const timestamp = await generateTimestamp(signedEvent.id)
|
const timestamp = await generateTimestamp(signedEvent.id)
|
||||||
@ -527,19 +441,62 @@ export const SignPage = () => {
|
|||||||
updatedMeta.modifiedAt = unixNow()
|
updatedMeta.modifiedAt = unixNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await isOnline()) {
|
await handleOnlineFlow(updatedMeta, encryptionKey)
|
||||||
await handleOnlineFlow(updatedMeta, encryptionKey)
|
|
||||||
} else {
|
const createSignature = JSON.parse(updatedMeta.createSignature)
|
||||||
setMeta(updatedMeta)
|
navigate(`${appPublicRoutes.verify}/${createSignature.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSignOffline = async () => {
|
||||||
|
const result = await initializeSigning('offline')
|
||||||
|
if (!result) {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metaInNavState) {
|
const { updatedMeta } = result
|
||||||
const createSignature = JSON.parse(metaInNavState.createSignature)
|
|
||||||
navigate(`${appPublicRoutes.verify}/${createSignature.id}`)
|
const zip = new JSZip()
|
||||||
} else {
|
for (const [filename, value] of Object.entries(files)) {
|
||||||
navigate(appPrivateRoutes.homePage)
|
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
|
// Sign the event for the meta file
|
||||||
@ -570,66 +527,6 @@ export const SignPage = () => {
|
|||||||
return metaCopy
|
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
|
// Check if the current user is the last signer
|
||||||
const checkIsLastSigner = (signers: string[]): boolean => {
|
const checkIsLastSigner = (signers: string[]): boolean => {
|
||||||
const usersNpub = hexToNpub(usersPubkey!)
|
const usersNpub = hexToNpub(usersPubkey!)
|
||||||
@ -638,16 +535,6 @@ export const SignPage = () => {
|
|||||||
return signerIndex === lastSignerIndex
|
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
|
// Handle the online flow: update users app data and send notifications
|
||||||
const handleOnlineFlow = async (
|
const handleOnlineFlow = async (
|
||||||
meta: Meta,
|
meta: Meta,
|
||||||
@ -718,99 +605,6 @@ export const SignPage = () => {
|
|||||||
setIsLoading(false)
|
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 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
|
* This prevSig will be used in the content of the provided signer's signedEvent
|
||||||
@ -855,8 +649,7 @@ export const SignPage = () => {
|
|||||||
setCurrentUserMarks={setCurrentUserMarks}
|
setCurrentUserMarks={setCurrentUserMarks}
|
||||||
setUpdatedMarks={setUpdatedMarks}
|
setUpdatedMarks={setUpdatedMarks}
|
||||||
handleSign={handleSign}
|
handleSign={handleSign}
|
||||||
handleExport={handleExport}
|
handleSignOffline={handleSignOffline}
|
||||||
handleEncryptedExport={handleEncryptedExport}
|
|
||||||
otherUserMarks={otherUserMarks}
|
otherUserMarks={otherUserMarks}
|
||||||
meta={meta}
|
meta={meta}
|
||||||
/>
|
/>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Box, Button, Typography } from '@mui/material'
|
import { Box, Button, Typography } from '@mui/material'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import { MuiFileInput } from 'mui-file-input'
|
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 { toast } from 'react-toastify'
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
import { NostrController } from '../../controllers'
|
import { NostrController } from '../../controllers'
|
||||||
@ -27,14 +27,14 @@ import {
|
|||||||
generateKeysFile,
|
generateKeysFile,
|
||||||
ARRAY_BUFFER,
|
ARRAY_BUFFER,
|
||||||
DEFLATE,
|
DEFLATE,
|
||||||
uploadMetaToFileStorage
|
uploadMetaToFileStorage,
|
||||||
|
decrypt
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { useLocation, useParams } from 'react-router-dom'
|
import { useLocation, useParams } from 'react-router-dom'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
|
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
|
||||||
import { useAppSelector, useNDK } from '../../hooks'
|
import { useAppSelector, useNDK } from '../../hooks'
|
||||||
import { getLastSignersSig } from '../../utils/sign.ts'
|
|
||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
import { Container } from '../../components/Container'
|
import { Container } from '../../components/Container'
|
||||||
import { useSigitMeta } from '../../hooks/useSigitMeta.tsx'
|
import { useSigitMeta } from '../../hooks/useSigitMeta.tsx'
|
||||||
@ -60,6 +60,7 @@ import {
|
|||||||
import { upgradeAndVerifyTimestamp } from '../../utils/opentimestamps.ts'
|
import { upgradeAndVerifyTimestamp } from '../../utils/opentimestamps.ts'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { MarkRender } from '../../components/MarkTypeStrategy/MarkRender.tsx'
|
import { MarkRender } from '../../components/MarkTypeStrategy/MarkRender.tsx'
|
||||||
|
import { SignerService } from '../../services/index.ts'
|
||||||
|
|
||||||
interface PdfViewProps {
|
interface PdfViewProps {
|
||||||
files: CurrentUserFile[]
|
files: CurrentUserFile[]
|
||||||
@ -185,7 +186,7 @@ export const VerifyPage = () => {
|
|||||||
* meta will be received in navigation from create & home page in online mode
|
* meta will be received in navigation from create & home page in online mode
|
||||||
*/
|
*/
|
||||||
let metaInNavState = location?.state?.meta || undefined
|
let metaInNavState = location?.state?.meta || undefined
|
||||||
const { uploadedZip } = location.state || {}
|
const uploadedZip = location?.state?.uploadedZip || undefined
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
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 [meta, setMeta] = useState<Meta>(metaInNavState)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -480,17 +475,35 @@ export const VerifyPage = () => {
|
|||||||
}
|
}
|
||||||
}, [encryptionKey, metaInNavState, zipUrl])
|
}, [encryptionKey, metaInNavState, zipUrl])
|
||||||
|
|
||||||
const handleVerify = async () => {
|
const handleVerify = useCallback(async (selectedFile: File) => {
|
||||||
if (!selectedFile) return
|
|
||||||
setIsLoading(true)
|
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)
|
console.log('err in loading zip file :>> ', err)
|
||||||
toast.error(err.message || 'An error occurred in loading zip file.')
|
toast.error(err.message || 'An error occurred in loading zip file.')
|
||||||
return null
|
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 files: { [filename: string]: SigitFile } = {}
|
||||||
const fileHashes: { [key: string]: string | null } = {}
|
const fileHashes: { [key: string]: string | null } = {}
|
||||||
@ -547,12 +560,21 @@ export const VerifyPage = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!parsedMetaJson) return
|
if (!parsedMetaJson) {
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setMeta(parsedMetaJson)
|
setMeta(parsedMetaJson)
|
||||||
|
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (uploadedZip) {
|
||||||
|
handleVerify(uploadedZip)
|
||||||
|
}
|
||||||
|
}, [handleVerify, uploadedZip])
|
||||||
|
|
||||||
// Handle errors during zip file generation
|
// Handle errors during zip file generation
|
||||||
const handleZipError = (err: unknown) => {
|
const handleZipError = (err: unknown) => {
|
||||||
@ -564,14 +586,6 @@ export const VerifyPage = () => {
|
|||||||
return null
|
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
|
// create final zip file
|
||||||
const createFinalZipFile = async (
|
const createFinalZipFile = async (
|
||||||
encryptedArrayBuffer: ArrayBuffer,
|
encryptedArrayBuffer: ArrayBuffer,
|
||||||
@ -584,28 +598,16 @@ export const VerifyPage = () => {
|
|||||||
type: 'application/sigit'
|
type: 'application/sigit'
|
||||||
})
|
})
|
||||||
|
|
||||||
const isLastSigner = checkIsLastSigner(signers)
|
|
||||||
|
|
||||||
const userSet = new Set<string>()
|
const userSet = new Set<string>()
|
||||||
|
if (submittedBy) {
|
||||||
if (isLastSigner) {
|
userSet.add(submittedBy)
|
||||||
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)!)
|
|
||||||
}
|
}
|
||||||
|
signers.forEach((signer) => {
|
||||||
|
userSet.add(npubToHex(signer)!)
|
||||||
|
})
|
||||||
|
viewers.forEach((viewer) => {
|
||||||
|
userSet.add(npubToHex(viewer)!)
|
||||||
|
})
|
||||||
|
|
||||||
const keysFileContent = await generateKeysFile(
|
const keysFileContent = await generateKeysFile(
|
||||||
Array.from(userSet),
|
Array.from(userSet),
|
||||||
@ -634,7 +636,10 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
const arrayBuffer = await prepareZipExport()
|
const arrayBuffer = await prepareZipExport()
|
||||||
if (!arrayBuffer) return
|
if (!arrayBuffer) {
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const blob = new Blob([arrayBuffer])
|
const blob = new Blob([arrayBuffer])
|
||||||
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
|
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
|
||||||
@ -644,7 +649,10 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
const handleEncryptedExport = async () => {
|
const handleEncryptedExport = async () => {
|
||||||
const arrayBuffer = await prepareZipExport()
|
const arrayBuffer = await prepareZipExport()
|
||||||
if (!arrayBuffer) return
|
if (!arrayBuffer) {
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const key = await generateEncryptionKey()
|
const key = await generateEncryptionKey()
|
||||||
|
|
||||||
@ -653,7 +661,11 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key)
|
const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key)
|
||||||
|
|
||||||
if (!finalZipFile) return
|
if (!finalZipFile) {
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`)
|
saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`)
|
||||||
|
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
@ -675,7 +687,11 @@ export const VerifyPage = () => {
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setLoadingSpinnerDesc('Signing nostr event')
|
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)
|
if (!prevSig) return Promise.resolve(null)
|
||||||
|
|
||||||
const signedEvent = await signEventForMetaFile(
|
const signedEvent = await signEventForMetaFile(
|
||||||
@ -736,7 +752,10 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
{selectedFile && (
|
{selectedFile && (
|
||||||
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
|
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
|
||||||
<Button onClick={handleVerify} variant="contained">
|
<Button
|
||||||
|
onClick={() => handleVerify(selectedFile)}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
Verify
|
Verify
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -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
|
* Checks if all signers have signed the sigit
|
||||||
* @param signers - an array of npubs of all signers from 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
|
* @param signedBy - an array of npubs that have signed it already
|
||||||
*/
|
*/
|
||||||
const isFullySigned = (
|
export const isFullySigned = (
|
||||||
signers: `npub1${string}`[],
|
signers: `npub1${string}`[],
|
||||||
signedBy: `npub1${string}`[]
|
signedBy: `npub1${string}`[]
|
||||||
): boolean => {
|
): boolean => {
|
||||||
return signers.every((signer) => signedBy.includes(signer))
|
return signers.every((signer) => signedBy.includes(signer))
|
||||||
}
|
}
|
||||||
|
|
||||||
export { getLastSignersSig, isFullySigned }
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user