feat: improve verification process #65
@ -65,7 +65,9 @@ export const UserComponent = ({ pubkey, name, image }: UserProps) => {
|
||||
const roboImage = `https://robohash.org/${npub}.png?set=set3`
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<Box
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: '10px', flexGrow: 1 }}
|
||||
>
|
||||
<img
|
||||
src={image || roboImage}
|
||||
alt="User Image"
|
||||
|
@ -268,23 +268,22 @@ export const CreatePage = () => {
|
||||
const viewers = users.filter((user) => user.role === UserRole.viewer)
|
||||
|
||||
setLoadingSpinnerDesc('Signing nostr event')
|
||||
const signedEvent = await signEventForMetaFile(
|
||||
fileHashes,
|
||||
const createSignature = await signEventForMetaFile(
|
||||
JSON.stringify({
|
||||
signers: signers.map((signer) => hexToNpub(signer.pubkey)),
|
||||
viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)),
|
||||
fileHashes
|
||||
}),
|
||||
nostrController,
|
||||
setIsLoading
|
||||
)
|
||||
|
||||
if (!signedEvent) return
|
||||
if (!createSignature) return
|
||||
|
||||
// create content for meta file
|
||||
const meta: Meta = {
|
||||
signers: signers.map((signer) => hexToNpub(signer.pubkey)),
|
||||
viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)),
|
||||
fileHashes,
|
||||
submittedBy: hexToNpub(usersPubkey!),
|
||||
signedEvents: {
|
||||
[hexToNpub(signedEvent.pubkey)]: JSON.stringify(signedEvent, null, 2)
|
||||
}
|
||||
createSignature: JSON.stringify(createSignature, null, 2),
|
||||
docSignatures: {}
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListSubheader,
|
||||
@ -10,6 +11,7 @@ import {
|
||||
TableHead,
|
||||
TableRow,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material'
|
||||
@ -18,7 +20,7 @@ import saveAs from 'file-saver'
|
||||
import JSZip from 'jszip'
|
||||
import _ from 'lodash'
|
||||
import { MuiFileInput } from 'mui-file-input'
|
||||
import { EventTemplate } from 'nostr-tools'
|
||||
import { Event, verifyEvent } from 'nostr-tools'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
@ -28,7 +30,13 @@ import { UserComponent } from '../../components/username'
|
||||
import { MetadataController, NostrController } from '../../controllers'
|
||||
import { appPrivateRoutes } from '../../routes'
|
||||
import { State } from '../../store/rootReducer'
|
||||
import { Meta, ProfileMetadata, User, UserRole } from '../../types'
|
||||
import {
|
||||
CreateSignatureEventContent,
|
||||
Meta,
|
||||
ProfileMetadata,
|
||||
User,
|
||||
UserRole
|
||||
} from '../../types'
|
||||
import {
|
||||
decryptArrayBuffer,
|
||||
encryptArrayBuffer,
|
||||
@ -44,6 +52,7 @@ import {
|
||||
uploadToFileStorage
|
||||
} from '../../utils'
|
||||
import styles from './style.module.scss'
|
||||
import { Download } from '@mui/icons-material'
|
||||
|
||||
enum SignedStatus {
|
||||
Fully_Signed,
|
||||
@ -68,6 +77,19 @@ export const SignPage = () => {
|
||||
const [meta, setMeta] = useState<Meta | null>(null)
|
||||
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 usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
|
||||
@ -76,42 +98,70 @@ export const SignPage = () => {
|
||||
const nostrController = NostrController.getInstance()
|
||||
|
||||
useEffect(() => {
|
||||
if (meta) {
|
||||
setDisplayInput(false)
|
||||
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)
|
||||
|
||||
// get list of users who have signed
|
||||
const signedBy = Object.keys(meta.signedEvents)
|
||||
// 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 (meta.signers.length > 0) {
|
||||
// check if all signers have signed then its fully signed
|
||||
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)!)
|
||||
if (arrayBuffer) {
|
||||
const hash = await getHash(arrayBuffer)
|
||||
|
||||
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
|
||||
if (hash) {
|
||||
fileHashes[fileName.replace(/^files\//, '')] = hash
|
||||
}
|
||||
} else {
|
||||
fileHashes[fileName.replace(/^files\//, '')] = null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// there's no signer just viewers. So its fully signed
|
||||
setSignedStatus(SignedStatus.Fully_Signed)
|
||||
|
||||
setCurrentFileHashes(fileHashes)
|
||||
}
|
||||
|
||||
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(() => {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -252,36 +349,11 @@ export const SignPage = () => {
|
||||
|
||||
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')
|
||||
const signedEvent = await signEventForMetaFile(
|
||||
fileHashes,
|
||||
JSON.stringify({
|
||||
fileHashes: currentFileHashes
|
||||
}),
|
||||
nostrController,
|
||||
setIsLoading
|
||||
)
|
||||
@ -290,8 +362,8 @@ export const SignPage = () => {
|
||||
|
||||
const metaCopy = _.cloneDeep(meta)
|
||||
|
||||
metaCopy.signedEvents = {
|
||||
...metaCopy.signedEvents,
|
||||
metaCopy.docSignatures = {
|
||||
...metaCopy.docSignatures,
|
||||
[hexToNpub(signedEvent.pubkey)]: JSON.stringify(signedEvent, null, 2)
|
||||
}
|
||||
|
||||
@ -349,21 +421,23 @@ export const SignPage = () => {
|
||||
|
||||
// check if the current user is the last signer
|
||||
const usersNpub = hexToNpub(usersPubkey!)
|
||||
const lastSignerIndex = meta.signers.length - 1
|
||||
const signerIndex = meta.signers.indexOf(usersNpub)
|
||||
const lastSignerIndex = signers.length - 1
|
||||
const signerIndex = signers.indexOf(usersNpub)
|
||||
const isLastSigner = signerIndex === lastSignerIndex
|
||||
|
||||
// if current user is the last signer, then send DMs to all signers and viewers
|
||||
if (isLastSigner) {
|
||||
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)
|
||||
})
|
||||
|
||||
meta.viewers.forEach((viewer) => {
|
||||
viewers.forEach((viewer) => {
|
||||
userSet.add(viewer)
|
||||
})
|
||||
|
||||
@ -381,7 +455,7 @@ export const SignPage = () => {
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const nextSigner = meta.signers[signerIndex + 1]
|
||||
const nextSigner = signers[signerIndex + 1]
|
||||
await sendDM(
|
||||
fileUrl,
|
||||
key,
|
||||
@ -406,28 +480,21 @@ export const SignPage = () => {
|
||||
|
||||
const usersNpub = hexToNpub(usersPubkey)
|
||||
if (
|
||||
!meta.signers.includes(usersNpub) &&
|
||||
!meta.viewers.includes(usersNpub) &&
|
||||
meta.submittedBy !== usersNpub
|
||||
!signers.includes(usersNpub) &&
|
||||
!viewers.includes(usersNpub) &&
|
||||
submittedBy !== usersNpub
|
||||
)
|
||||
return
|
||||
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('Signing nostr event')
|
||||
const event: EventTemplate = {
|
||||
kind: 1,
|
||||
content: '',
|
||||
created_at: Math.floor(Date.now() / 1000), // Current timestamp
|
||||
tags: []
|
||||
}
|
||||
|
||||
// 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
|
||||
})
|
||||
const signedEvent = await signEventForMetaFile(
|
||||
JSON.stringify({
|
||||
fileHashes: currentFileHashes
|
||||
}),
|
||||
nostrController,
|
||||
setIsLoading
|
||||
)
|
||||
|
||||
if (!signedEvent) return
|
||||
|
||||
@ -516,29 +583,33 @@ export const SignPage = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{meta && signedStatus === SignedStatus.Fully_Signed && (
|
||||
{submittedBy && zip && (
|
||||
<>
|
||||
<DisplayMeta meta={meta} nextSigner={nextSinger} />
|
||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||
<Button onClick={handleExport} variant="contained">
|
||||
Export
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
<DisplayMeta
|
||||
zip={zip}
|
||||
submittedBy={submittedBy}
|
||||
signers={signers}
|
||||
viewers={viewers}
|
||||
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 && (
|
||||
<DisplayMeta meta={meta} nextSigner={nextSinger} />
|
||||
)}
|
||||
|
||||
{meta && 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
|
||||
</Button>
|
||||
</Box>
|
||||
{signedStatus === SignedStatus.User_Is_Next_Signer && (
|
||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||
<Button onClick={handleSign} variant="contained">
|
||||
Sign
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
@ -547,11 +618,26 @@ export const SignPage = () => {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
|
||||
const DisplayMeta = ({
|
||||
zip,
|
||||
submittedBy,
|
||||
signers,
|
||||
viewers,
|
||||
creatorFileHashes,
|
||||
currentFileHashes,
|
||||
signedBy,
|
||||
nextSigner
|
||||
}: DisplayMetaProps) => {
|
||||
const theme = useTheme()
|
||||
|
||||
const textColor = theme.palette.getContrastText(
|
||||
@ -564,7 +650,7 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
meta.signers.forEach((signer) => {
|
||||
signers.forEach((signer) => {
|
||||
const hexKey = npubToHex(signer)
|
||||
setUsers((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)
|
||||
setUsers((prev) => {
|
||||
if (prev.findIndex((user) => user.pubkey === hexKey) !== -1) return prev
|
||||
@ -593,13 +679,13 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
|
||||
]
|
||||
})
|
||||
})
|
||||
}, [meta])
|
||||
}, [signers, viewers])
|
||||
|
||||
useEffect(() => {
|
||||
const metadataController = new MetadataController()
|
||||
|
||||
const hexKeys: string[] = [
|
||||
npubToHex(meta.submittedBy)!,
|
||||
npubToHex(submittedBy)!,
|
||||
...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 (
|
||||
<List
|
||||
@ -631,17 +729,7 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
|
||||
marginTop: 2
|
||||
}}
|
||||
subheader={
|
||||
<ListSubheader
|
||||
sx={{
|
||||
borderBottom: '0.5px solid',
|
||||
paddingBottom: 1,
|
||||
paddingTop: 1,
|
||||
fontSize: '1.5rem'
|
||||
}}
|
||||
className={styles.subHeader}
|
||||
>
|
||||
Meta Info
|
||||
</ListSubheader>
|
||||
<ListSubheader className={styles.subHeader}>Meta Info</ListSubheader>
|
||||
}
|
||||
>
|
||||
<ListItem
|
||||
@ -654,17 +742,14 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
|
||||
Submitted By
|
||||
</Typography>
|
||||
{(function () {
|
||||
const pubkey = npubToHex(meta.submittedBy)
|
||||
const profile = metadata[pubkey!]
|
||||
const profile = metadata[submittedBy]
|
||||
return (
|
||||
<UserComponent
|
||||
pubkey={pubkey!}
|
||||
pubkey={submittedBy}
|
||||
name={
|
||||
profile?.display_name ||
|
||||
profile?.name ||
|
||||
shorten(meta.submittedBy)
|
||||
profile?.display_name || profile?.name || shorten(submittedBy)
|
||||
}
|
||||
image={metadata[meta.submittedBy]?.picture}
|
||||
image={profile?.picture}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
@ -679,13 +764,40 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
|
||||
<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>
|
||||
<Box className={styles.filesWrapper}>
|
||||
{Object.entries(currentFileHashes).map(([filename, hash], index) => {
|
||||
const isValidHash = creatorFileHashes[filename] === hash
|
||||
|
||||
return (
|
||||
<Box key={`file-${index}`} className={styles.file}>
|
||||
<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 sx={{ marginTop: 1 }}>
|
||||
<Table>
|
||||
@ -705,7 +817,7 @@ const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
|
||||
if (user.role === UserRole.signer) {
|
||||
// check if user has signed the document
|
||||
const usersNpub = hexToNpub(user.pubkey)
|
||||
if (usersNpub in meta.signedEvents) {
|
||||
if (signedBy.includes(usersNpub)) {
|
||||
signedStatus = 'Signed'
|
||||
}
|
||||
// check if user is the next signer
|
||||
|
@ -10,6 +10,25 @@
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -15,8 +15,9 @@ import { toast } from 'react-toastify'
|
||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||
import { UserComponent } from '../../components/username'
|
||||
import { MetadataController } from '../../controllers'
|
||||
import { Meta, ProfileMetadata } from '../../types'
|
||||
import { CreateSignatureEventContent, Meta, ProfileMetadata } from '../../types'
|
||||
import {
|
||||
getHash,
|
||||
hexToNpub,
|
||||
npubToHex,
|
||||
parseJson,
|
||||
@ -36,17 +37,64 @@ export const VerifyPage = () => {
|
||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||
const [zip, setZip] = useState<JSZip>()
|
||||
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 }>(
|
||||
{}
|
||||
)
|
||||
|
||||
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 users = [meta.submittedBy, ...meta.signers, ...meta.viewers]
|
||||
const users = [submittedBy, ...signers, ...viewers]
|
||||
|
||||
users.forEach((user) => {
|
||||
const pubkey = npubToHex(user)!
|
||||
@ -72,7 +120,7 @@ export const VerifyPage = () => {
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [meta])
|
||||
}, [submittedBy, signers, viewers])
|
||||
|
||||
const handleVerify = async () => {
|
||||
if (!selectedFile) return
|
||||
@ -85,6 +133,7 @@ export const VerifyPage = () => {
|
||||
})
|
||||
|
||||
if (!zip) return
|
||||
setZip(zip)
|
||||
|
||||
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)
|
||||
setIsLoading(false)
|
||||
}
|
||||
@ -121,7 +215,7 @@ export const VerifyPage = () => {
|
||||
|
||||
if (verifySignature) {
|
||||
const npub = hexToNpub(pubkey)
|
||||
const signedEventString = meta ? meta.signedEvents[npub] : null
|
||||
const signedEventString = meta ? meta.docSignatures[npub] : null
|
||||
if (signedEventString) {
|
||||
try {
|
||||
const signedEvent = JSON.parse(signedEventString)
|
||||
@ -150,11 +244,11 @@ export const VerifyPage = () => {
|
||||
component="label"
|
||||
sx={{
|
||||
color: isValidSignature
|
||||
? theme.palette.text.primary
|
||||
? theme.palette.success.light
|
||||
: theme.palette.error.main
|
||||
}}
|
||||
>
|
||||
({isValidSignature ? 'Valid' : 'Invalid'} Signature)
|
||||
{isValidSignature ? 'Valid' : 'Invalid'} Signature
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
@ -229,17 +323,19 @@ export const VerifyPage = () => {
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<ListItem
|
||||
sx={{
|
||||
marginTop: 1,
|
||||
gap: '15px'
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ color: textColor }}>
|
||||
Submitted By
|
||||
</Typography>
|
||||
{displayUser(npubToHex(meta.submittedBy)!)}
|
||||
</ListItem>
|
||||
{submittedBy && (
|
||||
<ListItem
|
||||
sx={{
|
||||
marginTop: 1,
|
||||
gap: '15px'
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ color: textColor }}>
|
||||
Submitted By
|
||||
</Typography>
|
||||
{displayUser(submittedBy)}
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
<ListItem
|
||||
sx={{
|
||||
@ -253,7 +349,7 @@ export const VerifyPage = () => {
|
||||
{displayExportedBy()}
|
||||
</ListItem>
|
||||
|
||||
{meta.signers.length > 0 && (
|
||||
{signers.length > 0 && (
|
||||
<ListItem
|
||||
sx={{
|
||||
marginTop: 1,
|
||||
@ -265,7 +361,7 @@ export const VerifyPage = () => {
|
||||
Signers
|
||||
</Typography>
|
||||
<ul className={styles.usersList}>
|
||||
{meta.signers.map((signer) => (
|
||||
{signers.map((signer) => (
|
||||
<li
|
||||
key={signer}
|
||||
style={{
|
||||
@ -282,7 +378,7 @@ export const VerifyPage = () => {
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
{meta.viewers.length > 0 && (
|
||||
{viewers.length > 0 && (
|
||||
<ListItem
|
||||
sx={{
|
||||
marginTop: 1,
|
||||
@ -294,7 +390,7 @@ export const VerifyPage = () => {
|
||||
Viewers
|
||||
</Typography>
|
||||
<ul className={styles.usersList}>
|
||||
{meta.viewers.map((viewer) => (
|
||||
{viewers.map((viewer) => (
|
||||
<li key={viewer} style={{ color: textColor }}>
|
||||
{displayUser(npubToHex(viewer)!)}
|
||||
</li>
|
||||
@ -313,13 +409,37 @@ export const VerifyPage = () => {
|
||||
<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>
|
||||
<Box className={styles.filesWrapper}>
|
||||
{Object.entries(currentFileHashes).map(
|
||||
([filename, hash], index) => {
|
||||
const isValidHash = creatorFileHashes[filename] === hash
|
||||
|
||||
return (
|
||||
<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>
|
||||
</List>
|
||||
</>
|
||||
|
@ -10,6 +10,19 @@
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.filesWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-left: 15px;
|
||||
|
||||
.file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.usersList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -9,10 +9,13 @@ export interface User {
|
||||
}
|
||||
|
||||
export interface Meta {
|
||||
createSignature: string
|
||||
docSignatures: { [key: `npub1${string}`]: string }
|
||||
exportSignature?: string
|
||||
}
|
||||
|
||||
export interface CreateSignatureEventContent {
|
||||
signers: `npub1${string}`[]
|
||||
viewers: `npub1${string}`[]
|
||||
fileHashes: { [key: string]: string }
|
||||
submittedBy: `npub1${string}`
|
||||
signedEvents: { [key: `npub1${string}`]: string }
|
||||
exportSignature?: string
|
||||
}
|
||||
|
@ -177,22 +177,20 @@ export const sendDM = async (
|
||||
|
||||
/**
|
||||
* 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 setIsLoading Function to set loading state in the component.
|
||||
* @returns A Promise resolving to the signed event, or null if signing fails.
|
||||
*/
|
||||
export const signEventForMetaFile = async (
|
||||
fileHashes: {
|
||||
[key: string]: string
|
||||
},
|
||||
content: string,
|
||||
nostrController: NostrController,
|
||||
setIsLoading: (value: React.SetStateAction<boolean>) => void
|
||||
) => {
|
||||
// Construct the event metadata for the meta file
|
||||
const event: EventTemplate = {
|
||||
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
|
||||
tags: []
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user