sigit.io/src/pages/sign/index.tsx

998 lines
28 KiB
TypeScript
Raw Normal View History

import { Box, Button, Typography } from '@mui/material'
2024-05-14 09:27:05 +00:00
import axios from 'axios'
import saveAs from 'file-saver'
import JSZip from 'jszip'
import _ from 'lodash'
import { MuiFileInput } from 'mui-file-input'
import { Event, verifyEvent } from 'nostr-tools'
import { useCallback, useEffect, useState } from 'react'
2024-05-14 09:27:05 +00:00
import { useSelector } from 'react-redux'
2024-09-10 14:00:48 +00:00
import { useLocation, useNavigate, useParams } from 'react-router-dom'
2024-05-14 09:27:05 +00:00
import { toast } from 'react-toastify'
import { LoadingSpinner } from '../../components/LoadingSpinner'
import { NostrController } from '../../controllers'
import { appPublicRoutes } from '../../routes'
2024-05-14 09:27:05 +00:00
import { State } from '../../store/rootReducer'
2024-07-05 08:38:04 +00:00
import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types'
2024-05-14 09:27:05 +00:00
import {
decryptArrayBuffer,
encryptArrayBuffer,
extractMarksFromSignedMeta,
2024-07-05 08:38:04 +00:00
extractZipUrlAndEncryptionKey,
2024-05-14 09:27:05 +00:00
generateEncryptionKey,
generateKeysFile,
getCurrentUserFiles,
2024-05-14 09:27:05 +00:00
getHash,
hexToNpub,
isOnline,
loadZip,
unixNow,
npubToHex,
parseJson,
2024-05-14 09:27:05 +00:00
readContentOfZipEntry,
2024-06-28 09:24:14 +00:00
sendNotification,
2024-05-14 09:27:05 +00:00
signEventForMetaFile,
updateUsersAppData,
findOtherUserMarks
2024-05-14 09:27:05 +00:00
} from '../../utils'
2024-08-06 09:46:42 +00:00
import { Container } from '../../components/Container'
import { DisplayMeta } from './internal/displayMeta'
2024-05-14 09:27:05 +00:00
import styles from './style.module.scss'
2024-08-02 10:40:00 +00:00
import { CurrentUserMark, Mark } from '../../types/mark.ts'
2024-08-27 13:17:34 +00:00
import { getLastSignersSig, isFullySigned } from '../../utils/sign.ts'
2024-08-02 10:40:00 +00:00
import {
filterMarksByPubkey,
getCurrentUserMarks,
isCurrentUserMarksComplete,
updateMarks
2024-08-06 09:35:59 +00:00
} from '../../utils'
2024-08-02 10:40:00 +00:00
import PdfMarking from '../../components/PDFView/PdfMarking.tsx'
import {
convertToSigitFile,
getZipWithFiles,
SigitFile
} from '../../utils/file.ts'
import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts'
2024-09-10 14:00:48 +00:00
import { useAppSelector } from '../../hooks/store.ts'
2024-05-14 09:27:05 +00:00
enum SignedStatus {
Fully_Signed,
User_Is_Next_Signer,
User_Is_Not_Next_Signer
}
2024-05-15 11:11:57 +00:00
export const SignPage = () => {
const navigate = useNavigate()
const location = useLocation()
2024-09-10 14:00:48 +00:00
const params = useParams()
2024-07-08 20:16:47 +00:00
2024-09-11 09:59:12 +00:00
const usersAppData = useAppSelector((state) => state.userAppData)
2024-07-08 20:16:47 +00:00
/**
2024-09-10 14:00:48 +00:00
* Received from `location.state`
*
2024-07-08 20:16:47 +00:00
* uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains keys.json
* arrayBuffer will be received in navigation from create page in offline mode
* meta will be received in navigation from create & home page in online mode
*/
2024-09-11 09:59:12 +00:00
const {
meta: metaInNavState,
arrayBuffer: decryptedArrayBuffer,
uploadedZip
} = location.state || {
meta: undefined,
arrayBuffer: undefined,
uploadedZip: undefined
}
if (usersAppData) {
const sigitId = params.id
if (sigitId) {
const sigit = usersAppData.sigits[sigitId]
if (sigit) {
metaInNavState = sigit
}
}
}
2024-05-14 09:27:05 +00:00
const [displayInput, setDisplayInput] = useState(false)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
2024-05-14 09:27:05 +00:00
const [isLoading, setIsLoading] = useState(true)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const [meta, setMeta] = useState<Meta | null>(null)
const [signedStatus, setSignedStatus] = useState<SignedStatus>()
2024-05-22 06:19:40 +00:00
const [submittedBy, setSubmittedBy] = useState<string>()
const [signers, setSigners] = useState<`npub1${string}`[]>([])
const [viewers, setViewers] = useState<`npub1${string}`[]>([])
const [marks, setMarks] = useState<Mark[]>([])
2024-05-22 06:19:40 +00:00
const [creatorFileHashes, setCreatorFileHashes] = useState<{
[key: string]: string
}>({})
const [currentFileHashes, setCurrentFileHashes] = useState<{
[key: string]: string | null
}>({})
const [signedBy, setSignedBy] = useState<`npub1${string}`[]>([])
2024-05-14 09:27:05 +00:00
const [nextSinger, setNextSinger] = useState<string>()
// This state variable indicates whether the logged-in user is a signer, a creator, or neither.
const [isSignerOrCreator, setIsSignerOrCreator] = useState(false)
2024-05-14 09:27:05 +00:00
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
const [authUrl, setAuthUrl] = useState<string>()
const nostrController = NostrController.getInstance()
const [currentUserMarks, setCurrentUserMarks] = useState<CurrentUserMark[]>(
[]
)
2024-08-27 13:17:34 +00:00
const [isMarksCompleted, setIsMarksCompleted] = useState(false)
const [otherUserMarks, setOtherUserMarks] = useState<Mark[]>([])
2024-05-14 09:27:05 +00:00
2024-05-22 06:19:40 +00:00
useEffect(() => {
if (signers.length > 0) {
// check if all signers have signed then its fully signed
2024-08-27 13:17:34 +00:00
if (isFullySigned(signers, signedBy)) {
2024-05-14 09:27:05 +00:00
setSignedStatus(SignedStatus.Fully_Signed)
2024-05-22 06:19:40 +00:00
} else {
for (const signer of signers) {
if (!signedBy.includes(signer)) {
// signers in meta.json are in npub1 format
// so, convert it to hex before setting to nextSigner
setNextSinger(npubToHex(signer)!)
const usersNpub = hexToNpub(usersPubkey!)
if (signer === usersNpub) {
// logged in user is the next signer
setSignedStatus(SignedStatus.User_Is_Next_Signer)
} else {
setSignedStatus(SignedStatus.User_Is_Not_Next_Signer)
}
break
}
}
2024-05-14 09:27:05 +00:00
}
2024-05-22 06:19:40 +00:00
} else {
// there's no signer just viewers. So its fully signed
setSignedStatus(SignedStatus.Fully_Signed)
2024-05-14 09:27:05 +00:00
}
// Determine and set the status of the user
if (submittedBy && usersPubkey && submittedBy === usersPubkey) {
// If the submission was made by the user, set the status to true
setIsSignerOrCreator(true)
} else if (usersPubkey) {
// Convert the user's public key from hex to npub format
const usersNpub = hexToNpub(usersPubkey)
if (signers.includes(usersNpub)) {
// If the user's npub is in the list of signers, set the status to true
setIsSignerOrCreator(true)
}
}
}, [signers, signedBy, usersPubkey, submittedBy])
2024-05-14 09:27:05 +00:00
useEffect(() => {
2024-06-28 09:24:14 +00:00
const handleUpdatedMeta = async (meta: Meta) => {
const createSignatureEvent = await parseJson<Event>(
meta.createSignature
).catch((err) => {
console.log('err in parsing the createSignature event:>> ', err)
toast.error(
err.message || 'error occurred in parsing the create signature event'
)
setIsLoading(false)
return null
})
2024-05-14 09:27:05 +00:00
2024-06-28 09:24:14 +00:00
if (!createSignatureEvent) return
2024-05-14 09:27:05 +00:00
2024-06-28 09:24:14 +00:00
const isValidCreateSignature = verifyEvent(createSignatureEvent)
2024-06-12 14:44:06 +00:00
2024-06-28 09:24:14 +00:00
if (!isValidCreateSignature) {
toast.error('Create signature is invalid')
setIsLoading(false)
return
}
const createSignatureContent =
await parseJson<CreateSignatureEventContent>(
createSignatureEvent.content
).catch((err) => {
console.log(
`err in parsing the createSignature event's content :>> `,
err
)
2024-05-14 09:27:05 +00:00
toast.error(
2024-06-28 09:24:14 +00:00
err.message ||
`error occurred in parsing the create signature event's content`
2024-05-14 09:27:05 +00:00
)
setIsLoading(false)
2024-06-28 09:24:14 +00:00
return null
2024-05-14 09:27:05 +00:00
})
2024-06-28 09:24:14 +00:00
if (!createSignatureContent) return
setSigners(createSignatureContent.signers)
setViewers(createSignatureContent.viewers)
setCreatorFileHashes(createSignatureContent.fileHashes)
setSubmittedBy(createSignatureEvent.pubkey)
setMarks(createSignatureContent.markConfig)
if (usersPubkey) {
const metaMarks = filterMarksByPubkey(
createSignatureContent.markConfig,
usersPubkey!
)
2024-08-02 10:40:00 +00:00
const signedMarks = extractMarksFromSignedMeta(meta)
const currentUserMarks = getCurrentUserMarks(metaMarks, signedMarks)
const otherUserMarks = findOtherUserMarks(signedMarks, usersPubkey!)
setOtherUserMarks(otherUserMarks)
setCurrentUserMarks(currentUserMarks)
2024-08-27 13:17:34 +00:00
setIsMarksCompleted(isCurrentUserMarksComplete(currentUserMarks))
}
2024-06-28 09:24:14 +00:00
setSignedBy(Object.keys(meta.docSignatures) as `npub1${string}`[])
}
if (meta) {
handleUpdatedMeta(meta)
}
}, [meta, usersPubkey])
const handleDownload = async () => {
if (Object.entries(files).length === 0 || !meta || !usersPubkey) return
setLoadingSpinnerDesc('Generating file')
try {
const zip = await getZipWithFiles(meta, files)
const arrayBuffer = await zip.generateAsync({
type: ARRAY_BUFFER,
compression: DEFLATE,
compressionOptions: {
level: 6
}
})
if (!arrayBuffer) return
const blob = new Blob([arrayBuffer])
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
} catch (error) {
console.log('error in zip:>> ', error)
if (error instanceof Error) {
toast.error(error.message || 'Error occurred in generating zip file')
}
}
}
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) {
// Set up event listener for authentication event
nostrController.on('nsecbunker-auth', (url) => {
setAuthUrl(url)
})
// Set up timeout promise to handle encryption timeout
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error('Timeout occurred'))
}, 60000) // Timeout duration = 60 seconds
})
// decrypt the encryptionKey, with timeout
const encryptionKey = await Promise.race([
nostrController.nip04Decrypt(sender, key),
timeoutPromise
])
.then((res) => {
return res
})
.catch((err) => {
console.log('err :>> ', err)
return null
})
.finally(() => {
setAuthUrl(undefined) // Clear authentication URL
})
// 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]
)
2024-06-28 09:24:14 +00:00
useEffect(() => {
2024-08-02 10:40:00 +00:00
// online mode - from create and home page views
2024-07-05 08:38:04 +00:00
if (metaInNavState) {
2024-06-28 09:24:14 +00:00
const processSigit = async () => {
setIsLoading(true)
2024-07-05 08:38:04 +00:00
setLoadingSpinnerDesc('Extracting zipUrl and encryption key from meta')
2024-06-28 09:24:14 +00:00
2024-07-05 08:38:04 +00:00
const res = await extractZipUrlAndEncryptionKey(metaInNavState)
if (!res) {
2024-06-28 09:24:14 +00:00
setIsLoading(false)
return
}
2024-07-05 08:38:04 +00:00
const { zipUrl, encryptionKey } = res
2024-06-28 09:24:14 +00:00
setLoadingSpinnerDesc('Fetching file from file server')
axios
2024-07-05 08:38:04 +00:00
.get(zipUrl, {
2024-06-28 09:24:14 +00:00
responseType: 'arraybuffer'
})
2024-07-05 08:38:04 +00:00
.then((res) => {
handleArrayBufferFromBlossom(res.data, encryptionKey)
setMeta(metaInNavState)
2024-06-28 09:24:14 +00:00
})
.catch((err) => {
2024-07-05 08:38:04 +00:00
console.error(`error occurred in getting file from ${zipUrl}`, err)
2024-06-28 09:24:14 +00:00
toast.error(
2024-07-05 08:38:04 +00:00
err.message || `error occurred in getting file from ${zipUrl}`
2024-06-28 09:24:14 +00:00
)
})
.finally(() => {
setIsLoading(false)
})
}
processSigit()
} else if (decryptedArrayBuffer) {
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)
})
2024-05-14 09:27:05 +00:00
} else {
setIsLoading(false)
setDisplayInput(true)
}
}, [decryptedArrayBuffer, uploadedZip, metaInNavState, decrypt])
2024-07-05 08:38:04 +00:00
const handleArrayBufferFromBlossom = async (
arrayBuffer: ArrayBuffer,
encryptionKey: string
) => {
// array buffer returned from blossom is encrypted.
// So, first decrypt it
const decrypted = await decryptArrayBuffer(
arrayBuffer,
encryptionKey
).catch((err) => {
console.log('err in decryption:>> ', err)
toast.error(err.message || 'An error occurred in decrypting file.')
setIsLoading(false)
return null
})
if (!decrypted) return
const zip = await loadZip(decrypted)
if (!zip) {
2024-07-05 08:38:04 +00:00
setIsLoading(false)
return
}
2024-07-05 08:38:04 +00:00
const files: { [filename: string]: SigitFile } = {}
2024-07-05 08:38:04 +00:00
const fileHashes: { [key: string]: string | null } = {}
const fileNames = Object.values(zip.files).map((entry) => entry.name)
// generate hashes for all files in zipArchive
// these hashes can be used to verify the originality of files
for (const fileName of fileNames) {
const arrayBuffer = await readContentOfZipEntry(
zip,
fileName,
'arraybuffer'
)
if (arrayBuffer) {
files[fileName] = await convertToSigitFile(arrayBuffer, fileName)
2024-07-05 08:38:04 +00:00
const hash = await getHash(arrayBuffer)
if (hash) {
fileHashes[fileName] = hash
}
} else {
fileHashes[fileName] = null
}
}
setFiles(files)
setCurrentFileHashes(fileHashes)
}
2024-08-02 10:40:00 +00:00
const setUpdatedMarks = (markToUpdate: Mark) => {
const updatedMarks = updateMarks(marks, markToUpdate)
setMarks(updatedMarks)
}
const parseKeysJson = async (zip: JSZip) => {
const keysFileContent = await readContentOfZipEntry(
zip,
'keys.json',
'string'
)
2024-05-14 09:27:05 +00:00
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
})
}
2024-07-05 08:38:04 +00:00
const handleDecryptedArrayBuffer = async (arrayBuffer: ArrayBuffer) => {
2024-05-14 09:27:05 +00:00
const decryptedZipFile = new File([arrayBuffer], 'decrypted.zip')
setLoadingSpinnerDesc('Parsing zip file')
const zip = await loadZip(decryptedZipFile)
2024-05-14 09:27:05 +00:00
if (!zip) return
const files: { [filename: string]: SigitFile } = {}
2024-07-05 08:38:04 +00:00
const fileHashes: { [key: string]: string | null } = {}
const fileNames = Object.values(zip.files)
.filter((entry) => entry.name.startsWith('files/') && !entry.dir)
.map((entry) => entry.name)
.map((entry) => entry.replace(/^files\//, ''))
2024-05-14 09:27:05 +00:00
2024-07-05 08:38:04 +00:00
// generate hashes for all entries in files folder of zipArchive
// these hashes can be used to verify the originality of files
for (const fileName of fileNames) {
2024-07-05 08:38:04 +00:00
const arrayBuffer = await readContentOfZipEntry(
2024-06-28 09:24:14 +00:00
zip,
2024-07-05 08:38:04 +00:00
fileName,
'arraybuffer'
2024-06-28 09:24:14 +00:00
)
2024-07-05 08:38:04 +00:00
if (arrayBuffer) {
files[fileName] = await convertToSigitFile(arrayBuffer, fileName)
2024-07-05 08:38:04 +00:00
const hash = await getHash(arrayBuffer)
if (hash) {
fileHashes[fileName] = hash
}
} else {
fileHashes[fileName] = null
2024-06-28 09:24:14 +00:00
}
2024-07-05 08:38:04 +00:00
}
setFiles(files)
setCurrentFileHashes(fileHashes)
setDisplayInput(false)
setLoadingSpinnerDesc('Parsing meta.json')
const metaFileContent = await readContentOfZipEntry(
zip,
'meta.json',
'string'
)
if (!metaFileContent) {
setIsLoading(false)
return
}
2024-05-14 09:27:05 +00:00
2024-07-05 08:38:04 +00:00
const parsedMetaJson = await parseJson<Meta>(metaFileContent).catch(
(err) => {
2024-05-14 09:27:05 +00:00
console.log('err in parsing the content of meta.json :>> ', err)
toast.error(
err.message || 'error occurred in parsing the content of meta.json'
)
setIsLoading(false)
return null
2024-07-05 08:38:04 +00:00
}
)
2024-05-22 06:19:40 +00:00
2024-05-14 09:27:05 +00:00
setMeta(parsedMetaJson)
}
const handleDecrypt = async () => {
if (!selectedFile) return
2024-05-14 09:27:05 +00:00
setIsLoading(true)
const arrayBuffer = await decrypt(selectedFile)
2024-05-14 09:27:05 +00:00
if (!arrayBuffer) return
handleDecryptedArrayBuffer(arrayBuffer)
}
const handleSign = async () => {
2024-07-05 08:38:04 +00:00
if (Object.entries(files).length === 0 || !meta) return
2024-05-14 09:27:05 +00:00
setIsLoading(true)
setLoadingSpinnerDesc('Signing nostr event')
const prevSig = getPrevSignersSig(hexToNpub(usersPubkey!))
if (!prevSig) {
setIsLoading(false)
toast.error('Previous signature is invalid')
return
}
const marks = getSignerMarksForMeta() || []
const signedEvent = await signEventForMeta({ prevSig, marks })
if (!signedEvent) return
const updatedMeta = updateMetaSignatures(meta, signedEvent)
if (await isOnline()) {
2024-07-05 08:38:04 +00:00
await handleOnlineFlow(updatedMeta)
2024-06-12 14:44:06 +00:00
} else {
2024-07-05 08:38:04 +00:00
setMeta(updatedMeta)
setIsLoading(false)
}
}
// Sign the event for the meta file
const signEventForMeta = async (signerContent: {
prevSig: string
marks: Mark[]
}) => {
return await signEventForMetaFile(
JSON.stringify(signerContent),
2024-05-14 09:27:05 +00:00
nostrController,
setIsLoading
)
}
2024-05-14 09:27:05 +00:00
const getSignerMarksForMeta = (): Mark[] | undefined => {
if (currentUserMarks.length === 0) return
return currentUserMarks.map(({ mark }: CurrentUserMark) => mark)
}
// Update the meta signatures
const updateMetaSignatures = (meta: Meta, signedEvent: SignedEvent): Meta => {
2024-05-14 09:27:05 +00:00
const metaCopy = _.cloneDeep(meta)
2024-05-22 06:19:40 +00:00
metaCopy.docSignatures = {
...metaCopy.docSignatures,
[hexToNpub(signedEvent.pubkey)]: JSON.stringify(signedEvent, null, 2)
2024-05-14 09:27:05 +00:00
}
metaCopy.modifiedAt = unixNow()
return metaCopy
}
2024-05-14 09:27:05 +00:00
// 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'
})
}
// 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
}
2024-07-05 08:38:04 +00:00
// Handle the online flow: update users app data and send notifications
const handleOnlineFlow = async (meta: Meta) => {
2024-06-28 09:24:14 +00:00
setLoadingSpinnerDesc('Updating users app data')
2024-07-05 08:38:04 +00:00
const updatedEvent = await updateUsersAppData(meta)
2024-06-28 09:24:14 +00:00
if (!updatedEvent) {
setIsLoading(false)
return
}
2024-06-12 14:44:06 +00:00
2024-06-28 09:24:14 +00:00
const userSet = new Set<`npub1${string}`>()
2024-07-05 08:38:04 +00:00
if (submittedBy && submittedBy !== usersPubkey) {
2024-06-28 09:24:14 +00:00
userSet.add(hexToNpub(submittedBy))
}
2024-05-14 09:27:05 +00:00
2024-07-05 08:38:04 +00:00
const usersNpub = hexToNpub(usersPubkey!)
const isLastSigner = checkIsLastSigner(signers)
if (isLastSigner) {
2024-06-28 09:24:14 +00:00
signers.forEach((signer) => {
2024-07-05 08:38:04 +00:00
if (signer !== usersNpub) {
userSet.add(signer)
}
2024-06-28 09:24:14 +00:00
})
2024-06-12 14:44:06 +00:00
2024-06-28 09:24:14 +00:00
viewers.forEach((viewer) => {
userSet.add(viewer)
})
} else {
const currentSignerIndex = signers.indexOf(usersNpub)
const prevSigners = signers.slice(0, currentSignerIndex)
2024-05-14 09:27:05 +00:00
2024-06-28 09:24:14 +00:00
prevSigners.forEach((signer) => {
userSet.add(signer)
})
2024-06-12 14:44:06 +00:00
2024-06-28 09:24:14 +00:00
const nextSigner = signers[currentSignerIndex + 1]
userSet.add(nextSigner)
}
2024-06-12 14:44:06 +00:00
2024-06-28 09:24:14 +00:00
setLoadingSpinnerDesc('Sending notifications')
const users = Array.from(userSet)
const promises = users.map((user) =>
2024-07-05 08:38:04 +00:00
sendNotification(npubToHex(user)!, meta)
2024-06-28 09:24:14 +00:00
)
2024-07-05 08:38:04 +00:00
await Promise.all(promises)
.then(() => {
toast.success('Notifications sent successfully')
setMeta(meta)
})
.catch(() => {
toast.error('Failed to publish notifications')
})
2024-05-14 09:27:05 +00:00
2024-06-28 09:24:14 +00:00
setIsLoading(false)
}
2024-05-14 09:27:05 +00:00
// 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
}
2024-05-14 09:27:05 +00:00
const handleExport = async () => {
2024-07-05 08:38:04 +00:00
if (Object.entries(files).length === 0 || !meta || !usersPubkey) return
2024-05-14 09:27:05 +00:00
const usersNpub = hexToNpub(usersPubkey)
2024-05-14 09:27:05 +00:00
if (
2024-05-22 06:19:40 +00:00
!signers.includes(usersNpub) &&
!viewers.includes(usersNpub) &&
submittedBy !== usersNpub
2024-05-14 09:27:05 +00:00
)
return
setIsLoading(true)
setLoadingSpinnerDesc('Signing nostr event')
if (!meta) return
const prevSig = getLastSignersSig(meta, signers)
if (!prevSig) return
2024-05-22 06:19:40 +00:00
const signedEvent = await signEventForMetaFile(
JSON.stringify({
prevSig
2024-05-22 06:19:40 +00:00
}),
nostrController,
setIsLoading
)
2024-05-14 09:27:05 +00:00
if (!signedEvent) return
const exportSignature = JSON.stringify(signedEvent, null, 2)
const stringifiedMeta = JSON.stringify(
{
...meta,
exportSignature
},
null,
2
)
2024-07-05 08:38:04 +00:00
const zip = new JSZip()
2024-05-14 09:27:05 +00:00
zip.file('meta.json', stringifiedMeta)
for (const [fileName, file] of Object.entries(files)) {
zip.file(`files/${fileName}`, await file.arrayBuffer())
2024-08-06 14:48:46 +00:00
}
2024-07-05 08:38:04 +00:00
2024-05-14 09:27:05 +00:00
const arrayBuffer = await zip
.generateAsync({
type: 'arraybuffer',
compression: 'DEFLATE',
compressionOptions: {
level: 6
}
})
.catch((err) => {
console.log('err in zip:>> ', err)
setIsLoading(false)
toast.error(err.message || 'Error occurred in generating zip file')
return null
})
if (!arrayBuffer) return
const blob = new Blob([arrayBuffer])
saveAs(blob, `exported-${unixNow()}.sigit.zip`)
2024-05-14 09:27:05 +00:00
setIsLoading(false)
navigate(appPublicRoutes.verify)
2024-05-14 09:27:05 +00:00
}
const handleExportSigit = async () => {
2024-07-05 08:38:04 +00:00
if (Object.entries(files).length === 0 || !meta) return
const zip = new JSZip()
const stringifiedMeta = JSON.stringify(meta, null, 2)
zip.file('meta.json', stringifiedMeta)
for (const [fileName, file] of Object.entries(files)) {
zip.file(`files/${fileName}`, await file.arrayBuffer())
2024-08-06 14:48:46 +00:00
}
const arrayBuffer = await zip
.generateAsync({
type: 'arraybuffer',
compression: 'DEFLATE',
compressionOptions: {
level: 6
}
})
.catch((err) => {
console.log('err in zip:>> ', err)
setIsLoading(false)
toast.error(err.message || 'Error occurred in generating zip file')
return null
})
if (!arrayBuffer) return
const 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`)
}
/**
* 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
*/
const getPrevSignersSig = (npub: string) => {
if (!meta) return null
// if user is first signer then use creator's signature
if (signers[0] === npub) {
try {
const createSignatureEvent: Event = JSON.parse(meta.createSignature)
return createSignatureEvent.sig
} catch (error) {
return null
}
}
// find the index of signer
const currentSignerIndex = signers.findIndex((signer) => signer === npub)
// return null if could not found user in signer's list
if (currentSignerIndex === -1) return null
// find prev signer
const prevSigner = signers[currentSignerIndex - 1]
// get the signature of prev signer
try {
const prevSignersEvent: Event = JSON.parse(meta.docSignatures[prevSigner])
return prevSignersEvent.sig
} catch (error) {
return null
}
}
2024-05-14 09:27:05 +00:00
if (authUrl) {
return (
<iframe
2024-05-16 06:25:30 +00:00
title="Nsecbunker auth"
2024-05-14 09:27:05 +00:00
src={authUrl}
2024-05-16 06:25:30 +00:00
width="100%"
height="500px"
2024-05-14 09:27:05 +00:00
/>
)
}
if (isLoading) {
return <LoadingSpinner desc={loadingSpinnerDesc} />
}
2024-05-14 09:27:05 +00:00
2024-08-27 13:17:34 +00:00
if (!isMarksCompleted && signedStatus === SignedStatus.User_Is_Next_Signer) {
return (
2024-08-27 13:17:34 +00:00
<PdfMarking
files={getCurrentUserFiles(files, currentFileHashes, creatorFileHashes)}
currentUserMarks={currentUserMarks}
setIsMarksCompleted={setIsMarksCompleted}
setCurrentUserMarks={setCurrentUserMarks}
setUpdatedMarks={setUpdatedMarks}
handleDownload={handleDownload}
otherUserMarks={otherUserMarks}
meta={meta}
/>
2024-08-02 10:40:00 +00:00
)
}
return (
2024-08-27 13:17:34 +00:00
<>
<Container className={styles.container}>
{displayInput && (
<>
<Typography component="label" variant="h6">
Select sigit file
</Typography>
<Box className={styles.inputBlock}>
<MuiFileInput
placeholder="Select file"
inputProps={{ accept: '.sigit.zip' }}
value={selectedFile}
onChange={(value) => setSelectedFile(value)}
/>
</Box>
{selectedFile && (
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleDecrypt} variant="contained">
Decrypt
</Button>
</Box>
)}
</>
)}
{submittedBy && Object.entries(files).length > 0 && meta && (
<>
<DisplayMeta
meta={meta}
files={files}
submittedBy={submittedBy}
signers={signers}
viewers={viewers}
creatorFileHashes={creatorFileHashes}
currentFileHashes={currentFileHashes}
signedBy={signedBy}
nextSigner={nextSinger}
getPrevSignersSig={getPrevSignersSig}
/>
{signedStatus === SignedStatus.Fully_Signed && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleExport} variant="contained">
Export
</Button>
</Box>
)}
{signedStatus === SignedStatus.User_Is_Next_Signer && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleSign} variant="contained">
Sign
</Button>
</Box>
)}
{isSignerOrCreator && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleExportSigit} variant="contained">
Export Sigit
</Button>
</Box>
)}
</>
)}
</Container>
</>
)
2024-05-14 09:27:05 +00:00
}