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

293 lines
7.5 KiB
TypeScript
Raw Normal View History

import { Box, Button, Typography } from '@mui/material'
import { MuiFileInput } from 'mui-file-input'
import { useState } from 'react'
import { LoadingSpinner } from '../../components/LoadingSpinner'
import styles from './style.module.scss'
import JSZip from 'jszip'
import { toast } from 'react-toastify'
import {
encryptArrayBuffer,
generateEncryptionKey,
getHash,
parseJson,
readContentOfZipEntry,
sendDM,
signEventForMetaFile,
uploadToFileStorage
} from '../../utils'
import { useSelector } from 'react-redux'
import { State } from '../../store/rootReducer'
import { NostrController } from '../../controllers'
export const SignDocument = () => {
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const [isDraggingOver, setIsDraggingOver] = useState(false)
const [authUrl, setAuthUrl] = useState<string>()
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
const nostrController = NostrController.getInstance()
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault()
setIsDraggingOver(false)
const file = event.dataTransfer.files[0]
if (file.type === 'application/zip') setSelectedFile(file)
}
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault()
setIsDraggingOver(true)
}
const handleSign = async () => {
if (!selectedFile) return
setIsLoading(true)
setLoadingSpinnerDesc('Parsing zip file')
const 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
setLoadingSpinnerDesc('Parsing meta.json')
const metaFileContent = await readContentOfZipEntry(
zip,
'meta.json',
'string'
)
if (!metaFileContent) {
setIsLoading(false)
return
}
const meta = await parseJson(metaFileContent).catch((err) => {
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-04-22 11:24:50 +00:00
const hashesFileContent = await readContentOfZipEntry(
zip,
'hashes.json',
'string'
)
if (!hashesFileContent) {
setIsLoading(false)
return
}
const hashes = await parseJson(hashesFileContent).catch((err) => {
console.log('err in parsing the content of hashes.json :>> ', err)
toast.error(
err.message || 'error occurred in parsing the content of hashes.json'
)
setIsLoading(false)
return null
})
if (!hashes) return
setLoadingSpinnerDesc('Generating hashes for files')
const fileHashes: { [key: string]: string } = {}
const fileNames = Object.keys(meta.fileHashes)
// create 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) {
const filePath = `files/${fileName}`
const arrayBuffer = await readContentOfZipEntry(
zip,
filePath,
'arraybuffer'
)
if (!arrayBuffer) {
setIsLoading(false)
return
}
const hash = await getHash(arrayBuffer)
if (!hash) {
setIsLoading(false)
return
}
fileHashes[fileName] = hash
}
// check if the current user is the last signer
const lastSignerIndex = meta.signers.length - 1
const signerIndex = meta.signers.indexOf(usersPubkey)
const isLastSigner = signerIndex === lastSignerIndex
// if current user is the last signer, then send DMs to all viewers
if (isLastSigner) {
for (const viewer of meta.viewers) {
2024-04-22 11:24:50 +00:00
await signAndSendToNext(zip, meta, hashes, viewer, fileHashes, false)
}
} else {
const nextSigner = meta.signers[signerIndex + 1]
2024-04-22 11:24:50 +00:00
await signAndSendToNext(zip, meta, hashes, nextSigner, fileHashes, true)
}
setIsLoading(false)
}
const signAndSendToNext = async (
zip: JSZip,
meta: any,
2024-04-22 11:24:50 +00:00
hashes: any,
receiver: string,
fileHashes: {
[key: string]: string
},
isNextSigner: boolean
) => {
setLoadingSpinnerDesc('Signing nostr event for meta file')
const signedEvent = await signEventForMetaFile(
receiver,
fileHashes,
nostrController,
setIsLoading
)
if (!signedEvent) return
meta.signedEvents = {
...meta.signedEvents,
[signedEvent.pubkey]: JSON.stringify(signedEvent, null, 2)
}
const stringifiedMeta = JSON.stringify(meta, null, 2)
zip.file('meta.json', stringifiedMeta)
2024-04-22 11:24:50 +00:00
const metaHash = await getHash(stringifiedMeta)
if (!metaHash) return
hashes = {
...hashes,
[usersPubkey!]: metaHash
}
zip.file('hashes.json', JSON.stringify(hashes, null, 2))
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 encryptionKey = await generateEncryptionKey()
setLoadingSpinnerDesc('Encrypting zip file')
const encryptedArrayBuffer = await encryptArrayBuffer(
arrayBuffer,
encryptionKey
)
const blob = new Blob([encryptedArrayBuffer])
setLoadingSpinnerDesc('Uploading zip file to file storage.')
const fileUrl = await uploadToFileStorage(blob, nostrController)
.then((url) => {
toast.success('zip file uploaded to file storage')
return url
})
.catch((err) => {
console.log('err in upload:>> ', err)
toast.error(err.message || 'Error occurred in uploading zip file')
return null
})
if (!fileUrl) return
setLoadingSpinnerDesc(
`Sending DM to next ${isNextSigner ? 'signer' : 'viewer'}`
)
await sendDM(
fileUrl,
encryptionKey,
receiver,
nostrController,
isNextSigner,
setAuthUrl
)
}
if (authUrl) {
return (
<iframe
title='Nsecbunker auth'
src={authUrl}
width='100%'
height='500px'
/>
)
}
return (
<>
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
<Box className={styles.container}>
<Typography component='label' variant='h6'>
Select decrypted zip file
</Typography>
<Box
className={styles.inputBlock}
onDrop={handleDrop}
onDragOver={handleDragOver}
>
{isDraggingOver && (
<Box className={styles.fileDragOver}>
<Typography variant='body1'>Drop file here</Typography>
</Box>
)}
<MuiFileInput
placeholder='Drop file here, or click to select'
value={selectedFile}
onChange={(value) => setSelectedFile(value)}
InputProps={{
inputProps: {
accept: '.zip'
}
}}
/>
</Box>
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleSign} variant='contained'>
Sign
</Button>
</Box>
</Box>
</>
)
}