issue-38 #62

Closed
y wants to merge 48 commits from issue-38 into main
5 changed files with 383 additions and 0 deletions
Showing only changes of commit 144daaed4a - Show all commits

View File

@ -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
View 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>
</>
)
}

View 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;
}
}
}

View File

@ -6,6 +6,7 @@ import { ProfilePage } from '../pages/profile'
import { hexToNpub } from '../utils'
import { RelaysPage } from '../pages/relays'
import { SignPage } from '../pages/sign'
import { VerifyPage } from '../pages/verify'
export const appPrivateRoutes = {
homePage: '/',
@ -58,5 +59,9 @@ export const privateRoutes = [
{
path: appPrivateRoutes.sign,
element: <SignPage />
},
{
path: appPrivateRoutes.verify,
element: <VerifyPage />
}
]

View File

@ -14,4 +14,5 @@ export interface Meta {
fileHashes: { [key: string]: string }
submittedBy: string
signedEvents: { [key: string]: string }
exportSignature?: string
}