Signing Page - New Design #152
246
src/components/FilesUsers.tsx/index.tsx
Normal file
246
src/components/FilesUsers.tsx/index.tsx
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import { CheckCircle, Cancel } from '@mui/icons-material'
|
||||||
|
import { Divider, Tooltip } from '@mui/material'
|
||||||
|
import { verifyEvent } from 'nostr-tools'
|
||||||
|
import { useSigitProfiles } from '../../hooks/useSigitProfiles'
|
||||||
|
import { Meta, SignedEventContent } from '../../types'
|
||||||
|
import {
|
||||||
|
extractFileExtensions,
|
||||||
|
formatTimestamp,
|
||||||
|
fromUnixTimestamp,
|
||||||
|
hexToNpub,
|
||||||
|
npubToHex,
|
||||||
|
shorten
|
||||||
|
} from '../../utils'
|
||||||
|
import { UserAvatar } from '../UserAvatar'
|
||||||
|
import { useSigitMeta } from '../../hooks/useSigitMeta'
|
||||||
|
import { UserAvatarGroup } from '../UserAvatarGroup'
|
||||||
|
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import {
|
||||||
|
faCalendar,
|
||||||
|
faCalendarCheck,
|
||||||
|
faCalendarPlus,
|
||||||
|
faEye,
|
||||||
|
faFile,
|
||||||
|
faFileCircleExclamation
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { getExtensionIconLabel } from '../getExtensionIconLabel'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { State } from '../../store/rootReducer'
|
||||||
|
|
||||||
|
interface FileUsersProps {
|
||||||
|
meta: Meta
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileUsers = ({ meta }: FileUsersProps) => {
|
||||||
|
const { usersPubkey } = useSelector((state: State) => state.auth)
|
||||||
|
const {
|
||||||
|
submittedBy,
|
||||||
|
signers,
|
||||||
|
viewers,
|
||||||
|
fileHashes,
|
||||||
|
sig,
|
||||||
|
docSignatures,
|
||||||
|
parsedSignatureEvents,
|
||||||
|
createdAt,
|
||||||
|
signedStatus,
|
||||||
|
completedAt
|
||||||
|
} = useSigitMeta(meta)
|
||||||
|
const profiles = useSigitProfiles([
|
||||||
|
...(submittedBy ? [submittedBy] : []),
|
||||||
|
...signers,
|
||||||
|
...viewers
|
||||||
|
])
|
||||||
|
const userCanSign =
|
||||||
|
typeof usersPubkey !== 'undefined' &&
|
||||||
|
signers.includes(hexToNpub(usersPubkey))
|
||||||
|
|
||||||
|
const ext = extractFileExtensions(Object.keys(fileHashes))
|
||||||
|
|
||||||
|
const getPrevSignersSig = (npub: string) => {
|
||||||
|
// if user is first signer then use creator's signature
|
||||||
|
if (signers[0] === npub) {
|
||||||
|
return sig
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the index of signer
|
||||||
|
const currentSignerIndex = signers.findIndex((signer) => signer === npub)
|
||||||
|
// return null if could not found user in signer's list
|
||||||
|
if (currentSignerIndex === -1) return null
|
||||||
|
// find prev signer
|
||||||
|
const prevSigner = signers[currentSignerIndex - 1]
|
||||||
|
|
||||||
|
// get the signature of prev signer
|
||||||
|
try {
|
||||||
|
const prevSignersEvent = parsedSignatureEvents[prevSigner]
|
||||||
|
return prevSignersEvent.sig
|
||||||
|
} catch (error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayUser = (pubkey: string, verifySignature = false) => {
|
||||||
|
const profile = profiles[pubkey]
|
||||||
|
|
||||||
|
let isValidSignature = false
|
||||||
|
|
||||||
|
if (verifySignature) {
|
||||||
|
const npub = hexToNpub(pubkey)
|
||||||
|
const signedEventString = docSignatures[npub]
|
||||||
|
if (signedEventString) {
|
||||||
|
try {
|
||||||
|
const signedEvent = JSON.parse(signedEventString)
|
||||||
|
const isVerifiedEvent = verifyEvent(signedEvent)
|
||||||
|
|
||||||
|
if (isVerifiedEvent) {
|
||||||
|
// get the actual signature of prev signer
|
||||||
|
const prevSignersSig = getPrevSignersSig(npub)
|
||||||
|
|
||||||
|
// get the signature of prev signer from the content of current signers signedEvent
|
||||||
|
|
||||||
|
try {
|
||||||
|
const obj: SignedEventContent = JSON.parse(signedEvent.content)
|
||||||
|
if (
|
||||||
|
obj.prevSig &&
|
||||||
|
prevSignersSig &&
|
||||||
|
obj.prevSig === prevSignersSig
|
||||||
|
) {
|
||||||
|
isValidSignature = true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
isValidSignature = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`An error occurred in parsing and verifying the signature event for ${pubkey}`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<UserAvatar
|
||||||
|
pubkey={pubkey}
|
||||||
|
name={
|
||||||
|
profile?.display_name || profile?.name || shorten(hexToNpub(pubkey))
|
||||||
|
}
|
||||||
|
image={profile?.picture}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{verifySignature && (
|
||||||
|
<>
|
||||||
|
{isValidSignature && (
|
||||||
|
<Tooltip title="Valid signature">
|
||||||
|
<CheckCircle sx={{ color: 'green' }} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isValidSignature && (
|
||||||
|
<Tooltip title="Invalid signature">
|
||||||
|
<Cancel sx={{ color: 'red' }} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return submittedBy ? (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.section}>
|
||||||
|
<p>Signers</p>
|
||||||
|
{displayUser(submittedBy)}
|
||||||
|
{submittedBy && signers.length ? (
|
||||||
|
<Divider orientation="vertical" flexItem />
|
||||||
|
) : null}
|
||||||
|
<UserAvatarGroup max={7}>
|
||||||
|
{signers.length > 0 &&
|
||||||
|
signers.map((signer) => (
|
||||||
|
<span key={signer}>{displayUser(npubToHex(signer)!, true)}</span>
|
||||||
|
))}
|
||||||
|
{viewers.length > 0 &&
|
||||||
|
viewers.map((viewer) => (
|
||||||
|
<span key={viewer}>{displayUser(npubToHex(viewer)!)}</span>
|
||||||
|
))}
|
||||||
|
</UserAvatarGroup>
|
||||||
|
</div>
|
||||||
|
<div className={styles.section}>
|
||||||
|
<p>Details</p>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
title={'Publication date'}
|
||||||
|
placement="top"
|
||||||
|
arrow
|
||||||
|
disableInteractive
|
||||||
|
>
|
||||||
|
<span className={styles.detailsItem}>
|
||||||
|
<FontAwesomeIcon icon={faCalendarPlus} />{' '}
|
||||||
|
{createdAt ? formatTimestamp(createdAt) : <>—</>}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
title={'Completion date'}
|
||||||
|
placement="top"
|
||||||
|
arrow
|
||||||
|
disableInteractive
|
||||||
|
>
|
||||||
|
<span className={styles.detailsItem}>
|
||||||
|
<FontAwesomeIcon icon={faCalendarCheck} />{' '}
|
||||||
|
{completedAt ? formatTimestamp(completedAt) : <>—</>}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* User signed date */}
|
||||||
|
{userCanSign ? (
|
||||||
|
<Tooltip
|
||||||
|
title={'Your signature date'}
|
||||||
|
placement="top"
|
||||||
|
arrow
|
||||||
|
disableInteractive
|
||||||
|
>
|
||||||
|
<span className={styles.detailsItem}>
|
||||||
|
<FontAwesomeIcon icon={faCalendar} />{' '}
|
||||||
|
{hexToNpub(usersPubkey) in parsedSignatureEvents ? (
|
||||||
|
parsedSignatureEvents[hexToNpub(usersPubkey)].created_at ? (
|
||||||
|
formatTimestamp(
|
||||||
|
fromUnixTimestamp(
|
||||||
|
parsedSignatureEvents[hexToNpub(usersPubkey)].created_at
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<>—</>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<>—</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
<span className={styles.detailsItem}>
|
||||||
|
<FontAwesomeIcon icon={faEye} /> {signedStatus}
|
||||||
|
</span>
|
||||||
|
{ext.length > 0 ? (
|
||||||
|
<span className={styles.detailsItem}>
|
||||||
|
{ext.length > 1 ? (
|
||||||
|
<>
|
||||||
|
<FontAwesomeIcon icon={faFile} /> Multiple File Types
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
getExtensionIconLabel(ext[0])
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FontAwesomeIcon icon={faFileCircleExclamation} /> —
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : undefined
|
||||||
|
}
|
41
src/components/FilesUsers.tsx/style.module.scss
Normal file
41
src/components/FilesUsers.tsx/style.module.scss
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
@import '../../styles/colors.scss';
|
||||||
|
|
||||||
|
.container {
|
||||||
|
border-radius: 4px;
|
||||||
|
background: $overlay-background-color;
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
grid-gap: 25px;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
grid-gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailsItem {
|
||||||
|
transition: ease 0.2s;
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
font-size: 14px;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: start;
|
||||||
|
|
||||||
|
> :first-child {
|
||||||
|
padding: 5px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $primary-main;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,7 @@ import { Mark } from '../types/mark'
|
|||||||
import {
|
import {
|
||||||
fromUnixTimestamp,
|
fromUnixTimestamp,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
parseCreateSignatureEvent,
|
parseNostrEvent,
|
||||||
parseCreateSignatureEventContent,
|
parseCreateSignatureEventContent,
|
||||||
SigitMetaParseError,
|
SigitMetaParseError,
|
||||||
SigitStatus,
|
SigitStatus,
|
||||||
@ -21,7 +21,7 @@ import { NostrController } from '../controllers'
|
|||||||
* Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`,
|
* Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`,
|
||||||
* and `Event` (event's fields are made optional and pubkey and created_at replaced with our versions)
|
* and `Event` (event's fields are made optional and pubkey and created_at replaced with our versions)
|
||||||
*/
|
*/
|
||||||
interface FlatMeta
|
export interface FlatMeta
|
||||||
extends Meta,
|
extends Meta,
|
||||||
CreateSignatureEventContent,
|
CreateSignatureEventContent,
|
||||||
Partial<Omit<Event, 'pubkey' | 'created_at'>> {
|
Partial<Omit<Event, 'pubkey' | 'created_at'>> {
|
||||||
@ -37,6 +37,12 @@ interface FlatMeta
|
|||||||
// Decryption
|
// Decryption
|
||||||
encryptionKey: string | null
|
encryptionKey: string | null
|
||||||
|
|
||||||
|
// Parsed Document Signatures
|
||||||
|
parsedSignatureEvents: { [signer: `npub1${string}`]: Event }
|
||||||
|
|
||||||
|
// Calculated completion time
|
||||||
|
completedAt?: number
|
||||||
|
|
||||||
// Calculated status fields
|
// Calculated status fields
|
||||||
signedStatus: SigitStatus
|
signedStatus: SigitStatus
|
||||||
signersStatus: {
|
signersStatus: {
|
||||||
@ -67,6 +73,12 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
const [title, setTitle] = useState<string>('')
|
const [title, setTitle] = useState<string>('')
|
||||||
const [zipUrl, setZipUrl] = useState<string>('')
|
const [zipUrl, setZipUrl] = useState<string>('')
|
||||||
|
|
||||||
|
const [parsedSignatureEvents, setParsedSignatureEvents] = useState<{
|
||||||
|
[signer: `npub1${string}`]: Event
|
||||||
|
}>({})
|
||||||
|
|
||||||
|
const [completedAt, setCompletedAt] = useState<number>()
|
||||||
|
|
||||||
const [signedStatus, setSignedStatus] = useState<SigitStatus>(
|
const [signedStatus, setSignedStatus] = useState<SigitStatus>(
|
||||||
SigitStatus.Partial
|
SigitStatus.Partial
|
||||||
)
|
)
|
||||||
@ -80,9 +92,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
if (!meta) return
|
if (!meta) return
|
||||||
;(async function () {
|
;(async function () {
|
||||||
try {
|
try {
|
||||||
const createSignatureEvent = await parseCreateSignatureEvent(
|
const createSignatureEvent = await parseNostrEvent(meta.createSignature)
|
||||||
meta.createSignature
|
|
||||||
)
|
|
||||||
|
|
||||||
const { kind, tags, created_at, pubkey, id, sig, content } =
|
const { kind, tags, created_at, pubkey, id, sig, content } =
|
||||||
createSignatureEvent
|
createSignatureEvent
|
||||||
@ -131,13 +141,22 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse each signature event and set signer status
|
// Temp. map to hold events
|
||||||
|
const parsedSignatureEventsMap = new Map<`npub1${string}`, Event>()
|
||||||
for (const npub in meta.docSignatures) {
|
for (const npub in meta.docSignatures) {
|
||||||
try {
|
try {
|
||||||
const event = await parseCreateSignatureEvent(
|
// Parse each signature event
|
||||||
|
const event = await parseNostrEvent(
|
||||||
meta.docSignatures[npub as `npub1${string}`]
|
meta.docSignatures[npub as `npub1${string}`]
|
||||||
)
|
)
|
||||||
|
|
||||||
const isValidSignature = verifyEvent(event)
|
const isValidSignature = verifyEvent(event)
|
||||||
|
|
||||||
|
// Save events to a map, to save all at once outside loop
|
||||||
|
// We need the object to find completedAt
|
||||||
|
// Avoided using parsedSignatureEvents due to useEffect deps
|
||||||
|
parsedSignatureEventsMap.set(npub as `npub1${string}`, event)
|
||||||
|
|
||||||
setSignersStatus((prev) => {
|
setSignersStatus((prev) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
@ -155,6 +174,11 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setParsedSignatureEvents(
|
||||||
|
Object.fromEntries(parsedSignatureEventsMap.entries())
|
||||||
|
)
|
||||||
|
|
||||||
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
|
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
|
||||||
const isCompletelySigned = signers.every((signer) =>
|
const isCompletelySigned = signers.every((signer) =>
|
||||||
signedBy.includes(signer)
|
signedBy.includes(signer)
|
||||||
@ -162,6 +186,20 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
setSignedStatus(
|
setSignedStatus(
|
||||||
isCompletelySigned ? SigitStatus.Complete : SigitStatus.Partial
|
isCompletelySigned ? SigitStatus.Complete : SigitStatus.Partial
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Check if all signers signed and are valid
|
||||||
|
if (isCompletelySigned) {
|
||||||
|
setCompletedAt(
|
||||||
|
fromUnixTimestamp(
|
||||||
|
signedBy.reduce((p, c) => {
|
||||||
|
return Math.max(
|
||||||
|
p,
|
||||||
|
parsedSignatureEventsMap.get(c)?.created_at || 0
|
||||||
|
)
|
||||||
|
}, 0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof SigitMetaParseError) {
|
if (error instanceof SigitMetaParseError) {
|
||||||
toast.error(error.message)
|
toast.error(error.message)
|
||||||
@ -189,6 +227,8 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
markConfig,
|
markConfig,
|
||||||
title,
|
title,
|
||||||
zipUrl,
|
zipUrl,
|
||||||
|
parsedSignatureEvents,
|
||||||
|
completedAt,
|
||||||
signedStatus,
|
signedStatus,
|
||||||
signersStatus,
|
signersStatus,
|
||||||
encryptionKey
|
encryptionKey
|
||||||
|
@ -1,36 +1,20 @@
|
|||||||
import {
|
import { Box, Button, Tooltip, Typography, useTheme } from '@mui/material'
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListSubheader,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
useTheme
|
|
||||||
} from '@mui/material'
|
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import { MuiFileInput } from 'mui-file-input'
|
import { MuiFileInput } from 'mui-file-input'
|
||||||
import { Event, verifyEvent } from 'nostr-tools'
|
import { Event, verifyEvent } from 'nostr-tools'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
import { UserAvatar } from '../../components/UserAvatar'
|
|
||||||
import { NostrController } from '../../controllers'
|
import { NostrController } from '../../controllers'
|
||||||
import {
|
import { CreateSignatureEventContent, Meta } from '../../types'
|
||||||
CreateSignatureEventContent,
|
|
||||||
Meta,
|
|
||||||
SignedEventContent
|
|
||||||
} from '../../types'
|
|
||||||
import {
|
import {
|
||||||
decryptArrayBuffer,
|
decryptArrayBuffer,
|
||||||
extractMarksFromSignedMeta,
|
extractMarksFromSignedMeta,
|
||||||
getHash,
|
getHash,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
unixNow,
|
unixNow,
|
||||||
npubToHex,
|
|
||||||
parseJson,
|
parseJson,
|
||||||
readContentOfZipEntry,
|
readContentOfZipEntry,
|
||||||
shorten,
|
|
||||||
signEventForMetaFile
|
signEventForMetaFile
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
@ -50,7 +34,8 @@ import { getLastSignersSig } from '../../utils/sign.ts'
|
|||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
import { Container } from '../../components/Container'
|
import { Container } from '../../components/Container'
|
||||||
import { useSigitMeta } from '../../hooks/useSigitMeta.tsx'
|
import { useSigitMeta } from '../../hooks/useSigitMeta.tsx'
|
||||||
import { useSigitProfiles } from '../../hooks/useSigitProfiles.tsx'
|
import { Files } from '../../layouts/Files.tsx'
|
||||||
|
import { FileUsers } from '../../components/FilesUsers.tsx/index.tsx'
|
||||||
|
|
||||||
export const VerifyPage = () => {
|
export const VerifyPage = () => {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
@ -67,11 +52,6 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
const { submittedBy, zipUrl, encryptionKey, signers, viewers, fileHashes } =
|
const { submittedBy, zipUrl, encryptionKey, signers, viewers, fileHashes } =
|
||||||
useSigitMeta(meta)
|
useSigitMeta(meta)
|
||||||
const profiles = useSigitProfiles([
|
|
||||||
...(submittedBy ? [submittedBy] : []),
|
|
||||||
...signers,
|
|
||||||
...viewers
|
|
||||||
])
|
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||||
@ -283,35 +263,6 @@ export const VerifyPage = () => {
|
|||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPrevSignersSig = (npub: string) => {
|
|
||||||
if (!meta) return null
|
|
||||||
|
|
||||||
// if user is first signer then use creator's signature
|
|
||||||
if (signers[0] === npub) {
|
|
||||||
try {
|
|
||||||
const createSignatureEvent: Event = JSON.parse(meta.createSignature)
|
|
||||||
return createSignatureEvent.sig
|
|
||||||
} catch (error) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// find the index of signer
|
|
||||||
const currentSignerIndex = signers.findIndex((signer) => signer === npub)
|
|
||||||
// return null if could not found user in signer's list
|
|
||||||
if (currentSignerIndex === -1) return null
|
|
||||||
// find prev signer
|
|
||||||
const prevSigner = signers[currentSignerIndex - 1]
|
|
||||||
|
|
||||||
// get the signature of prev signer
|
|
||||||
try {
|
|
||||||
const prevSignersEvent: Event = JSON.parse(meta.docSignatures[prevSigner])
|
|
||||||
return prevSignersEvent.sig
|
|
||||||
} catch (error) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
if (Object.entries(files).length === 0 || !meta || !usersPubkey) return
|
if (Object.entries(files).length === 0 || !meta || !usersPubkey) return
|
||||||
|
|
||||||
@ -379,76 +330,6 @@ export const VerifyPage = () => {
|
|||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayUser = (pubkey: string, verifySignature = false) => {
|
|
||||||
const profile = profiles[pubkey]
|
|
||||||
|
|
||||||
let isValidSignature = false
|
|
||||||
|
|
||||||
if (verifySignature) {
|
|
||||||
const npub = hexToNpub(pubkey)
|
|
||||||
const signedEventString = meta ? meta.docSignatures[npub] : null
|
|
||||||
if (signedEventString) {
|
|
||||||
try {
|
|
||||||
const signedEvent = JSON.parse(signedEventString)
|
|
||||||
const isVerifiedEvent = verifyEvent(signedEvent)
|
|
||||||
|
|
||||||
if (isVerifiedEvent) {
|
|
||||||
// get the actual signature of prev signer
|
|
||||||
const prevSignersSig = getPrevSignersSig(npub)
|
|
||||||
|
|
||||||
// get the signature of prev signer from the content of current signers signedEvent
|
|
||||||
|
|
||||||
try {
|
|
||||||
const obj: SignedEventContent = JSON.parse(signedEvent.content)
|
|
||||||
if (
|
|
||||||
obj.prevSig &&
|
|
||||||
prevSignersSig &&
|
|
||||||
obj.prevSig === prevSignersSig
|
|
||||||
) {
|
|
||||||
isValidSignature = true
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
isValidSignature = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`An error occurred in parsing and verifying the signature event for ${pubkey}`,
|
|
||||||
error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<UserAvatar
|
|
||||||
pubkey={pubkey}
|
|
||||||
name={
|
|
||||||
profile?.display_name || profile?.name || shorten(hexToNpub(pubkey))
|
|
||||||
}
|
|
||||||
image={profile?.picture}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{verifySignature && (
|
|
||||||
<>
|
|
||||||
{isValidSignature && (
|
|
||||||
<Tooltip title="Valid signature">
|
|
||||||
<CheckCircle sx={{ color: theme.palette.success.light }} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isValidSignature && (
|
|
||||||
<Tooltip title="Invalid signature">
|
|
||||||
<Cancel sx={{ color: theme.palette.error.main }} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayExportedBy = () => {
|
const displayExportedBy = () => {
|
||||||
if (!meta || !meta.exportSignature) return null
|
if (!meta || !meta.exportSignature) return null
|
||||||
|
|
||||||
@ -458,7 +339,7 @@ export const VerifyPage = () => {
|
|||||||
const exportSignatureEvent = JSON.parse(exportSignatureString) as Event
|
const exportSignatureEvent = JSON.parse(exportSignatureString) as Event
|
||||||
|
|
||||||
if (verifyEvent(exportSignatureEvent)) {
|
if (verifyEvent(exportSignatureEvent)) {
|
||||||
return displayUser(exportSignatureEvent.pubkey)
|
// return displayUser(exportSignatureEvent.pubkey)
|
||||||
} else {
|
} else {
|
||||||
toast.error(`Invalid export signature!`)
|
toast.error(`Invalid export signature!`)
|
||||||
return (
|
return (
|
||||||
@ -505,109 +386,9 @@ export const VerifyPage = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{meta && (
|
{meta && (
|
||||||
|
<Files
|
||||||
|
left={
|
||||||
<>
|
<>
|
||||||
<List
|
|
||||||
sx={{
|
|
||||||
bgcolor: 'background.paper',
|
|
||||||
marginTop: 2
|
|
||||||
}}
|
|
||||||
subheader={
|
|
||||||
<ListSubheader className={styles.subHeader}>
|
|
||||||
Meta Info
|
|
||||||
</ListSubheader>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{submittedBy && (
|
|
||||||
<ListItem
|
|
||||||
sx={{
|
|
||||||
marginTop: 1,
|
|
||||||
gap: '15px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h6" sx={{ color: textColor }}>
|
|
||||||
Submitted By
|
|
||||||
</Typography>
|
|
||||||
{displayUser(submittedBy)}
|
|
||||||
</ListItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ListItem
|
|
||||||
sx={{
|
|
||||||
marginTop: 1,
|
|
||||||
gap: '15px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h6" sx={{ color: textColor }}>
|
|
||||||
Exported By
|
|
||||||
</Typography>
|
|
||||||
{displayExportedBy()}
|
|
||||||
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
|
||||||
<Button onClick={handleExport} variant="contained">
|
|
||||||
Export Sigit
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</ListItem>
|
|
||||||
|
|
||||||
{signers.length > 0 && (
|
|
||||||
<ListItem
|
|
||||||
sx={{
|
|
||||||
marginTop: 1,
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'flex-start'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h6" sx={{ color: textColor }}>
|
|
||||||
Signers
|
|
||||||
</Typography>
|
|
||||||
<ul className={styles.usersList}>
|
|
||||||
{signers.map((signer) => (
|
|
||||||
<li
|
|
||||||
key={signer}
|
|
||||||
style={{
|
|
||||||
color: textColor,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '15px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{displayUser(npubToHex(signer)!, true)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</ListItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{viewers.length > 0 && (
|
|
||||||
<ListItem
|
|
||||||
sx={{
|
|
||||||
marginTop: 1,
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'flex-start'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h6" sx={{ color: textColor }}>
|
|
||||||
Viewers
|
|
||||||
</Typography>
|
|
||||||
<ul className={styles.usersList}>
|
|
||||||
{viewers.map((viewer) => (
|
|
||||||
<li key={viewer} style={{ color: textColor }}>
|
|
||||||
{displayUser(npubToHex(viewer)!)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</ListItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ListItem
|
|
||||||
sx={{
|
|
||||||
marginTop: 1,
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'flex-start'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h6" sx={{ color: textColor }}>
|
|
||||||
Files
|
|
||||||
</Typography>
|
|
||||||
<Box className={styles.filesWrapper}>
|
<Box className={styles.filesWrapper}>
|
||||||
{Object.entries(currentFileHashes).map(
|
{Object.entries(currentFileHashes).map(
|
||||||
([filename, hash], index) => {
|
([filename, hash], index) => {
|
||||||
@ -643,9 +424,17 @@ export const VerifyPage = () => {
|
|||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</ListItem>
|
{displayExportedBy()}
|
||||||
</List>
|
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Button onClick={handleExport} variant="contained">
|
||||||
|
Export Sigit
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
</>
|
</>
|
||||||
|
}
|
||||||
|
right={<FileUsers meta={meta} />}
|
||||||
|
content={<div style={{ height: '300vh' }}></div>}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
|
@ -62,7 +62,7 @@ function handleError(error: unknown): Error {
|
|||||||
|
|
||||||
// Reuse common error messages for meta parsing
|
// Reuse common error messages for meta parsing
|
||||||
export enum SigitMetaParseErrorType {
|
export enum SigitMetaParseErrorType {
|
||||||
'PARSE_ERROR_SIGNATURE_EVENT' = 'error occurred in parsing the create signature event',
|
'PARSE_ERROR_EVENT' = 'error occurred in parsing the create signature event',
|
||||||
'PARSE_ERROR_SIGNATURE_EVENT_CONTENT' = "err in parsing the createSignature event's content"
|
'PARSE_ERROR_SIGNATURE_EVENT_CONTENT' = "err in parsing the createSignature event's content"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,24 +76,19 @@ export interface SigitCardDisplayInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper for createSignatureEvent parse that throws custom SigitMetaParseError with cause and context
|
* Wrapper for event parser that throws custom SigitMetaParseError with cause and context
|
||||||
* @param raw Raw string for parsing
|
* @param raw Raw string for parsing
|
||||||
* @returns parsed Event
|
* @returns parsed Event
|
||||||
*/
|
*/
|
||||||
export const parseCreateSignatureEvent = async (
|
export const parseNostrEvent = async (raw: string): Promise<Event> => {
|
||||||
raw: string
|
|
||||||
): Promise<Event> => {
|
|
||||||
try {
|
try {
|
||||||
const createSignatureEvent = await parseJson<Event>(raw)
|
const event = await parseJson<Event>(raw)
|
||||||
return createSignatureEvent
|
return event
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new SigitMetaParseError(
|
throw new SigitMetaParseError(SigitMetaParseErrorType.PARSE_ERROR_EVENT, {
|
||||||
SigitMetaParseErrorType.PARSE_ERROR_SIGNATURE_EVENT,
|
|
||||||
{
|
|
||||||
cause: handleError(error),
|
cause: handleError(error),
|
||||||
context: raw
|
context: raw
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,9 +130,7 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const createSignatureEvent = await parseCreateSignatureEvent(
|
const createSignatureEvent = await parseNostrEvent(meta.createSignature)
|
||||||
meta.createSignature
|
|
||||||
)
|
|
||||||
|
|
||||||
// created_at in nostr events are stored in seconds
|
// created_at in nostr events are stored in seconds
|
||||||
sigitInfo.createdAt = fromUnixTimestamp(createSignatureEvent.created_at)
|
sigitInfo.createdAt = fromUnixTimestamp(createSignatureEvent.created_at)
|
||||||
@ -147,13 +140,7 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const files = Object.keys(createSignatureContent.fileHashes)
|
const files = Object.keys(createSignatureContent.fileHashes)
|
||||||
const extensions = files.reduce((result: string[], file: string) => {
|
const extensions = extractFileExtensions(files)
|
||||||
const extension = file.split('.').pop()
|
|
||||||
if (extension) {
|
|
||||||
result.push(extension)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
|
const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
|
||||||
const isCompletelySigned = createSignatureContent.signers.every((signer) =>
|
const isCompletelySigned = createSignatureContent.signers.every((signer) =>
|
||||||
@ -179,3 +166,15 @@ export const extractSigitCardDisplayInfo = async (meta: Meta) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const extractFileExtensions = (fileNames: string[]) => {
|
||||||
|
const extensions = fileNames.reduce((result: string[], file: string) => {
|
||||||
|
const extension = file.split('.').pop()
|
||||||
|
if (extension) {
|
||||||
|
result.push(extension)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return extensions
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user