feat: improve verification process #65

Merged
s merged 1 commits from issue-48 into main 2024-05-22 08:03:34 +00:00
8 changed files with 451 additions and 185 deletions

View File

@ -65,7 +65,9 @@ export const UserComponent = ({ pubkey, name, image }: UserProps) => {
const roboImage = `https://robohash.org/${npub}.png?set=set3` const roboImage = `https://robohash.org/${npub}.png?set=set3`
return ( return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: '10px' }}> <Box
sx={{ display: 'flex', alignItems: 'center', gap: '10px', flexGrow: 1 }}
>
<img <img
src={image || roboImage} src={image || roboImage}
alt="User Image" alt="User Image"

View File

@ -268,23 +268,22 @@ export const CreatePage = () => {
const viewers = users.filter((user) => user.role === UserRole.viewer) const viewers = users.filter((user) => user.role === UserRole.viewer)
setLoadingSpinnerDesc('Signing nostr event') setLoadingSpinnerDesc('Signing nostr event')
const signedEvent = await signEventForMetaFile( const createSignature = await signEventForMetaFile(
fileHashes, JSON.stringify({
signers: signers.map((signer) => hexToNpub(signer.pubkey)),
viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)),
fileHashes
}),
nostrController, nostrController,
setIsLoading setIsLoading
) )
if (!signedEvent) return if (!createSignature) return
// create content for meta file // create content for meta file
const meta: Meta = { const meta: Meta = {
signers: signers.map((signer) => hexToNpub(signer.pubkey)), createSignature: JSON.stringify(createSignature, null, 2),
viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)), docSignatures: {}
fileHashes,
submittedBy: hexToNpub(usersPubkey!),
signedEvents: {
[hexToNpub(signedEvent.pubkey)]: JSON.stringify(signedEvent, null, 2)
}
} }
try { try {

View File

@ -1,6 +1,7 @@
import { import {
Box, Box,
Button, Button,
IconButton,
List, List,
ListItem, ListItem,
ListSubheader, ListSubheader,
@ -10,6 +11,7 @@ import {
TableHead, TableHead,
TableRow, TableRow,
TextField, TextField,
Tooltip,
Typography, Typography,
useTheme useTheme
} from '@mui/material' } from '@mui/material'
@ -18,7 +20,7 @@ import saveAs from 'file-saver'
import JSZip from 'jszip' import JSZip from 'jszip'
import _ from 'lodash' import _ from 'lodash'
import { MuiFileInput } from 'mui-file-input' import { MuiFileInput } from 'mui-file-input'
import { EventTemplate } from 'nostr-tools' import { Event, verifyEvent } from 'nostr-tools'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { useNavigate, useSearchParams } from 'react-router-dom' import { useNavigate, useSearchParams } from 'react-router-dom'
@ -28,7 +30,13 @@ import { UserComponent } from '../../components/username'
import { MetadataController, NostrController } from '../../controllers' import { MetadataController, NostrController } from '../../controllers'
import { appPrivateRoutes } from '../../routes' import { appPrivateRoutes } from '../../routes'
import { State } from '../../store/rootReducer' import { State } from '../../store/rootReducer'
import { Meta, ProfileMetadata, User, UserRole } from '../../types' import {
CreateSignatureEventContent,
Meta,
ProfileMetadata,
User,
UserRole
} from '../../types'
import { import {
decryptArrayBuffer, decryptArrayBuffer,
encryptArrayBuffer, encryptArrayBuffer,
@ -44,6 +52,7 @@ import {
uploadToFileStorage uploadToFileStorage
} from '../../utils' } from '../../utils'
import styles from './style.module.scss' import styles from './style.module.scss'
import { Download } from '@mui/icons-material'
enum SignedStatus { enum SignedStatus {
Fully_Signed, Fully_Signed,
@ -68,6 +77,19 @@ export const SignPage = () => {
const [meta, setMeta] = useState<Meta | null>(null) const [meta, setMeta] = useState<Meta | null>(null)
const [signedStatus, setSignedStatus] = useState<SignedStatus>() const [signedStatus, setSignedStatus] = useState<SignedStatus>()
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
}>({})
const [signedBy, setSignedBy] = useState<`npub1${string}`[]>([])
const [nextSinger, setNextSinger] = useState<string>() const [nextSinger, setNextSinger] = useState<string>()
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
@ -76,42 +98,70 @@ export const SignPage = () => {
const nostrController = NostrController.getInstance() const nostrController = NostrController.getInstance()
useEffect(() => { useEffect(() => {
if (meta) { if (zip) {
setDisplayInput(false) 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)
// get list of users who have signed // generate hashes for all entries in files folder of zipArchive
const signedBy = Object.keys(meta.signedEvents) // these hashes can be used to verify the originality of files
for (const fileName of fileNames) {
const arrayBuffer = await readContentOfZipEntry(
zip,
fileName,
'arraybuffer'
)
if (meta.signers.length > 0) { if (arrayBuffer) {
// check if all signers have signed then its fully signed const hash = await getHash(arrayBuffer)
if (meta.signers.every((signer) => signedBy.includes(signer))) {
setSignedStatus(SignedStatus.Fully_Signed)
} else {
for (const signer of meta.signers) {
if (!signedBy.includes(signer)) {
// signers in meta.json are in npub1 format
// so, convert it to hex before setting to nextSigner
setNextSinger(npubToHex(signer)!)
const usersNpub = hexToNpub(usersPubkey!) if (hash) {
fileHashes[fileName.replace(/^files\//, '')] = hash
if (signer === usersNpub) {
// logged in user is the next signer
setSignedStatus(SignedStatus.User_Is_Next_Signer)
} else {
setSignedStatus(SignedStatus.User_Is_Not_Next_Signer)
}
break
} }
} else {
fileHashes[fileName.replace(/^files\//, '')] = null
} }
} }
} else {
// there's no signer just viewers. So its fully signed setCurrentFileHashes(fileHashes)
setSignedStatus(SignedStatus.Fully_Signed)
} }
generateCurrentFileHashes()
} }
}, [meta, usersPubkey]) }, [zip])
useEffect(() => {
if (signers.length > 0) {
// check if all signers have signed then its fully signed
if (signers.every((signer) => signedBy.includes(signer))) {
setSignedStatus(SignedStatus.Fully_Signed)
} else {
for (const signer of signers) {
if (!signedBy.includes(signer)) {
// signers in meta.json are in npub1 format
// so, convert it to hex before setting to nextSigner
setNextSinger(npubToHex(signer)!)
const usersNpub = hexToNpub(usersPubkey!)
if (signer === usersNpub) {
// logged in user is the next signer
setSignedStatus(SignedStatus.User_Is_Next_Signer)
} else {
setSignedStatus(SignedStatus.User_Is_Not_Next_Signer)
}
break
}
}
}
} else {
// there's no signer just viewers. So its fully signed
setSignedStatus(SignedStatus.Fully_Signed)
}
}, [signers, signedBy, usersPubkey])
useEffect(() => { useEffect(() => {
const fileUrl = searchParams.get('file') const fileUrl = searchParams.get('file')
@ -205,6 +255,53 @@ export const SignPage = () => {
} }
) )
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)
setSignedBy(Object.keys(parsedMetaJson.docSignatures) as `npub1${string}`[])
setMeta(parsedMetaJson) setMeta(parsedMetaJson)
} }
@ -252,36 +349,11 @@ export const SignPage = () => {
setLoadingSpinnerDesc('Generating hashes for files') setLoadingSpinnerDesc('Generating hashes for files')
const fileHashes: { [key: string]: string } = {}
const fileNames = Object.keys(meta.fileHashes)
// 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 filePath = `files/${fileName}`
const arrayBuffer = await readContentOfZipEntry(
zip,
filePath,
'arraybuffer'
)
if (!arrayBuffer) {
setIsLoading(false)
return
}
const hash = await getHash(arrayBuffer)
if (!hash) {
setIsLoading(false)
return
}
fileHashes[fileName] = hash
}
setLoadingSpinnerDesc('Signing nostr event') setLoadingSpinnerDesc('Signing nostr event')
const signedEvent = await signEventForMetaFile( const signedEvent = await signEventForMetaFile(
fileHashes, JSON.stringify({
fileHashes: currentFileHashes
}),
nostrController, nostrController,
setIsLoading setIsLoading
) )
@ -290,8 +362,8 @@ export const SignPage = () => {
const metaCopy = _.cloneDeep(meta) const metaCopy = _.cloneDeep(meta)
metaCopy.signedEvents = { metaCopy.docSignatures = {
...metaCopy.signedEvents, ...metaCopy.docSignatures,
[hexToNpub(signedEvent.pubkey)]: JSON.stringify(signedEvent, null, 2) [hexToNpub(signedEvent.pubkey)]: JSON.stringify(signedEvent, null, 2)
} }
@ -349,21 +421,23 @@ export const SignPage = () => {
// check if the current user is the last signer // check if the current user is the last signer
const usersNpub = hexToNpub(usersPubkey!) const usersNpub = hexToNpub(usersPubkey!)
const lastSignerIndex = meta.signers.length - 1 const lastSignerIndex = signers.length - 1
const signerIndex = meta.signers.indexOf(usersNpub) const signerIndex = signers.indexOf(usersNpub)
const isLastSigner = signerIndex === lastSignerIndex const isLastSigner = signerIndex === lastSignerIndex
// if current user is the last signer, then send DMs to all signers and viewers // if current user is the last signer, then send DMs to all signers and viewers
if (isLastSigner) { if (isLastSigner) {
const userSet = new Set<`npub1${string}`>() const userSet = new Set<`npub1${string}`>()
userSet.add(meta.submittedBy) if (submittedBy) {
userSet.add(hexToNpub(submittedBy))
}
meta.signers.forEach((signer) => { signers.forEach((signer) => {
userSet.add(signer) userSet.add(signer)
}) })
meta.viewers.forEach((viewer) => { viewers.forEach((viewer) => {
userSet.add(viewer) userSet.add(viewer)
}) })
@ -381,7 +455,7 @@ export const SignPage = () => {
) )
} }
} else { } else {
const nextSigner = meta.signers[signerIndex + 1] const nextSigner = signers[signerIndex + 1]
await sendDM( await sendDM(
fileUrl, fileUrl,
key, key,
@ -406,28 +480,21 @@ export const SignPage = () => {
const usersNpub = hexToNpub(usersPubkey) const usersNpub = hexToNpub(usersPubkey)
if ( if (
!meta.signers.includes(usersNpub) && !signers.includes(usersNpub) &&
!meta.viewers.includes(usersNpub) && !viewers.includes(usersNpub) &&
meta.submittedBy !== usersNpub submittedBy !== usersNpub
) )
return return
setIsLoading(true) setIsLoading(true)
setLoadingSpinnerDesc('Signing nostr event') setLoadingSpinnerDesc('Signing nostr event')
const event: EventTemplate = { const signedEvent = await signEventForMetaFile(
kind: 1, JSON.stringify({
content: '', fileHashes: currentFileHashes
created_at: Math.floor(Date.now() / 1000), // Current timestamp }),
tags: [] nostrController,
} setIsLoading
)
// Sign the event
const signedEvent = await nostrController.signEvent(event).catch((err) => {
console.error(err)
toast.error(err.message || 'Error occurred in signing nostr event')
setIsLoading(false) // Set loading state to false
return null
})
if (!signedEvent) return if (!signedEvent) return
@ -516,29 +583,33 @@ export const SignPage = () => {
</> </>
)} )}
{meta && signedStatus === SignedStatus.Fully_Signed && ( {submittedBy && zip && (
<> <>
<DisplayMeta meta={meta} nextSigner={nextSinger} /> <DisplayMeta
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}> zip={zip}
<Button onClick={handleExport} variant="contained"> submittedBy={submittedBy}
Export signers={signers}
</Button> viewers={viewers}
</Box> creatorFileHashes={creatorFileHashes}
</> currentFileHashes={currentFileHashes}
)} signedBy={signedBy}
nextSigner={nextSinger}
/>
{signedStatus === SignedStatus.Fully_Signed && (
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleExport} variant="contained">
Export
</Button>
</Box>
)}
{meta && signedStatus === SignedStatus.User_Is_Not_Next_Signer && ( {signedStatus === SignedStatus.User_Is_Next_Signer && (
<DisplayMeta meta={meta} nextSigner={nextSinger} /> <Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
)} <Button onClick={handleSign} variant="contained">
Sign
{meta && signedStatus === SignedStatus.User_Is_Next_Signer && ( </Button>
<> </Box>
<DisplayMeta meta={meta} nextSigner={nextSinger} /> )}
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleSign} variant="contained">
Sign
</Button>
</Box>
</> </>
)} )}
</Box> </Box>
@ -547,11 +618,26 @@ export const SignPage = () => {
} }
type DisplayMetaProps = { type DisplayMetaProps = {
meta: Meta zip: JSZip
submittedBy: string
signers: `npub1${string}`[]
viewers: `npub1${string}`[]
creatorFileHashes: { [key: string]: string }
currentFileHashes: { [key: string]: string | null }
signedBy: `npub1${string}`[]
nextSigner?: string nextSigner?: string
} }
const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => { const DisplayMeta = ({
zip,
submittedBy,
signers,
viewers,
creatorFileHashes,
currentFileHashes,
signedBy,
nextSigner
}: DisplayMetaProps) => {
const theme = useTheme() const theme = useTheme()
const textColor = theme.palette.getContrastText( const textColor = theme.palette.getContrastText(
@ -564,7 +650,7 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
const [users, setUsers] = useState<User[]>([]) const [users, setUsers] = useState<User[]>([])
useEffect(() => { useEffect(() => {
meta.signers.forEach((signer) => { signers.forEach((signer) => {
const hexKey = npubToHex(signer) const hexKey = npubToHex(signer)
setUsers((prev) => { setUsers((prev) => {
if (prev.findIndex((user) => user.pubkey === hexKey) !== -1) return prev if (prev.findIndex((user) => user.pubkey === hexKey) !== -1) return prev
@ -579,7 +665,7 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
}) })
}) })
meta.viewers.forEach((viewer) => { viewers.forEach((viewer) => {
const hexKey = npubToHex(viewer) const hexKey = npubToHex(viewer)
setUsers((prev) => { setUsers((prev) => {
if (prev.findIndex((user) => user.pubkey === hexKey) !== -1) return prev if (prev.findIndex((user) => user.pubkey === hexKey) !== -1) return prev
@ -593,13 +679,13 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
] ]
}) })
}) })
}, [meta]) }, [signers, viewers])
useEffect(() => { useEffect(() => {
const metadataController = new MetadataController() const metadataController = new MetadataController()
const hexKeys: string[] = [ const hexKeys: string[] = [
npubToHex(meta.submittedBy)!, npubToHex(submittedBy)!,
...users.map((user) => user.pubkey) ...users.map((user) => user.pubkey)
] ]
@ -622,7 +708,19 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
}) })
} }
}) })
}, [users, meta.submittedBy]) }, [users, submittedBy])
const downloadFile = async (filename: string) => {
const arrayBuffer = await readContentOfZipEntry(
zip,
`files/${filename}`,
'arraybuffer'
)
if (!arrayBuffer) return
const blob = new Blob([arrayBuffer])
saveAs(blob, filename)
}
return ( return (
<List <List
@ -631,17 +729,7 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
marginTop: 2 marginTop: 2
}} }}
subheader={ subheader={
<ListSubheader <ListSubheader className={styles.subHeader}>Meta Info</ListSubheader>
sx={{
borderBottom: '0.5px solid',
paddingBottom: 1,
paddingTop: 1,
fontSize: '1.5rem'
}}
className={styles.subHeader}
>
Meta Info
</ListSubheader>
} }
> >
<ListItem <ListItem
@ -654,17 +742,14 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
Submitted By Submitted By
</Typography> </Typography>
{(function () { {(function () {
const pubkey = npubToHex(meta.submittedBy) const profile = metadata[submittedBy]
const profile = metadata[pubkey!]
return ( return (
<UserComponent <UserComponent
pubkey={pubkey!} pubkey={submittedBy}
name={ name={
profile?.display_name || profile?.display_name || profile?.name || shorten(submittedBy)
profile?.name ||
shorten(meta.submittedBy)
} }
image={metadata[meta.submittedBy]?.picture} image={profile?.picture}
/> />
) )
})()} })()}
@ -679,13 +764,40 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
<Typography variant="h6" sx={{ color: textColor }}> <Typography variant="h6" sx={{ color: textColor }}>
Files Files
</Typography> </Typography>
<ul> <Box className={styles.filesWrapper}>
{Object.keys(meta.fileHashes).map((file, index) => ( {Object.entries(currentFileHashes).map(([filename, hash], index) => {
<li key={index} style={{ color: textColor }}> const isValidHash = creatorFileHashes[filename] === hash
{file}
</li> return (
))} <Box key={`file-${index}`} className={styles.file}>
</ul> <Tooltip title="Download File" arrow>
<IconButton onClick={() => downloadFile(filename)}>
<Download />
</IconButton>
</Tooltip>
<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>
</ListItem> </ListItem>
<ListItem sx={{ marginTop: 1 }}> <ListItem sx={{ marginTop: 1 }}>
<Table> <Table>
@ -705,7 +817,7 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
if (user.role === UserRole.signer) { if (user.role === UserRole.signer) {
// check if user has signed the document // check if user has signed the document
const usersNpub = hexToNpub(user.pubkey) const usersNpub = hexToNpub(user.pubkey)
if (usersNpub in meta.signedEvents) { if (signedBy.includes(usersNpub)) {
signedStatus = 'Signed' signedStatus = 'Signed'
} }
// check if user is the next signer // check if user is the next signer

View File

@ -10,6 +10,25 @@
gap: 25px; gap: 25px;
} }
.subHeader {
border-bottom: 0.5px solid;
padding: 8px 16px;
font-size: 1.5rem;
}
.filesWrapper {
display: flex;
flex-direction: column;
gap: 10px;
margin-left: 15px;
.file {
display: flex;
align-items: center;
gap: 15px;
}
}
.user { .user {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -15,8 +15,9 @@ import { toast } from 'react-toastify'
import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoadingSpinner } from '../../components/LoadingSpinner'
import { UserComponent } from '../../components/username' import { UserComponent } from '../../components/username'
import { MetadataController } from '../../controllers' import { MetadataController } from '../../controllers'
import { Meta, ProfileMetadata } from '../../types' import { CreateSignatureEventContent, Meta, ProfileMetadata } from '../../types'
import { import {
getHash,
hexToNpub, hexToNpub,
npubToHex, npubToHex,
parseJson, parseJson,
@ -36,17 +37,64 @@ export const VerifyPage = () => {
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const [selectedFile, setSelectedFile] = useState<File | null>(null) const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [zip, setZip] = useState<JSZip>()
const [meta, setMeta] = useState<Meta | null>(null) const [meta, setMeta] = useState<Meta | null>(null)
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
}>({})
const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>( const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
{} {}
) )
useEffect(() => { useEffect(() => {
if (meta) { 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) {
const metadataController = new MetadataController() const metadataController = new MetadataController()
const users = [meta.submittedBy, ...meta.signers, ...meta.viewers] const users = [submittedBy, ...signers, ...viewers]
users.forEach((user) => { users.forEach((user) => {
const pubkey = npubToHex(user)! const pubkey = npubToHex(user)!
@ -72,7 +120,7 @@ export const VerifyPage = () => {
} }
}) })
} }
}, [meta]) }, [submittedBy, signers, viewers])
const handleVerify = async () => { const handleVerify = async () => {
if (!selectedFile) return if (!selectedFile) return
@ -85,6 +133,7 @@ export const VerifyPage = () => {
}) })
if (!zip) return if (!zip) return
setZip(zip)
setLoadingSpinnerDesc('Parsing meta.json') setLoadingSpinnerDesc('Parsing meta.json')
@ -110,6 +159,51 @@ export const VerifyPage = () => {
} }
) )
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)
setMeta(parsedMetaJson) setMeta(parsedMetaJson)
setIsLoading(false) setIsLoading(false)
} }
@ -121,7 +215,7 @@ export const VerifyPage = () => {
if (verifySignature) { if (verifySignature) {
const npub = hexToNpub(pubkey) const npub = hexToNpub(pubkey)
const signedEventString = meta ? meta.signedEvents[npub] : null const signedEventString = meta ? meta.docSignatures[npub] : null
if (signedEventString) { if (signedEventString) {
try { try {
const signedEvent = JSON.parse(signedEventString) const signedEvent = JSON.parse(signedEventString)
@ -150,11 +244,11 @@ export const VerifyPage = () => {
component="label" component="label"
sx={{ sx={{
color: isValidSignature color: isValidSignature
? theme.palette.text.primary ? theme.palette.success.light
: theme.palette.error.main : theme.palette.error.main
}} }}
> >
({isValidSignature ? 'Valid' : 'Invalid'} Signature) {isValidSignature ? 'Valid' : 'Invalid'} Signature
</Typography> </Typography>
)} )}
</> </>
@ -229,17 +323,19 @@ export const VerifyPage = () => {
</ListSubheader> </ListSubheader>
} }
> >
<ListItem {submittedBy && (
sx={{ <ListItem
marginTop: 1, sx={{
gap: '15px' marginTop: 1,
}} gap: '15px'
> }}
<Typography variant="h6" sx={{ color: textColor }}> >
Submitted By <Typography variant="h6" sx={{ color: textColor }}>
</Typography> Submitted By
{displayUser(npubToHex(meta.submittedBy)!)} </Typography>
</ListItem> {displayUser(submittedBy)}
</ListItem>
)}
<ListItem <ListItem
sx={{ sx={{
@ -253,7 +349,7 @@ export const VerifyPage = () => {
{displayExportedBy()} {displayExportedBy()}
</ListItem> </ListItem>
{meta.signers.length > 0 && ( {signers.length > 0 && (
<ListItem <ListItem
sx={{ sx={{
marginTop: 1, marginTop: 1,
@ -265,7 +361,7 @@ export const VerifyPage = () => {
Signers Signers
</Typography> </Typography>
<ul className={styles.usersList}> <ul className={styles.usersList}>
{meta.signers.map((signer) => ( {signers.map((signer) => (
<li <li
key={signer} key={signer}
style={{ style={{
@ -282,7 +378,7 @@ export const VerifyPage = () => {
</ListItem> </ListItem>
)} )}
{meta.viewers.length > 0 && ( {viewers.length > 0 && (
<ListItem <ListItem
sx={{ sx={{
marginTop: 1, marginTop: 1,
@ -294,7 +390,7 @@ export const VerifyPage = () => {
Viewers Viewers
</Typography> </Typography>
<ul className={styles.usersList}> <ul className={styles.usersList}>
{meta.viewers.map((viewer) => ( {viewers.map((viewer) => (
<li key={viewer} style={{ color: textColor }}> <li key={viewer} style={{ color: textColor }}>
{displayUser(npubToHex(viewer)!)} {displayUser(npubToHex(viewer)!)}
</li> </li>
@ -313,13 +409,37 @@ export const VerifyPage = () => {
<Typography variant="h6" sx={{ color: textColor }}> <Typography variant="h6" sx={{ color: textColor }}>
Files Files
</Typography> </Typography>
<ul> <Box className={styles.filesWrapper}>
{Object.keys(meta.fileHashes).map((file, index) => ( {Object.entries(currentFileHashes).map(
<li key={index} style={{ color: textColor }}> ([filename, hash], index) => {
{file} const isValidHash = creatorFileHashes[filename] === hash
</li>
))} return (
</ul> <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>
</ListItem> </ListItem>
</List> </List>
</> </>

View File

@ -10,6 +10,19 @@
font-size: 1.5rem; font-size: 1.5rem;
} }
.filesWrapper {
display: flex;
flex-direction: column;
gap: 10px;
margin-left: 15px;
.file {
display: flex;
align-items: center;
gap: 15px;
}
}
.usersList { .usersList {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -9,10 +9,13 @@ export interface User {
} }
export interface Meta { export interface Meta {
createSignature: string
docSignatures: { [key: `npub1${string}`]: string }
exportSignature?: string
}
export interface CreateSignatureEventContent {
signers: `npub1${string}`[] signers: `npub1${string}`[]
viewers: `npub1${string}`[] viewers: `npub1${string}`[]
fileHashes: { [key: string]: string } fileHashes: { [key: string]: string }
submittedBy: `npub1${string}`
signedEvents: { [key: `npub1${string}`]: string }
exportSignature?: string
} }

View File

@ -177,22 +177,20 @@ export const sendDM = async (
/** /**
* Signs an event for a meta.json file. * Signs an event for a meta.json file.
* @param fileHashes Object containing file hashes. * @param content contains content for event.
* @param nostrController The NostrController instance for signing the event. * @param nostrController The NostrController instance for signing the event.
* @param setIsLoading Function to set loading state in the component. * @param setIsLoading Function to set loading state in the component.
* @returns A Promise resolving to the signed event, or null if signing fails. * @returns A Promise resolving to the signed event, or null if signing fails.
*/ */
export const signEventForMetaFile = async ( export const signEventForMetaFile = async (
fileHashes: { content: string,
[key: string]: string
},
nostrController: NostrController, nostrController: NostrController,
setIsLoading: (value: React.SetStateAction<boolean>) => void setIsLoading: (value: React.SetStateAction<boolean>) => void
) => { ) => {
// Construct the event metadata for the meta file // Construct the event metadata for the meta file
const event: EventTemplate = { const event: EventTemplate = {
kind: 1, // Event type for meta file kind: 1, // Event type for meta file
content: JSON.stringify(fileHashes), // Convert file hashes to JSON string content: content, // content for event
created_at: Math.floor(Date.now() / 1000), // Current timestamp created_at: Math.floor(Date.now() / 1000), // Current timestamp
tags: [] tags: []
} }