2024-04-18 11:12:11 +00:00
|
|
|
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
|
2024-04-18 11:12:11 +00:00
|
|
|
|
|
|
|
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)
|
2024-04-18 11:12:11 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const nextSigner = meta.signers[signerIndex + 1]
|
|
|
|
|
2024-04-22 11:24:50 +00:00
|
|
|
await signAndSendToNext(zip, meta, hashes, nextSigner, fileHashes, true)
|
2024-04-18 11:12:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
setIsLoading(false)
|
|
|
|
}
|
|
|
|
|
|
|
|
const signAndSendToNext = async (
|
|
|
|
zip: JSZip,
|
|
|
|
meta: any,
|
2024-04-22 11:24:50 +00:00
|
|
|
hashes: any,
|
2024-04-18 11:12:11 +00:00
|
|
|
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))
|
|
|
|
|
2024-04-18 11:12:11 +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 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)
|
2024-05-08 14:01:31 +00:00
|
|
|
setIsLoading(false)
|
2024-04-18 11:12:11 +00:00
|
|
|
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>
|
|
|
|
</>
|
|
|
|
)
|
|
|
|
}
|