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

451 lines
13 KiB
TypeScript
Raw Normal View History

2024-05-14 09:27:05 +00:00
import {
Box,
Button,
List,
ListItem,
ListSubheader,
Typography,
useTheme
} from '@mui/material'
import JSZip from 'jszip'
import { MuiFileInput } from 'mui-file-input'
2024-05-16 11:22:05 +00:00
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'
2024-05-16 11:22:05 +00:00
import { UserComponent } from '../../components/username'
2024-05-16 05:40:56 +00:00
import { MetadataController } from '../../controllers'
2024-05-22 06:19:40 +00:00
import { CreateSignatureEventContent, Meta, ProfileMetadata } from '../../types'
2024-05-14 09:27:05 +00:00
import {
2024-05-22 06:19:40 +00:00
getHash,
2024-05-14 09:27:05 +00:00
hexToNpub,
npubToHex,
2024-05-14 09:27:05 +00:00
parseJson,
readContentOfZipEntry,
2024-05-16 05:40:56 +00:00
shorten
2024-05-14 09:27:05 +00:00
} from '../../utils'
import styles from './style.module.scss'
export const VerifyPage = () => {
2024-05-16 05:40:56 +00:00
const theme = useTheme()
2024-05-14 09:27:05 +00:00
2024-05-16 05:40:56 +00:00
const textColor = theme.palette.getContrastText(
theme.palette.background.paper
)
2024-05-14 09:27:05 +00:00
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 [zip, setZip] = useState<JSZip>()
2024-05-14 09:27:05 +00:00
const [meta, setMeta] = useState<Meta | null>(null)
2024-05-22 06:19:40 +00:00
const [submittedBy, setSubmittedBy] = useState<string>()
const [signers, setSigners] = useState<`npub1${string}`[]>([])
const [viewers, setViewers] = useState<`npub1${string}`[]>([])
const [creatorFileHashes, setCreatorFileHashes] = useState<{
[key: string]: string
}>({})
const [currentFileHashes, setCurrentFileHashes] = useState<{
[key: string]: string | null
}>({})
2024-05-16 05:40:56 +00:00
const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
{}
)
2024-05-14 09:27:05 +00:00
useEffect(() => {
2024-05-22 06:19:40 +00:00
if (zip) {
const generateCurrentFileHashes = async () => {
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
}
}
setCurrentFileHashes(fileHashes)
}
generateCurrentFileHashes()
}
}, [zip])
useEffect(() => {
if (submittedBy) {
2024-05-16 05:40:56 +00:00
const metadataController = new MetadataController()
2024-05-22 06:19:40 +00:00
const users = [submittedBy, ...signers, ...viewers]
2024-05-16 05:40:56 +00:00
users.forEach((user) => {
const pubkey = npubToHex(user)!
if (!(pubkey in metadata)) {
2024-05-16 05:40:56 +00:00
metadataController
.findMetadata(pubkey)
2024-05-16 05:40:56 +00:00
.then((metadataEvent) => {
const metadataContent =
metadataController.extractProfileMetadataContent(metadataEvent)
if (metadataContent)
setMetadata((prev) => ({
...prev,
[pubkey]: metadataContent
2024-05-16 05:40:56 +00:00
}))
})
.catch((err) => {
console.error(
`error occurred in finding metadata for: ${user}`,
err
)
})
2024-05-14 09:27:05 +00:00
}
})
2024-05-16 05:40:56 +00:00
}
2024-05-22 06:19:40 +00:00
}, [submittedBy, signers, viewers])
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-05-22 06:19:40 +00:00
setZip(zip)
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
setSigners(createSignatureContent.signers)
setViewers(createSignatureContent.viewers)
setCreatorFileHashes(createSignatureContent.fileHashes)
setSubmittedBy(createSignatureEvent.pubkey)
2024-05-14 09:27:05 +00:00
setMeta(parsedMetaJson)
2024-05-16 05:40:56 +00:00
setIsLoading(false)
2024-05-14 09:27:05 +00:00
}
2024-05-16 05:40:56 +00:00
const displayUser = (pubkey: string, verifySignature = false) => {
const profile = metadata[pubkey]
let isValidSignature = false
if (verifySignature) {
const npub = hexToNpub(pubkey)
2024-05-22 06:19:40 +00:00
const signedEventString = meta ? meta.docSignatures[npub] : null
2024-05-16 05:40:56 +00:00
if (signedEventString) {
try {
const signedEvent = JSON.parse(signedEventString)
isValidSignature = verifyEvent(signedEvent)
} catch (error) {
console.error(
`An error occurred in parsing and verifying the signature event for ${pubkey}`,
error
)
}
2024-05-14 09:27:05 +00:00
}
}
2024-05-16 05:40:56 +00:00
return (
2024-05-16 11:22:05 +00:00
<>
<UserComponent
pubkey={pubkey}
name={
profile?.display_name || profile?.name || shorten(hexToNpub(pubkey))
}
image={profile?.picture}
2024-05-16 05:40:56 +00:00
/>
2024-05-16 11:22:05 +00:00
2024-05-16 05:40:56 +00:00
{verifySignature && (
<Typography
component="label"
sx={{
color: isValidSignature
2024-05-22 06:19:40 +00:00
? theme.palette.success.light
: theme.palette.error.main
}}
>
2024-05-22 06:19:40 +00:00
{isValidSignature ? 'Valid' : 'Invalid'} Signature
2024-05-16 05:40:56 +00:00
</Typography>
)}
2024-05-16 11:22:05 +00:00
</>
2024-05-14 09:27:05 +00:00
)
2024-05-16 05:40:56 +00:00
}
2024-05-14 09:27:05 +00:00
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)) {
return displayUser(exportSignatureEvent.pubkey)
} 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} />}
<Box 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: '.zip'
}
}}
/>
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 && (
2024-05-14 09:27:05 +00:00
<>
2024-05-16 05:40:56 +00:00
<List
sx={{
bgcolor: 'background.paper',
marginTop: 2
}}
subheader={
<ListSubheader className={styles.subHeader}>
Meta Info
</ListSubheader>
}
>
2024-05-22 06:19:40 +00:00
{submittedBy && (
<ListItem
sx={{
marginTop: 1,
gap: '15px'
}}
>
<Typography variant="h6" sx={{ color: textColor }}>
Submitted By
</Typography>
{displayUser(submittedBy)}
</ListItem>
)}
2024-05-16 05:40:56 +00:00
<ListItem
sx={{
marginTop: 1,
gap: '15px'
}}
>
2024-05-16 06:25:30 +00:00
<Typography variant="h6" sx={{ color: textColor }}>
2024-05-16 05:40:56 +00:00
Exported By
</Typography>
{displayExportedBy()}
</ListItem>
2024-05-22 06:19:40 +00:00
{signers.length > 0 && (
2024-05-16 05:40:56 +00:00
<ListItem
sx={{
marginTop: 1,
flexDirection: 'column',
alignItems: 'flex-start'
}}
>
2024-05-16 06:25:30 +00:00
<Typography variant="h6" sx={{ color: textColor }}>
2024-05-16 05:40:56 +00:00
Signers
</Typography>
<ul className={styles.usersList}>
2024-05-22 06:19:40 +00:00
{signers.map((signer) => (
2024-05-16 11:22:05 +00:00
<li
key={signer}
style={{
color: textColor,
display: 'flex',
alignItems: 'center',
gap: '15px'
}}
>
{displayUser(npubToHex(signer)!, true)}
2024-05-16 05:40:56 +00:00
</li>
))}
</ul>
</ListItem>
)}
2024-05-14 09:27:05 +00:00
2024-05-22 06:19:40 +00:00
{viewers.length > 0 && (
2024-05-16 05:40:56 +00:00
<ListItem
sx={{
marginTop: 1,
flexDirection: 'column',
alignItems: 'flex-start'
}}
>
2024-05-16 06:25:30 +00:00
<Typography variant="h6" sx={{ color: textColor }}>
2024-05-16 05:40:56 +00:00
Viewers
</Typography>
<ul className={styles.usersList}>
2024-05-22 06:19:40 +00:00
{viewers.map((viewer) => (
2024-05-16 05:40:56 +00:00
<li key={viewer} style={{ color: textColor }}>
{displayUser(npubToHex(viewer)!)}
2024-05-16 05:40:56 +00:00
</li>
))}
</ul>
</ListItem>
)}
2024-05-14 09:27:05 +00:00
2024-05-16 05:40:56 +00:00
<ListItem
sx={{
marginTop: 1,
flexDirection: 'column',
alignItems: 'flex-start'
}}
>
2024-05-16 06:25:30 +00:00
<Typography variant="h6" sx={{ color: textColor }}>
2024-05-16 05:40:56 +00:00
Files
</Typography>
2024-05-22 06:19:40 +00:00
<Box className={styles.filesWrapper}>
{Object.entries(currentFileHashes).map(
([filename, hash], index) => {
const isValidHash = creatorFileHashes[filename] === hash
return (
<Box key={`file-${index}`} className={styles.file}>
<Typography
component="label"
sx={{
color: textColor,
flexGrow: 1
}}
>
{filename}
</Typography>
<Typography
component="label"
sx={{
color: isValidHash
? theme.palette.success.light
: theme.palette.error.main
}}
>
{isValidHash ? 'Valid' : 'Invalid'} hash
</Typography>
</Box>
)
}
)}
</Box>
2024-05-16 05:40:56 +00:00
</ListItem>
</List>
2024-05-14 09:27:05 +00:00
</>
)}
</Box>
</>
)
}