Some checks failed
Open PR on Staging / audit_and_check (pull_request) Failing after 30s
467 lines
14 KiB
TypeScript
467 lines
14 KiB
TypeScript
import { Box, Button, Tooltip, Typography, useTheme } from '@mui/material'
|
|
import JSZip from 'jszip'
|
|
import { MuiFileInput } from 'mui-file-input'
|
|
import { Event, verifyEvent } from 'nostr-tools'
|
|
import { useEffect, useState } from 'react'
|
|
import { toast } from 'react-toastify'
|
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
|
import { NostrController } from '../../controllers'
|
|
import { CreateSignatureEventContent, Meta } from '../../types'
|
|
import {
|
|
decryptArrayBuffer,
|
|
extractMarksFromSignedMeta,
|
|
getHash,
|
|
hexToNpub,
|
|
unixNow,
|
|
parseJson,
|
|
readContentOfZipEntry,
|
|
signEventForMetaFile,
|
|
shorten
|
|
} from '../../utils'
|
|
import styles from './style.module.scss'
|
|
import { Cancel, CheckCircle } from '@mui/icons-material'
|
|
import { useLocation } from 'react-router-dom'
|
|
import axios from 'axios'
|
|
import { PdfFile } from '../../types/drawing.ts'
|
|
import {
|
|
addMarks,
|
|
convertToPdfBlob,
|
|
convertToPdfFile,
|
|
groupMarksByPage
|
|
} from '../../utils/pdf.ts'
|
|
import { State } from '../../store/rootReducer.ts'
|
|
import { useSelector } from 'react-redux'
|
|
import { getLastSignersSig } from '../../utils/sign.ts'
|
|
import { saveAs } from 'file-saver'
|
|
import { Container } from '../../components/Container'
|
|
import { useSigitMeta } from '../../hooks/useSigitMeta.tsx'
|
|
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
|
|
import { UsersDetails } from '../../components/UsersDetails.tsx/index.tsx'
|
|
import { UserAvatar } from '../../components/UserAvatar/index.tsx'
|
|
import { useSigitProfiles } from '../../hooks/useSigitProfiles.tsx'
|
|
import { TooltipChild } from '../../components/TooltipChild.tsx'
|
|
|
|
export const VerifyPage = () => {
|
|
const theme = useTheme()
|
|
const textColor = theme.palette.getContrastText(
|
|
theme.palette.background.paper
|
|
)
|
|
|
|
const location = useLocation()
|
|
/**
|
|
* uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains meta.json
|
|
* meta will be received in navigation from create & home page in online mode
|
|
*/
|
|
const { uploadedZip, meta } = location.state || {}
|
|
|
|
const { submittedBy, zipUrl, encryptionKey, signers, viewers, fileHashes } =
|
|
useSigitMeta(meta)
|
|
|
|
const profiles = useSigitProfiles([
|
|
...(submittedBy ? [submittedBy] : []),
|
|
...signers,
|
|
...viewers
|
|
])
|
|
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
|
|
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
|
|
|
const [currentFileHashes, setCurrentFileHashes] = useState<{
|
|
[key: string]: string | null
|
|
}>(fileHashes)
|
|
const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({})
|
|
|
|
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
|
|
const nostrController = NostrController.getInstance()
|
|
|
|
useEffect(() => {
|
|
if (uploadedZip) {
|
|
setSelectedFile(uploadedZip)
|
|
} else if (meta && encryptionKey) {
|
|
const processSigit = async () => {
|
|
setIsLoading(true)
|
|
|
|
setLoadingSpinnerDesc('Fetching file from file server')
|
|
axios
|
|
.get(zipUrl, {
|
|
responseType: 'arraybuffer'
|
|
})
|
|
.then(async (res) => {
|
|
const fileName = zipUrl.split('/').pop()
|
|
const file = new File([res.data], fileName!)
|
|
|
|
const encryptedArrayBuffer = await file.arrayBuffer()
|
|
const arrayBuffer = await decryptArrayBuffer(
|
|
encryptedArrayBuffer,
|
|
encryptionKey
|
|
).catch((err) => {
|
|
console.log('err in decryption:>> ', err)
|
|
toast.error(
|
|
err.message || 'An error occurred in decrypting file.'
|
|
)
|
|
return null
|
|
})
|
|
|
|
if (arrayBuffer) {
|
|
const zip = await JSZip.loadAsync(arrayBuffer).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
|
|
|
|
const files: { [filename: string]: PdfFile } = {}
|
|
const fileHashes: { [key: string]: string | null } = {}
|
|
const fileNames = Object.values(zip.files).map(
|
|
(entry) => entry.name
|
|
)
|
|
|
|
// 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 arrayBuffer = await readContentOfZipEntry(
|
|
zip,
|
|
fileName,
|
|
'arraybuffer'
|
|
)
|
|
|
|
if (arrayBuffer) {
|
|
files[fileName] = await convertToPdfFile(
|
|
arrayBuffer,
|
|
fileName!
|
|
)
|
|
const hash = await getHash(arrayBuffer)
|
|
|
|
if (hash) {
|
|
fileHashes[fileName.replace(/^files\//, '')] = hash
|
|
}
|
|
} else {
|
|
fileHashes[fileName.replace(/^files\//, '')] = null
|
|
}
|
|
}
|
|
|
|
setCurrentFileHashes(fileHashes)
|
|
setFiles(files)
|
|
|
|
setIsLoading(false)
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error(`error occurred in getting file from ${zipUrl}`, err)
|
|
toast.error(
|
|
err.message || `error occurred in getting file from ${zipUrl}`
|
|
)
|
|
})
|
|
.finally(() => {
|
|
setIsLoading(false)
|
|
})
|
|
}
|
|
|
|
processSigit()
|
|
}
|
|
}, [encryptionKey, meta, uploadedZip, zipUrl])
|
|
|
|
const handleVerify = async () => {
|
|
if (!selectedFile) return
|
|
setIsLoading(true)
|
|
|
|
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
|
|
|
|
const fileHashes: { [key: string]: string | null } = {}
|
|
const fileNames = Object.values(zip.files)
|
|
.filter((entry) => entry.name.startsWith('files/') && !entry.dir)
|
|
.map((entry) => entry.name)
|
|
|
|
// 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 arrayBuffer = await readContentOfZipEntry(
|
|
zip,
|
|
fileName,
|
|
'arraybuffer'
|
|
)
|
|
|
|
if (arrayBuffer) {
|
|
const hash = await getHash(arrayBuffer)
|
|
|
|
if (hash) {
|
|
fileHashes[fileName.replace(/^files\//, '')] = hash
|
|
}
|
|
} else {
|
|
fileHashes[fileName.replace(/^files\//, '')] = null
|
|
}
|
|
}
|
|
|
|
console.log('fileHashes :>> ', fileHashes)
|
|
setCurrentFileHashes(fileHashes)
|
|
|
|
setLoadingSpinnerDesc('Parsing meta.json')
|
|
|
|
const metaFileContent = await readContentOfZipEntry(
|
|
zip,
|
|
'meta.json',
|
|
'string'
|
|
)
|
|
|
|
if (!metaFileContent) {
|
|
setIsLoading(false)
|
|
return
|
|
}
|
|
|
|
const parsedMetaJson = await parseJson<Meta>(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 (!parsedMetaJson) return
|
|
|
|
const createSignatureEvent = await parseJson<Event>(
|
|
parsedMetaJson.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
|
|
})
|
|
|
|
if (!createSignatureEvent) return
|
|
|
|
const isValidCreateSignature = verifyEvent(createSignatureEvent)
|
|
|
|
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
|
|
)
|
|
toast.error(
|
|
err.message ||
|
|
`error occurred in parsing the create signature event's content`
|
|
)
|
|
setIsLoading(false)
|
|
return null
|
|
})
|
|
|
|
if (!createSignatureContent) return
|
|
|
|
setIsLoading(false)
|
|
}
|
|
|
|
const handleExport = async () => {
|
|
if (Object.entries(files).length === 0 || !meta || !usersPubkey) return
|
|
|
|
const usersNpub = hexToNpub(usersPubkey)
|
|
if (
|
|
!signers.includes(usersNpub) &&
|
|
!viewers.includes(usersNpub) &&
|
|
submittedBy !== usersNpub
|
|
) {
|
|
return
|
|
}
|
|
|
|
setIsLoading(true)
|
|
setLoadingSpinnerDesc('Signing nostr event')
|
|
|
|
const prevSig = getLastSignersSig(meta, signers)
|
|
if (!prevSig) return
|
|
|
|
const signedEvent = await signEventForMetaFile(
|
|
JSON.stringify({ prevSig }),
|
|
nostrController,
|
|
setIsLoading
|
|
)
|
|
|
|
if (!signedEvent) return
|
|
|
|
const exportSignature = JSON.stringify(signedEvent, null, 2)
|
|
const updatedMeta = { ...meta, exportSignature }
|
|
const stringifiedMeta = JSON.stringify(updatedMeta, null, 2)
|
|
|
|
const zip = new JSZip()
|
|
zip.file('meta.json', stringifiedMeta)
|
|
|
|
const marks = extractMarksFromSignedMeta(updatedMeta)
|
|
const marksByPage = groupMarksByPage(marks)
|
|
|
|
for (const [fileName, pdf] of Object.entries(files)) {
|
|
const pages = await addMarks(pdf.file, marksByPage)
|
|
const blob = await convertToPdfBlob(pages)
|
|
zip.file(`files/${fileName}`, blob)
|
|
}
|
|
|
|
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`)
|
|
|
|
setIsLoading(false)
|
|
}
|
|
|
|
const displayExportedBy = () => {
|
|
if (!meta || !meta.exportSignature) return null
|
|
|
|
const exportSignatureString = meta.exportSignature
|
|
|
|
try {
|
|
const exportSignatureEvent = JSON.parse(exportSignatureString) as Event
|
|
|
|
if (verifyEvent(exportSignatureEvent)) {
|
|
const exportedBy = exportSignatureEvent.pubkey
|
|
const profile = profiles[exportedBy]
|
|
return (
|
|
<Tooltip
|
|
title={
|
|
profile?.display_name ||
|
|
profile?.name ||
|
|
shorten(hexToNpub(exportedBy))
|
|
}
|
|
placement="top"
|
|
arrow
|
|
disableInteractive
|
|
>
|
|
<TooltipChild>
|
|
<UserAvatar pubkey={exportedBy} image={profile?.picture} />
|
|
</TooltipChild>
|
|
</Tooltip>
|
|
)
|
|
} else {
|
|
toast.error(`Invalid export signature!`)
|
|
return (
|
|
<Typography component="label" sx={{ color: 'red' }}>
|
|
Invalid export signature
|
|
</Typography>
|
|
)
|
|
}
|
|
} catch (error) {
|
|
console.error(`An error occurred wile parsing exportSignature`, error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
|
<Container className={styles.container}>
|
|
{!meta && (
|
|
<>
|
|
<Typography component="label" variant="h6">
|
|
Select exported zip file
|
|
</Typography>
|
|
|
|
<MuiFileInput
|
|
placeholder="Select file"
|
|
value={selectedFile}
|
|
onChange={(value) => setSelectedFile(value)}
|
|
InputProps={{
|
|
inputProps: {
|
|
accept: '.sigit.zip'
|
|
}
|
|
}}
|
|
/>
|
|
|
|
{selectedFile && (
|
|
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
|
|
<Button onClick={handleVerify} variant="contained">
|
|
Verify
|
|
</Button>
|
|
</Box>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{meta && (
|
|
<StickySideColumns
|
|
left={
|
|
<>
|
|
<Box className={styles.filesWrapper}>
|
|
{Object.entries(currentFileHashes).map(
|
|
([filename, hash], index) => {
|
|
const isValidHash = fileHashes[filename] === hash
|
|
|
|
return (
|
|
<Box key={`file-${index}`} className={styles.file}>
|
|
<Typography
|
|
component="label"
|
|
sx={{
|
|
color: textColor,
|
|
flexGrow: 1
|
|
}}
|
|
>
|
|
{filename}
|
|
</Typography>
|
|
{isValidHash && (
|
|
<Tooltip title="File integrity check passed" arrow>
|
|
<CheckCircle
|
|
sx={{ color: theme.palette.success.light }}
|
|
/>
|
|
</Tooltip>
|
|
)}
|
|
{!isValidHash && (
|
|
<Tooltip title="File integrity check failed" arrow>
|
|
<Cancel
|
|
sx={{ color: theme.palette.error.main }}
|
|
/>
|
|
</Tooltip>
|
|
)}
|
|
</Box>
|
|
)
|
|
}
|
|
)}
|
|
</Box>
|
|
{displayExportedBy()}
|
|
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
|
<Button onClick={handleExport} variant="contained">
|
|
Export Sigit
|
|
</Button>
|
|
</Box>
|
|
</>
|
|
}
|
|
right={<UsersDetails meta={meta} />}
|
|
/>
|
|
)}
|
|
</Container>
|
|
</>
|
|
)
|
|
}
|