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

467 lines
14 KiB
TypeScript
Raw Normal View History

import { Box, Button, Tooltip, Typography, useTheme } from '@mui/material'
2024-05-14 09:27:05 +00:00
import JSZip from 'jszip'
import { MuiFileInput } from 'mui-file-input'
import { Event, verifyEvent } from 'nostr-tools'
2024-05-14 09:27:05 +00:00
import { useEffect, useState } from 'react'
import { toast } from 'react-toastify'
import { LoadingSpinner } from '../../components/LoadingSpinner'
import { NostrController } from '../../controllers'
import { CreateSignatureEventContent, Meta } from '../../types'
2024-05-14 09:27:05 +00:00
import {
decryptArrayBuffer,
extractMarksFromSignedMeta,
2024-05-22 06:19:40 +00:00
getHash,
hexToNpub,
unixNow,
2024-05-14 09:27:05 +00:00
parseJson,
readContentOfZipEntry,
signEventForMetaFile,
shorten
2024-05-14 09:27:05 +00:00
} from '../../utils'
import styles from './style.module.scss'
import { Cancel, CheckCircle } from '@mui/icons-material'
import { useLocation } from 'react-router-dom'
2024-07-05 08:38:04 +00:00
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'
2024-05-14 09:27:05 +00:00
export const VerifyPage = () => {
2024-05-16 05:40:56 +00:00
const theme = useTheme()
const textColor = theme.palette.getContrastText(
theme.palette.background.paper
)
2024-05-14 09:27:05 +00:00
const location = useLocation()
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 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
])
2024-05-16 05:40:56 +00:00
const [isLoading, setIsLoading] = useState(false)
2024-05-14 09:27:05 +00:00
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
2024-05-16 05:40:56 +00:00
const [selectedFile, setSelectedFile] = useState<File | null>(null)
2024-05-22 06:19:40 +00:00
const [currentFileHashes, setCurrentFileHashes] = useState<{
[key: string]: string | null
}>(fileHashes)
const [files, setFiles] = useState<{ [filename: string]: PdfFile }>({})
2024-05-22 06:19:40 +00:00
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
const nostrController = NostrController.getInstance()
2024-05-14 09:27:05 +00:00
useEffect(() => {
if (uploadedZip) {
setSelectedFile(uploadedZip)
} else if (meta && encryptionKey) {
2024-07-05 08:38:04 +00:00
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 } = {}
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 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!
)
2024-07-05 08:38:04 +00:00
const hash = await getHash(arrayBuffer)
if (hash) {
fileHashes[fileName.replace(/^files\//, '')] = hash
}
} else {
fileHashes[fileName.replace(/^files\//, '')] = null
}
}
setCurrentFileHashes(fileHashes)
setFiles(files)
2024-07-05 08:38:04 +00:00
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)
})
2024-05-22 06:19:40 +00:00
}
2024-07-05 08:38:04 +00:00
processSigit()
2024-05-22 06:19:40 +00:00
}
}, [encryptionKey, meta, uploadedZip, zipUrl])
2024-05-14 09:27:05 +00:00
2024-05-16 05:40:56 +00:00
const handleVerify = async () => {
if (!selectedFile) return
setIsLoading(true)
2024-05-14 09:27:05 +00:00
2024-05-16 05:40:56 +00:00
const zip = await JSZip.loadAsync(selectedFile).catch((err) => {
2024-05-14 09:27:05 +00:00
console.log('err in loading zip file :>> ', err)
toast.error(err.message || 'An error occurred in loading zip file.')
return null
})
if (!zip) return
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)
// 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)
2024-05-14 09:27:05 +00:00
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
}
)
2024-05-22 06:19:40 +00:00
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
2024-05-16 05:40:56 +00:00
setIsLoading(false)
2024-05-14 09:27:05 +00:00
}
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)
}
2024-05-16 05:40:56 +00:00
const displayExportedBy = () => {
if (!meta || !meta.exportSignature) return null
2024-05-14 09:27:05 +00:00
2024-05-16 05:40:56 +00:00
const exportSignatureString = meta.exportSignature
2024-05-14 09:27:05 +00:00
2024-05-16 05:40:56 +00:00
try {
const exportSignatureEvent = JSON.parse(exportSignatureString) as Event
2024-05-14 09:27:05 +00:00
2024-05-16 05:40:56 +00:00
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>
)
2024-05-16 05:40:56 +00:00
} else {
toast.error(`Invalid export signature!`)
return (
2024-05-16 06:25:30 +00:00
<Typography component="label" sx={{ color: 'red' }}>
2024-05-16 05:40:56 +00:00
Invalid export signature
</Typography>
2024-05-14 09:27:05 +00:00
)
}
2024-05-16 05:40:56 +00:00
} catch (error) {
console.error(`An error occurred wile parsing exportSignature`, error)
2024-05-14 09:27:05 +00:00
return null
2024-05-16 05:40:56 +00:00
}
2024-05-14 09:27:05 +00:00
}
return (
<>
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
<Container className={styles.container}>
2024-05-16 05:40:56 +00:00
{!meta && (
2024-05-14 09:27:05 +00:00
<>
2024-05-15 08:50:21 +00:00
<Typography component="label" variant="h6">
2024-05-16 05:40:56 +00:00
Select exported zip file
2024-05-14 09:27:05 +00:00
</Typography>
2024-05-16 05:40:56 +00:00
<MuiFileInput
2024-05-16 06:25:30 +00:00
placeholder="Select file"
2024-05-16 05:40:56 +00:00
value={selectedFile}
onChange={(value) => setSelectedFile(value)}
InputProps={{
inputProps: {
accept: '.sigit.zip'
2024-05-16 05:40:56 +00:00
}
}}
/>
2024-05-14 09:27:05 +00:00
2024-05-16 05:40:56 +00:00
{selectedFile && (
2024-05-15 06:19:28 +00:00
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
2024-05-16 06:25:30 +00:00
<Button onClick={handleVerify} variant="contained">
2024-05-16 05:40:56 +00:00
Verify
2024-05-14 09:27:05 +00:00
</Button>
</Box>
)}
</>
)}
2024-05-16 05:40:56 +00:00
{meta && (
<StickySideColumns
left={
<>
2024-05-22 06:19:40 +00:00
<Box className={styles.filesWrapper}>
{Object.entries(currentFileHashes).map(
([filename, hash], index) => {
const isValidHash = fileHashes[filename] === hash
2024-05-22 06:19:40 +00:00
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>
)}
2024-05-22 06:19:40 +00:00
</Box>
)
}
)}
</Box>
{displayExportedBy()}
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleExport} variant="contained">
Export Sigit
</Button>
</Box>
</>
}
right={<UsersDetails meta={meta} />}
/>
2024-05-14 09:27:05 +00:00
)}
</Container>
2024-05-14 09:27:05 +00:00
</>
)
}