262 lines
6.8 KiB
TypeScript
262 lines
6.8 KiB
TypeScript
|
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
|
||
|
})
|
||
|
|
||
|
if (!meta) 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) {
|
||
|
await signAndSendToNext(zip, meta, viewer, fileHashes, false)
|
||
|
}
|
||
|
} else {
|
||
|
const nextSigner = meta.signers[signerIndex + 1]
|
||
|
|
||
|
await signAndSendToNext(zip, meta, nextSigner, fileHashes, true)
|
||
|
}
|
||
|
|
||
|
setIsLoading(false)
|
||
|
}
|
||
|
|
||
|
const signAndSendToNext = async (
|
||
|
zip: JSZip,
|
||
|
meta: 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)
|
||
|
|
||
|
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>
|
||
|
</>
|
||
|
)
|
||
|
}
|