333 lines
9.2 KiB
TypeScript
333 lines
9.2 KiB
TypeScript
|
import {
|
||
|
Box,
|
||
|
Button,
|
||
|
List,
|
||
|
ListItem,
|
||
|
ListSubheader,
|
||
|
Typography,
|
||
|
useTheme
|
||
|
} from '@mui/material'
|
||
|
import JSZip from 'jszip'
|
||
|
import { MuiFileInput } from 'mui-file-input'
|
||
|
import { useEffect, useState } from 'react'
|
||
|
import { Link } from 'react-router-dom'
|
||
|
import { toast } from 'react-toastify'
|
||
|
import placeholderAvatar from '../../assets/images/nostr-logo.jpg'
|
||
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||
|
import { MetadataController } from '../../controllers'
|
||
|
import { getProfileRoute } from '../../routes'
|
||
|
import { Meta, ProfileMetadata } from '../../types'
|
||
|
import {
|
||
|
hexToNpub,
|
||
|
parseJson,
|
||
|
readContentOfZipEntry,
|
||
|
shorten
|
||
|
} from '../../utils'
|
||
|
import styles from './style.module.scss'
|
||
|
import { Event, verifyEvent } from 'nostr-tools'
|
||
|
|
||
|
export const VerifyPage = () => {
|
||
|
const theme = useTheme()
|
||
|
|
||
|
const textColor = theme.palette.getContrastText(
|
||
|
theme.palette.background.paper
|
||
|
)
|
||
|
|
||
|
const [isLoading, setIsLoading] = useState(false)
|
||
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||
|
|
||
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||
|
const [meta, setMeta] = useState<Meta | null>(null)
|
||
|
|
||
|
const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
|
||
|
{}
|
||
|
)
|
||
|
|
||
|
useEffect(() => {
|
||
|
if (meta) {
|
||
|
const metadataController = new MetadataController()
|
||
|
|
||
|
const users = [meta.submittedBy, ...meta.signers, ...meta.viewers]
|
||
|
|
||
|
users.forEach((user) => {
|
||
|
if (!(user in metadata)) {
|
||
|
metadataController
|
||
|
.findMetadata(user)
|
||
|
.then((metadataEvent) => {
|
||
|
const metadataContent =
|
||
|
metadataController.extractProfileMetadataContent(metadataEvent)
|
||
|
if (metadataContent)
|
||
|
setMetadata((prev) => ({
|
||
|
...prev,
|
||
|
[user]: metadataContent
|
||
|
}))
|
||
|
})
|
||
|
.catch((err) => {
|
||
|
console.error(
|
||
|
`error occurred in finding metadata for: ${user}`,
|
||
|
err
|
||
|
)
|
||
|
})
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
}, [meta])
|
||
|
|
||
|
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
|
||
|
|
||
|
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
|
||
|
}
|
||
|
)
|
||
|
|
||
|
setMeta(parsedMetaJson)
|
||
|
setIsLoading(false)
|
||
|
}
|
||
|
|
||
|
const imageLoadError = (event: any) => {
|
||
|
event.target.src = placeholderAvatar
|
||
|
}
|
||
|
|
||
|
const getRoboImageUrl = (pubkey: string) => {
|
||
|
const npub = hexToNpub(pubkey)
|
||
|
return `https://robohash.org/${npub}.png?set=set3`
|
||
|
}
|
||
|
|
||
|
const displayUser = (pubkey: string, verifySignature = false) => {
|
||
|
const profile = metadata[pubkey]
|
||
|
|
||
|
let isValidSignature = false
|
||
|
|
||
|
if (verifySignature) {
|
||
|
const signedEventString = meta ? meta.signedEvents[pubkey] : null
|
||
|
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
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return (
|
||
|
<Box className={styles.user}>
|
||
|
<img
|
||
|
onError={imageLoadError}
|
||
|
src={profile?.picture || getRoboImageUrl(pubkey)}
|
||
|
alt='Profile Image'
|
||
|
className='profile-image'
|
||
|
style={{
|
||
|
borderWidth: '3px',
|
||
|
borderStyle: 'solid',
|
||
|
borderColor: `#${pubkey.substring(0, 6)}`
|
||
|
}}
|
||
|
/>
|
||
|
<Link to={getProfileRoute(pubkey)}>
|
||
|
<Typography component='label' className={styles.name}>
|
||
|
{profile?.display_name ||
|
||
|
profile?.name ||
|
||
|
shorten(hexToNpub(pubkey))}
|
||
|
</Typography>
|
||
|
</Link>
|
||
|
{verifySignature && (
|
||
|
<Typography component='label'>
|
||
|
({isValidSignature ? 'Valid' : 'Not Valid'} Signature)
|
||
|
</Typography>
|
||
|
)}
|
||
|
</Box>
|
||
|
)
|
||
|
}
|
||
|
|
||
|
const displayExportedBy = () => {
|
||
|
if (!meta || !meta.exportSignature) return null
|
||
|
|
||
|
const exportSignatureString = meta.exportSignature
|
||
|
|
||
|
try {
|
||
|
const exportSignatureEvent = JSON.parse(exportSignatureString) as Event
|
||
|
|
||
|
if (verifyEvent(exportSignatureEvent)) {
|
||
|
return displayUser(exportSignatureEvent.pubkey)
|
||
|
} 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} />}
|
||
|
<Box 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: '.zip'
|
||
|
}
|
||
|
}}
|
||
|
/>
|
||
|
|
||
|
{selectedFile && (
|
||
|
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
|
||
|
<Button onClick={handleVerify} variant='contained'>
|
||
|
Verify
|
||
|
</Button>
|
||
|
</Box>
|
||
|
)}
|
||
|
</>
|
||
|
)}
|
||
|
|
||
|
{meta && (
|
||
|
<>
|
||
|
<List
|
||
|
sx={{
|
||
|
bgcolor: 'background.paper',
|
||
|
marginTop: 2
|
||
|
}}
|
||
|
subheader={
|
||
|
<ListSubheader className={styles.subHeader}>
|
||
|
Meta Info
|
||
|
</ListSubheader>
|
||
|
}
|
||
|
>
|
||
|
<ListItem
|
||
|
sx={{
|
||
|
marginTop: 1,
|
||
|
gap: '15px'
|
||
|
}}
|
||
|
>
|
||
|
<Typography variant='h6' sx={{ color: textColor }}>
|
||
|
Submitted By
|
||
|
</Typography>
|
||
|
{displayUser(meta.submittedBy)}
|
||
|
</ListItem>
|
||
|
|
||
|
<ListItem
|
||
|
sx={{
|
||
|
marginTop: 1,
|
||
|
gap: '15px'
|
||
|
}}
|
||
|
>
|
||
|
<Typography variant='h6' sx={{ color: textColor }}>
|
||
|
Exported By
|
||
|
</Typography>
|
||
|
{displayExportedBy()}
|
||
|
</ListItem>
|
||
|
|
||
|
{meta.signers.length > 0 && (
|
||
|
<ListItem
|
||
|
sx={{
|
||
|
marginTop: 1,
|
||
|
flexDirection: 'column',
|
||
|
alignItems: 'flex-start'
|
||
|
}}
|
||
|
>
|
||
|
<Typography variant='h6' sx={{ color: textColor }}>
|
||
|
Signers
|
||
|
</Typography>
|
||
|
<ul className={styles.usersList}>
|
||
|
{meta.signers.map((signer) => (
|
||
|
<li key={signer} style={{ color: textColor }}>
|
||
|
{displayUser(signer, true)}
|
||
|
</li>
|
||
|
))}
|
||
|
</ul>
|
||
|
</ListItem>
|
||
|
)}
|
||
|
|
||
|
{meta.viewers.length > 0 && (
|
||
|
<ListItem
|
||
|
sx={{
|
||
|
marginTop: 1,
|
||
|
flexDirection: 'column',
|
||
|
alignItems: 'flex-start'
|
||
|
}}
|
||
|
>
|
||
|
<Typography variant='h6' sx={{ color: textColor }}>
|
||
|
Viewers
|
||
|
</Typography>
|
||
|
<ul className={styles.usersList}>
|
||
|
{meta.viewers.map((viewer) => (
|
||
|
<li key={viewer} style={{ color: textColor }}>
|
||
|
{displayUser(viewer)}
|
||
|
</li>
|
||
|
))}
|
||
|
</ul>
|
||
|
</ListItem>
|
||
|
)}
|
||
|
|
||
|
<ListItem
|
||
|
sx={{
|
||
|
marginTop: 1,
|
||
|
flexDirection: 'column',
|
||
|
alignItems: 'flex-start'
|
||
|
}}
|
||
|
>
|
||
|
<Typography variant='h6' sx={{ color: textColor }}>
|
||
|
Files
|
||
|
</Typography>
|
||
|
<ul>
|
||
|
{Object.keys(meta.fileHashes).map((file, index) => (
|
||
|
<li key={index} style={{ color: textColor }}>
|
||
|
{file}
|
||
|
</li>
|
||
|
))}
|
||
|
</ul>
|
||
|
</ListItem>
|
||
|
</List>
|
||
|
</>
|
||
|
)}
|
||
|
</Box>
|
||
|
</>
|
||
|
)
|
||
|
}
|