issue-25 #42
@ -20,6 +20,12 @@ export const HomePage = () => {
|
||||
>
|
||||
Sign
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => navigate(appPrivateRoutes.verify)}
|
||||
variant='contained'
|
||||
>
|
||||
Verify
|
||||
</Button>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
332
src/pages/verify/index.tsx
Normal file
332
src/pages/verify/index.tsx
Normal file
@ -0,0 +1,332 @@
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
39
src/pages/verify/style.module.scss
Normal file
39
src/pages/verify/style.module.scss
Normal file
@ -0,0 +1,39 @@
|
||||
@import '../../colors.scss';
|
||||
|
||||
.container {
|
||||
color: $text-color;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.subHeader {
|
||||
border-bottom: 0.5px solid;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.usersList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
list-style: none;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.name {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.tableCell {
|
||||
border-right: 1px solid rgba(224, 224, 224, 1);
|
||||
|
||||
.user {
|
||||
@extend .user;
|
||||
}
|
||||
}
|
||||
}
|
@ -5,11 +5,13 @@ import { Login } from '../pages/login'
|
||||
import { ProfilePage } from '../pages/profile'
|
||||
import { hexToNpub } from '../utils'
|
||||
import { SignPage } from '../pages/sign'
|
||||
import { VerifyPage } from '../pages/verify'
|
||||
|
||||
export const appPrivateRoutes = {
|
||||
homePage: '/',
|
||||
create: '/create',
|
||||
sign: '/sign'
|
||||
sign: '/sign',
|
||||
verify: '/verify'
|
||||
}
|
||||
|
||||
export const appPublicRoutes = {
|
||||
@ -51,5 +53,9 @@ export const privateRoutes = [
|
||||
{
|
||||
path: appPrivateRoutes.sign,
|
||||
element: <SignPage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.verify,
|
||||
element: <VerifyPage />
|
||||
}
|
||||
]
|
||||
|
@ -14,4 +14,5 @@ export interface Meta {
|
||||
fileHashes: { [key: string]: string }
|
||||
submittedBy: string
|
||||
signedEvents: { [key: string]: string }
|
||||
exportSignature?: string
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user