feat: add verify page
This commit is contained in:
parent
8ebeb7ef93
commit
144daaed4a
@ -20,6 +20,12 @@ export const HomePage = () => {
|
|||||||
>
|
>
|
||||||
Sign
|
Sign
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate(appPrivateRoutes.verify)}
|
||||||
|
variant='contained'
|
||||||
|
>
|
||||||
|
Verify
|
||||||
|
</Button>
|
||||||
</Box>
|
</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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ import { ProfilePage } from '../pages/profile'
|
|||||||
import { hexToNpub } from '../utils'
|
import { hexToNpub } from '../utils'
|
||||||
import { RelaysPage } from '../pages/relays'
|
import { RelaysPage } from '../pages/relays'
|
||||||
import { SignPage } from '../pages/sign'
|
import { SignPage } from '../pages/sign'
|
||||||
|
import { VerifyPage } from '../pages/verify'
|
||||||
|
|
||||||
export const appPrivateRoutes = {
|
export const appPrivateRoutes = {
|
||||||
homePage: '/',
|
homePage: '/',
|
||||||
@ -58,5 +59,9 @@ export const privateRoutes = [
|
|||||||
{
|
{
|
||||||
path: appPrivateRoutes.sign,
|
path: appPrivateRoutes.sign,
|
||||||
element: <SignPage />
|
element: <SignPage />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: appPrivateRoutes.verify,
|
||||||
|
element: <VerifyPage />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -14,4 +14,5 @@ export interface Meta {
|
|||||||
fileHashes: { [key: string]: string }
|
fileHashes: { [key: string]: string }
|
||||||
submittedBy: string
|
submittedBy: string
|
||||||
signedEvents: { [key: string]: string }
|
signedEvents: { [key: string]: string }
|
||||||
|
exportSignature?: string
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user