2024-05-14 09:27:05 +00:00
|
|
|
import {
|
|
|
|
Box,
|
|
|
|
Button,
|
2024-05-22 06:19:40 +00:00
|
|
|
IconButton,
|
2024-05-14 09:27:05 +00:00
|
|
|
List,
|
|
|
|
ListItem,
|
|
|
|
ListSubheader,
|
|
|
|
Table,
|
|
|
|
TableBody,
|
|
|
|
TableCell,
|
|
|
|
TableHead,
|
|
|
|
TableRow,
|
|
|
|
TextField,
|
2024-05-22 06:19:40 +00:00
|
|
|
Tooltip,
|
2024-05-14 09:27:05 +00:00
|
|
|
Typography,
|
|
|
|
useTheme
|
|
|
|
} from '@mui/material'
|
|
|
|
import axios from 'axios'
|
|
|
|
import saveAs from 'file-saver'
|
|
|
|
import JSZip from 'jszip'
|
|
|
|
import _ from 'lodash'
|
|
|
|
import { MuiFileInput } from 'mui-file-input'
|
2024-05-30 17:28:40 +00:00
|
|
|
import { Event, kinds, verifyEvent } from 'nostr-tools'
|
2024-05-14 09:27:05 +00:00
|
|
|
import { useEffect, useState } from 'react'
|
|
|
|
import { useSelector } from 'react-redux'
|
2024-06-03 09:01:24 +00:00
|
|
|
import { useNavigate, useSearchParams, useLocation } from 'react-router-dom'
|
2024-05-14 09:27:05 +00:00
|
|
|
import { toast } from 'react-toastify'
|
|
|
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
2024-05-16 11:22:05 +00:00
|
|
|
import { UserComponent } from '../../components/username'
|
2024-05-14 09:27:05 +00:00
|
|
|
import { MetadataController, NostrController } from '../../controllers'
|
2024-05-16 11:22:05 +00:00
|
|
|
import { appPrivateRoutes } from '../../routes'
|
2024-05-14 09:27:05 +00:00
|
|
|
import { State } from '../../store/rootReducer'
|
2024-05-22 06:19:40 +00:00
|
|
|
import {
|
|
|
|
CreateSignatureEventContent,
|
|
|
|
Meta,
|
|
|
|
ProfileMetadata,
|
2024-05-24 06:39:28 +00:00
|
|
|
SignedEventContent,
|
2024-05-22 06:19:40 +00:00
|
|
|
User,
|
|
|
|
UserRole
|
|
|
|
} from '../../types'
|
2024-05-14 09:27:05 +00:00
|
|
|
import {
|
|
|
|
decryptArrayBuffer,
|
|
|
|
encryptArrayBuffer,
|
|
|
|
generateEncryptionKey,
|
|
|
|
getHash,
|
|
|
|
hexToNpub,
|
|
|
|
parseJson,
|
2024-05-17 08:34:56 +00:00
|
|
|
npubToHex,
|
2024-05-14 09:27:05 +00:00
|
|
|
readContentOfZipEntry,
|
|
|
|
sendDM,
|
|
|
|
shorten,
|
|
|
|
signEventForMetaFile,
|
2024-05-31 07:14:33 +00:00
|
|
|
uploadToFileStorage,
|
|
|
|
isOnline
|
2024-05-14 09:27:05 +00:00
|
|
|
} from '../../utils'
|
|
|
|
import styles from './style.module.scss'
|
2024-05-24 06:39:28 +00:00
|
|
|
import {
|
|
|
|
Cancel,
|
|
|
|
CheckCircle,
|
|
|
|
Download,
|
|
|
|
HourglassTop
|
|
|
|
} from '@mui/icons-material'
|
2024-05-28 10:10:06 +00:00
|
|
|
import CopyModal from '../../components/copyModal'
|
2024-05-14 09:27:05 +00:00
|
|
|
enum SignedStatus {
|
|
|
|
Fully_Signed,
|
|
|
|
User_Is_Next_Signer,
|
|
|
|
User_Is_Not_Next_Signer
|
|
|
|
}
|
|
|
|
|
2024-05-15 11:11:57 +00:00
|
|
|
export const SignPage = () => {
|
2024-05-16 05:43:37 +00:00
|
|
|
const navigate = useNavigate()
|
2024-06-03 09:01:24 +00:00
|
|
|
const location = useLocation()
|
|
|
|
const { file, encryptionKey: encKey } = location.state || {}
|
|
|
|
|
2024-05-16 05:38:27 +00:00
|
|
|
const [searchParams, setSearchParams] = useSearchParams()
|
2024-05-14 09:27:05 +00:00
|
|
|
|
|
|
|
const [displayInput, setDisplayInput] = useState(false)
|
|
|
|
|
|
|
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
|
|
|
const [encryptionKey, setEncryptionKey] = useState('')
|
|
|
|
|
|
|
|
const [zip, setZip] = useState<JSZip>()
|
|
|
|
|
|
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
|
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
2024-05-28 10:10:06 +00:00
|
|
|
const [openCopyModal, setOpenCopyModel] = useState(false)
|
|
|
|
const [textToCopy, setTextToCopy] = useState('')
|
2024-05-14 09:27:05 +00:00
|
|
|
|
|
|
|
const [meta, setMeta] = useState<Meta | null>(null)
|
|
|
|
const [signedStatus, setSignedStatus] = useState<SignedStatus>()
|
|
|
|
|
2024-05-22 06:19:40 +00:00
|
|
|
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}`[]>([])
|
|
|
|
|
2024-05-14 09:27:05 +00:00
|
|
|
const [nextSinger, setNextSinger] = useState<string>()
|
|
|
|
|
2024-05-28 10:10:06 +00:00
|
|
|
// This state variable indicates whether the logged-in user is a signer, a creator, or neither.
|
|
|
|
const [isSignerOrCreator, setIsSignerOrCreator] = useState(false)
|
|
|
|
|
2024-05-14 09:27:05 +00:00
|
|
|
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
|
|
|
|
|
|
|
|
const [authUrl, setAuthUrl] = useState<string>()
|
|
|
|
const nostrController = NostrController.getInstance()
|
|
|
|
|
|
|
|
useEffect(() => {
|
2024-05-22 06:19:40 +00:00
|
|
|
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'
|
|
|
|
)
|
2024-05-14 09:27:05 +00:00
|
|
|
|
2024-05-22 06:19:40 +00:00
|
|
|
if (arrayBuffer) {
|
|
|
|
const hash = await getHash(arrayBuffer)
|
|
|
|
|
|
|
|
if (hash) {
|
|
|
|
fileHashes[fileName.replace(/^files\//, '')] = hash
|
2024-05-14 09:27:05 +00:00
|
|
|
}
|
2024-05-22 06:19:40 +00:00
|
|
|
} else {
|
|
|
|
fileHashes[fileName.replace(/^files\//, '')] = null
|
2024-05-14 09:27:05 +00:00
|
|
|
}
|
|
|
|
}
|
2024-05-22 06:19:40 +00:00
|
|
|
|
|
|
|
setCurrentFileHashes(fileHashes)
|
|
|
|
}
|
|
|
|
|
|
|
|
generateCurrentFileHashes()
|
|
|
|
}
|
|
|
|
}, [zip])
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (signers.length > 0) {
|
|
|
|
// check if all signers have signed then its fully signed
|
|
|
|
if (signers.every((signer) => signedBy.includes(signer))) {
|
2024-05-14 09:27:05 +00:00
|
|
|
setSignedStatus(SignedStatus.Fully_Signed)
|
2024-05-22 06:19:40 +00:00
|
|
|
} 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
|
|
|
|
}
|
|
|
|
}
|
2024-05-14 09:27:05 +00:00
|
|
|
}
|
2024-05-22 06:19:40 +00:00
|
|
|
} else {
|
|
|
|
// there's no signer just viewers. So its fully signed
|
|
|
|
setSignedStatus(SignedStatus.Fully_Signed)
|
2024-05-14 09:27:05 +00:00
|
|
|
}
|
2024-05-28 10:10:06 +00:00
|
|
|
|
|
|
|
// Determine and set the status of the user
|
|
|
|
if (submittedBy && usersPubkey && submittedBy === usersPubkey) {
|
|
|
|
// If the submission was made by the user, set the status to true
|
|
|
|
setIsSignerOrCreator(true)
|
|
|
|
} else if (usersPubkey) {
|
|
|
|
// Convert the user's public key from hex to npub format
|
|
|
|
const usersNpub = hexToNpub(usersPubkey)
|
|
|
|
if (signers.includes(usersNpub)) {
|
|
|
|
// If the user's npub is in the list of signers, set the status to true
|
|
|
|
setIsSignerOrCreator(true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [signers, signedBy, usersPubkey, submittedBy])
|
2024-05-14 09:27:05 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const fileUrl = searchParams.get('file')
|
|
|
|
const key = searchParams.get('key')
|
|
|
|
|
|
|
|
if (fileUrl && key) {
|
|
|
|
setIsLoading(true)
|
|
|
|
setLoadingSpinnerDesc('Fetching file from file server')
|
|
|
|
|
|
|
|
axios
|
|
|
|
.get(fileUrl, {
|
|
|
|
responseType: 'arraybuffer'
|
|
|
|
})
|
|
|
|
.then((res) => {
|
|
|
|
const fileName = fileUrl.split('/').pop()
|
|
|
|
const file = new File([res.data], fileName!)
|
|
|
|
|
2024-05-16 05:38:27 +00:00
|
|
|
decrypt(file, decodeURIComponent(key)).then((arrayBuffer) => {
|
2024-05-14 09:27:05 +00:00
|
|
|
if (arrayBuffer) handleDecryptedArrayBuffer(arrayBuffer)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
console.error(`error occurred in getting file from ${fileUrl}`, err)
|
|
|
|
toast.error(
|
|
|
|
err.message || `error occurred in getting file from ${fileUrl}`
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
setIsLoading(false)
|
|
|
|
})
|
2024-06-03 09:01:24 +00:00
|
|
|
} else if (file && encKey) {
|
|
|
|
decrypt(file, decodeURIComponent(encKey))
|
|
|
|
.then((arrayBuffer) => {
|
|
|
|
if (arrayBuffer) handleDecryptedArrayBuffer(arrayBuffer)
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
console.error(`error occurred in decryption`, err)
|
|
|
|
toast.error(err.message || `error occurred in decryption`)
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
setIsLoading(false)
|
|
|
|
})
|
2024-05-14 09:27:05 +00:00
|
|
|
} else {
|
|
|
|
setIsLoading(false)
|
|
|
|
setDisplayInput(true)
|
|
|
|
}
|
2024-06-03 09:01:24 +00:00
|
|
|
}, [searchParams, file, encKey])
|
2024-05-14 09:27:05 +00:00
|
|
|
|
|
|
|
const decrypt = async (file: File, key: string) => {
|
|
|
|
setLoadingSpinnerDesc('Decrypting file')
|
|
|
|
|
|
|
|
const encryptedArrayBuffer = await file.arrayBuffer()
|
|
|
|
|
|
|
|
const arrayBuffer = await decryptArrayBuffer(encryptedArrayBuffer, key)
|
|
|
|
.catch((err) => {
|
|
|
|
console.log('err in decryption:>> ', err)
|
|
|
|
toast.error(err.message || 'An error occurred in decrypting file.')
|
|
|
|
return null
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
setIsLoading(false)
|
|
|
|
})
|
|
|
|
|
|
|
|
return arrayBuffer
|
|
|
|
}
|
|
|
|
|
|
|
|
const handleDecryptedArrayBuffer = async (arrayBuffer: ArrayBuffer) => {
|
|
|
|
const decryptedZipFile = new File([arrayBuffer], 'decrypted.zip')
|
|
|
|
|
|
|
|
setLoadingSpinnerDesc('Parsing zip file')
|
|
|
|
|
|
|
|
const zip = await JSZip.loadAsync(decryptedZipFile).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
|
|
|
|
|
|
|
|
setZip(zip)
|
2024-05-28 10:10:06 +00:00
|
|
|
setDisplayInput(false)
|
2024-05-14 09:27:05 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2024-05-22 06:19:40 +00:00
|
|
|
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}`[])
|
|
|
|
|
2024-05-14 09:27:05 +00:00
|
|
|
setMeta(parsedMetaJson)
|
|
|
|
}
|
|
|
|
|
|
|
|
const handleDecrypt = async () => {
|
|
|
|
if (!selectedFile || !encryptionKey) return
|
|
|
|
|
|
|
|
setIsLoading(true)
|
2024-05-15 11:25:21 +00:00
|
|
|
const arrayBuffer = await decrypt(
|
|
|
|
selectedFile,
|
|
|
|
decodeURIComponent(encryptionKey)
|
|
|
|
)
|
2024-05-14 09:27:05 +00:00
|
|
|
|
|
|
|
if (!arrayBuffer) return
|
|
|
|
|
|
|
|
handleDecryptedArrayBuffer(arrayBuffer)
|
|
|
|
}
|
|
|
|
|
|
|
|
const handleSign = async () => {
|
|
|
|
if (!zip || !meta) return
|
|
|
|
|
|
|
|
setIsLoading(true)
|
|
|
|
setLoadingSpinnerDesc('parsing hashes.json file')
|
|
|
|
|
|
|
|
const hashesFileContent = await readContentOfZipEntry(
|
|
|
|
zip,
|
|
|
|
'hashes.json',
|
|
|
|
'string'
|
|
|
|
)
|
|
|
|
|
|
|
|
if (!hashesFileContent) {
|
|
|
|
setIsLoading(false)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let hashes = await parseJson(hashesFileContent).catch((err) => {
|
|
|
|
console.log('err in parsing the content of hashes.json :>> ', err)
|
|
|
|
toast.error(
|
|
|
|
err.message || 'error occurred in parsing the content of hashes.json'
|
|
|
|
)
|
|
|
|
setIsLoading(false)
|
|
|
|
return null
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!hashes) return
|
|
|
|
|
|
|
|
setLoadingSpinnerDesc('Generating hashes for files')
|
|
|
|
|
|
|
|
setLoadingSpinnerDesc('Signing nostr event')
|
2024-05-24 06:39:28 +00:00
|
|
|
|
|
|
|
const prevSig = getPrevSignersSig(hexToNpub(usersPubkey!))
|
|
|
|
if (!prevSig) return
|
|
|
|
|
2024-05-14 09:27:05 +00:00
|
|
|
const signedEvent = await signEventForMetaFile(
|
2024-05-22 06:19:40 +00:00
|
|
|
JSON.stringify({
|
2024-05-24 06:39:28 +00:00
|
|
|
prevSig
|
2024-05-22 06:19:40 +00:00
|
|
|
}),
|
2024-05-14 09:27:05 +00:00
|
|
|
nostrController,
|
|
|
|
setIsLoading
|
|
|
|
)
|
|
|
|
|
|
|
|
if (!signedEvent) return
|
|
|
|
|
|
|
|
const metaCopy = _.cloneDeep(meta)
|
|
|
|
|
2024-05-22 06:19:40 +00:00
|
|
|
metaCopy.docSignatures = {
|
|
|
|
...metaCopy.docSignatures,
|
2024-05-17 08:34:56 +00:00
|
|
|
[hexToNpub(signedEvent.pubkey)]: JSON.stringify(signedEvent, null, 2)
|
2024-05-14 09:27:05 +00:00
|
|
|
}
|
|
|
|
|
2024-05-14 11:35:21 +00:00
|
|
|
const stringifiedMeta = JSON.stringify(metaCopy, null, 2)
|
2024-05-14 09:27:05 +00:00
|
|
|
zip.file('meta.json', stringifiedMeta)
|
|
|
|
|
|
|
|
const metaHash = await getHash(stringifiedMeta)
|
|
|
|
if (!metaHash) return
|
|
|
|
|
|
|
|
hashes = {
|
|
|
|
...hashes,
|
|
|
|
[usersPubkey!]: metaHash
|
|
|
|
}
|
|
|
|
|
|
|
|
zip.file('hashes.json', JSON.stringify(hashes, null, 2))
|
|
|
|
|
|
|
|
const arrayBuffer = await zip
|
|
|
|
.generateAsync({
|
|
|
|
type: 'arraybuffer',
|
|
|
|
compression: 'DEFLATE',
|
|
|
|
compressionOptions: {
|
|
|
|
level: 6
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
console.log('err in zip:>> ', err)
|
|
|
|
setIsLoading(false)
|
|
|
|
toast.error(err.message || 'Error occurred in generating zip file')
|
|
|
|
return null
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!arrayBuffer) return
|
|
|
|
|
2024-05-17 08:34:56 +00:00
|
|
|
const key = await generateEncryptionKey()
|
2024-05-14 09:27:05 +00:00
|
|
|
|
|
|
|
setLoadingSpinnerDesc('Encrypting zip file')
|
2024-05-17 08:34:56 +00:00
|
|
|
const encryptedArrayBuffer = await encryptArrayBuffer(arrayBuffer, key)
|
2024-05-14 09:27:05 +00:00
|
|
|
|
|
|
|
const blob = new Blob([encryptedArrayBuffer])
|
|
|
|
|
2024-05-31 07:14:33 +00:00
|
|
|
if (await isOnline()) {
|
2024-05-28 10:10:06 +00:00
|
|
|
setLoadingSpinnerDesc('Uploading zip file to file storage.')
|
|
|
|
const fileUrl = await uploadToFileStorage(blob, nostrController)
|
|
|
|
.then((url) => {
|
|
|
|
toast.success('zip file uploaded to file storage')
|
|
|
|
return url
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
console.log('err in upload:>> ', err)
|
|
|
|
setIsLoading(false)
|
|
|
|
toast.error(err.message || 'Error occurred in uploading zip file')
|
|
|
|
return null
|
|
|
|
})
|
2024-05-14 09:27:05 +00:00
|
|
|
|
2024-05-28 10:10:06 +00:00
|
|
|
if (!fileUrl) return
|
2024-05-14 09:27:05 +00:00
|
|
|
|
2024-05-28 10:10:06 +00:00
|
|
|
// check if the current user is the last signer
|
|
|
|
const usersNpub = hexToNpub(usersPubkey!)
|
|
|
|
const lastSignerIndex = signers.length - 1
|
|
|
|
const signerIndex = signers.indexOf(usersNpub)
|
|
|
|
const isLastSigner = signerIndex === lastSignerIndex
|
2024-05-14 09:27:05 +00:00
|
|
|
|
2024-05-28 10:10:06 +00:00
|
|
|
// if current user is the last signer, then send DMs to all signers and viewers
|
|
|
|
if (isLastSigner) {
|
|
|
|
const userSet = new Set<`npub1${string}`>()
|
2024-05-14 09:27:05 +00:00
|
|
|
|
2024-05-28 10:10:06 +00:00
|
|
|
if (submittedBy) {
|
|
|
|
userSet.add(hexToNpub(submittedBy))
|
|
|
|
}
|
2024-05-14 09:27:05 +00:00
|
|
|
|
2024-05-28 10:10:06 +00:00
|
|
|
signers.forEach((signer) => {
|
|
|
|
userSet.add(signer)
|
|
|
|
})
|
2024-05-14 09:27:05 +00:00
|
|
|
|
2024-05-28 10:10:06 +00:00
|
|
|
viewers.forEach((viewer) => {
|
|
|
|
userSet.add(viewer)
|
|
|
|
})
|
2024-05-14 09:27:05 +00:00
|
|
|
|
2024-05-28 10:10:06 +00:00
|
|
|
const users = Array.from(userSet)
|
|
|
|
|
|
|
|
for (const user of users) {
|
|
|
|
// todo: execute in parallel
|
|
|
|
await sendDM(
|
|
|
|
fileUrl,
|
|
|
|
key,
|
|
|
|
npubToHex(user)!,
|
|
|
|
nostrController,
|
|
|
|
false,
|
|
|
|
setAuthUrl
|
|
|
|
)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const nextSigner = signers[signerIndex + 1]
|
2024-05-14 09:27:05 +00:00
|
|
|
await sendDM(
|
|
|
|
fileUrl,
|
2024-05-17 08:34:56 +00:00
|
|
|
key,
|
2024-05-28 10:10:06 +00:00
|
|
|
npubToHex(nextSigner)!,
|
2024-05-14 09:27:05 +00:00
|
|
|
nostrController,
|
2024-05-28 10:10:06 +00:00
|
|
|
true,
|
2024-05-14 09:27:05 +00:00
|
|
|
setAuthUrl
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-05-28 10:10:06 +00:00
|
|
|
setIsLoading(false)
|
2024-05-17 08:34:56 +00:00
|
|
|
|
2024-05-28 10:10:06 +00:00
|
|
|
// update search params with updated file url and encryption key
|
|
|
|
setSearchParams({
|
|
|
|
file: fileUrl,
|
|
|
|
key: key
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
handleDecryptedArrayBuffer(arrayBuffer).finally(() => setIsLoading(false))
|
|
|
|
}
|
2024-05-14 09:27:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const handleExport = async () => {
|
|
|
|
if (!meta || !zip || !usersPubkey) return
|
|
|
|
|
2024-05-17 08:34:56 +00:00
|
|
|
const usersNpub = hexToNpub(usersPubkey)
|
2024-05-14 09:27:05 +00:00
|
|
|
if (
|
2024-05-22 06:19:40 +00:00
|
|
|
!signers.includes(usersNpub) &&
|
|
|
|
!viewers.includes(usersNpub) &&
|
|
|
|
submittedBy !== usersNpub
|
2024-05-14 09:27:05 +00:00
|
|
|
)
|
|
|
|
return
|
|
|
|
|
|
|
|
setIsLoading(true)
|
|
|
|
setLoadingSpinnerDesc('Signing nostr event')
|
2024-05-24 06:39:28 +00:00
|
|
|
|
|
|
|
const prevSig = await getLastSignersSig()
|
|
|
|
if (!prevSig) return
|
|
|
|
|
2024-05-22 06:19:40 +00:00
|
|
|
const signedEvent = await signEventForMetaFile(
|
|
|
|
JSON.stringify({
|
2024-05-24 06:39:28 +00:00
|
|
|
prevSig
|
2024-05-22 06:19:40 +00:00
|
|
|
}),
|
|
|
|
nostrController,
|
|
|
|
setIsLoading
|
|
|
|
)
|
2024-05-14 09:27:05 +00:00
|
|
|
|
|
|
|
if (!signedEvent) return
|
|
|
|
|
|
|
|
const exportSignature = JSON.stringify(signedEvent, null, 2)
|
|
|
|
|
|
|
|
const stringifiedMeta = JSON.stringify(
|
|
|
|
{
|
|
|
|
...meta,
|
|
|
|
exportSignature
|
|
|
|
},
|
|
|
|
null,
|
|
|
|
2
|
|
|
|
)
|
|
|
|
zip.file('meta.json', stringifiedMeta)
|
|
|
|
|
|
|
|
const arrayBuffer = await zip
|
|
|
|
.generateAsync({
|
|
|
|
type: 'arraybuffer',
|
|
|
|
compression: 'DEFLATE',
|
|
|
|
compressionOptions: {
|
|
|
|
level: 6
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
console.log('err in zip:>> ', err)
|
|
|
|
setIsLoading(false)
|
|
|
|
toast.error(err.message || 'Error occurred in generating zip file')
|
|
|
|
return null
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!arrayBuffer) return
|
|
|
|
|
|
|
|
const blob = new Blob([arrayBuffer])
|
|
|
|
saveAs(blob, 'exported.zip')
|
|
|
|
|
|
|
|
setIsLoading(false)
|
2024-05-16 05:43:37 +00:00
|
|
|
|
|
|
|
navigate(appPrivateRoutes.verify)
|
2024-05-14 09:27:05 +00:00
|
|
|
}
|
|
|
|
|
2024-05-28 10:10:06 +00:00
|
|
|
const handleExportSigit = async () => {
|
|
|
|
if (!zip) return
|
|
|
|
|
|
|
|
const arrayBuffer = await zip
|
|
|
|
.generateAsync({
|
|
|
|
type: 'arraybuffer',
|
|
|
|
compression: 'DEFLATE',
|
|
|
|
compressionOptions: {
|
|
|
|
level: 6
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
console.log('err in zip:>> ', err)
|
|
|
|
setIsLoading(false)
|
|
|
|
toast.error(err.message || 'Error occurred in generating zip file')
|
|
|
|
return null
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!arrayBuffer) return
|
|
|
|
|
|
|
|
const key = await generateEncryptionKey()
|
|
|
|
|
|
|
|
setLoadingSpinnerDesc('Encrypting zip file')
|
|
|
|
const encryptedArrayBuffer = await encryptArrayBuffer(arrayBuffer, key)
|
|
|
|
const blob = new Blob([encryptedArrayBuffer])
|
|
|
|
saveAs(blob, 'exported.sigit')
|
|
|
|
|
|
|
|
setTextToCopy(key)
|
|
|
|
setOpenCopyModel(true)
|
|
|
|
}
|
|
|
|
|
2024-05-24 06:39:28 +00:00
|
|
|
/**
|
|
|
|
* This function accepts an npub of a signer and return the signature of its previous signer.
|
|
|
|
* This prevSig will be used in the content of the provided signer's signedEvent
|
|
|
|
*/
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This function returns the signature of last signer
|
|
|
|
* It will be used in the content of export signature's signedEvent
|
|
|
|
*/
|
|
|
|
const getLastSignersSig = () => {
|
|
|
|
if (!meta) return null
|
|
|
|
|
|
|
|
// if there're no signers then use creator's signature
|
|
|
|
if (signers.length === 0) {
|
|
|
|
try {
|
|
|
|
const createSignatureEvent: Event = JSON.parse(meta.createSignature)
|
|
|
|
return createSignatureEvent.sig
|
|
|
|
} catch (error) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// get last signer
|
|
|
|
const lastSigner = signers[signers.length - 1]
|
|
|
|
|
|
|
|
// get the signature of last signer
|
|
|
|
try {
|
|
|
|
const lastSignatureEvent: Event = JSON.parse(
|
|
|
|
meta.docSignatures[lastSigner]
|
|
|
|
)
|
|
|
|
return lastSignatureEvent.sig
|
|
|
|
} catch (error) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-14 09:27:05 +00:00
|
|
|
if (authUrl) {
|
|
|
|
return (
|
|
|
|
<iframe
|
2024-05-16 06:25:30 +00:00
|
|
|
title="Nsecbunker auth"
|
2024-05-14 09:27:05 +00:00
|
|
|
src={authUrl}
|
2024-05-16 06:25:30 +00:00
|
|
|
width="100%"
|
|
|
|
height="500px"
|
2024-05-14 09:27:05 +00:00
|
|
|
/>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
|
|
|
<Box className={styles.container}>
|
|
|
|
{displayInput && (
|
|
|
|
<>
|
2024-05-16 06:25:30 +00:00
|
|
|
<Typography component="label" variant="h6">
|
2024-05-14 09:27:05 +00:00
|
|
|
Select sigit file
|
|
|
|
</Typography>
|
|
|
|
|
2024-05-15 06:19:28 +00:00
|
|
|
<Box className={styles.inputBlock}>
|
2024-05-14 09:27:05 +00:00
|
|
|
<MuiFileInput
|
2024-05-16 06:25:30 +00:00
|
|
|
placeholder="Select file"
|
2024-05-28 10:10:06 +00:00
|
|
|
inputProps={{ accept: '.sigit' }}
|
2024-05-14 09:27:05 +00:00
|
|
|
value={selectedFile}
|
|
|
|
onChange={(value) => setSelectedFile(value)}
|
|
|
|
/>
|
|
|
|
|
|
|
|
{selectedFile && (
|
|
|
|
<TextField
|
2024-05-16 06:25:30 +00:00
|
|
|
label="Encryption Key"
|
|
|
|
variant="outlined"
|
2024-05-14 09:27:05 +00:00
|
|
|
value={encryptionKey}
|
|
|
|
onChange={(e) => setEncryptionKey(e.target.value)}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
{selectedFile && encryptionKey && (
|
2024-05-15 06:19:28 +00:00
|
|
|
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
|
2024-05-16 06:25:30 +00:00
|
|
|
<Button onClick={handleDecrypt} variant="contained">
|
2024-05-14 09:27:05 +00:00
|
|
|
Decrypt
|
|
|
|
</Button>
|
|
|
|
</Box>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
|
2024-05-24 06:39:28 +00:00
|
|
|
{submittedBy && zip && meta && (
|
2024-05-14 09:27:05 +00:00
|
|
|
<>
|
2024-05-22 06:19:40 +00:00
|
|
|
<DisplayMeta
|
2024-05-24 06:39:28 +00:00
|
|
|
meta={meta}
|
2024-05-22 06:19:40 +00:00
|
|
|
zip={zip}
|
|
|
|
submittedBy={submittedBy}
|
|
|
|
signers={signers}
|
|
|
|
viewers={viewers}
|
|
|
|
creatorFileHashes={creatorFileHashes}
|
|
|
|
currentFileHashes={currentFileHashes}
|
|
|
|
signedBy={signedBy}
|
|
|
|
nextSigner={nextSinger}
|
2024-05-24 06:39:28 +00:00
|
|
|
getPrevSignersSig={getPrevSignersSig}
|
2024-05-22 06:19:40 +00:00
|
|
|
/>
|
2024-05-28 10:10:06 +00:00
|
|
|
|
2024-05-22 06:19:40 +00:00
|
|
|
{signedStatus === SignedStatus.Fully_Signed && (
|
|
|
|
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
|
|
|
<Button onClick={handleExport} variant="contained">
|
|
|
|
Export
|
|
|
|
</Button>
|
|
|
|
</Box>
|
|
|
|
)}
|
2024-05-14 09:27:05 +00:00
|
|
|
|
2024-05-22 06:19:40 +00:00
|
|
|
{signedStatus === SignedStatus.User_Is_Next_Signer && (
|
|
|
|
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
|
|
|
<Button onClick={handleSign} variant="contained">
|
|
|
|
Sign
|
|
|
|
</Button>
|
|
|
|
</Box>
|
|
|
|
)}
|
2024-05-28 10:10:06 +00:00
|
|
|
|
|
|
|
{isSignerOrCreator &&
|
|
|
|
signedStatus === SignedStatus.User_Is_Not_Next_Signer && (
|
|
|
|
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
|
|
|
<Button onClick={handleExportSigit} variant="contained">
|
|
|
|
Export Sigit
|
|
|
|
</Button>
|
|
|
|
</Box>
|
|
|
|
)}
|
2024-05-14 09:27:05 +00:00
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</Box>
|
2024-05-28 10:10:06 +00:00
|
|
|
<CopyModal
|
|
|
|
open={openCopyModal}
|
|
|
|
handleClose={() => setOpenCopyModel(false)}
|
|
|
|
title="Decryption key for Sigit file"
|
|
|
|
textToCopy={textToCopy}
|
|
|
|
/>
|
2024-05-14 09:27:05 +00:00
|
|
|
</>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
type DisplayMetaProps = {
|
2024-05-24 06:39:28 +00:00
|
|
|
meta: Meta
|
2024-05-22 06:19:40 +00:00
|
|
|
zip: JSZip
|
|
|
|
submittedBy: string
|
|
|
|
signers: `npub1${string}`[]
|
|
|
|
viewers: `npub1${string}`[]
|
|
|
|
creatorFileHashes: { [key: string]: string }
|
|
|
|
currentFileHashes: { [key: string]: string | null }
|
|
|
|
signedBy: `npub1${string}`[]
|
2024-05-14 09:27:05 +00:00
|
|
|
nextSigner?: string
|
2024-05-24 06:39:28 +00:00
|
|
|
getPrevSignersSig: (usersNpub: string) => string | null
|
2024-05-14 09:27:05 +00:00
|
|
|
}
|
|
|
|
|
2024-05-22 06:19:40 +00:00
|
|
|
const DisplayMeta = ({
|
2024-05-24 06:39:28 +00:00
|
|
|
meta,
|
2024-05-22 06:19:40 +00:00
|
|
|
zip,
|
|
|
|
submittedBy,
|
|
|
|
signers,
|
|
|
|
viewers,
|
|
|
|
creatorFileHashes,
|
|
|
|
currentFileHashes,
|
|
|
|
signedBy,
|
2024-05-24 06:39:28 +00:00
|
|
|
nextSigner,
|
|
|
|
getPrevSignersSig
|
2024-05-22 06:19:40 +00:00
|
|
|
}: DisplayMetaProps) => {
|
2024-05-14 09:27:05 +00:00
|
|
|
const theme = useTheme()
|
|
|
|
|
|
|
|
const textColor = theme.palette.getContrastText(
|
|
|
|
theme.palette.background.paper
|
|
|
|
)
|
|
|
|
|
|
|
|
const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
|
|
|
|
{}
|
|
|
|
)
|
|
|
|
const [users, setUsers] = useState<User[]>([])
|
|
|
|
|
|
|
|
useEffect(() => {
|
2024-05-22 06:19:40 +00:00
|
|
|
signers.forEach((signer) => {
|
2024-05-17 08:34:56 +00:00
|
|
|
const hexKey = npubToHex(signer)
|
2024-05-14 09:27:05 +00:00
|
|
|
setUsers((prev) => {
|
2024-05-17 08:34:56 +00:00
|
|
|
if (prev.findIndex((user) => user.pubkey === hexKey) !== -1) return prev
|
2024-05-14 09:27:05 +00:00
|
|
|
|
|
|
|
return [
|
|
|
|
...prev,
|
|
|
|
{
|
2024-05-17 08:34:56 +00:00
|
|
|
pubkey: hexKey!,
|
2024-05-14 10:37:55 +00:00
|
|
|
role: UserRole.signer
|
2024-05-14 09:27:05 +00:00
|
|
|
}
|
|
|
|
]
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2024-05-22 06:19:40 +00:00
|
|
|
viewers.forEach((viewer) => {
|
2024-05-17 08:34:56 +00:00
|
|
|
const hexKey = npubToHex(viewer)
|
2024-05-14 09:27:05 +00:00
|
|
|
setUsers((prev) => {
|
2024-05-17 08:34:56 +00:00
|
|
|
if (prev.findIndex((user) => user.pubkey === hexKey) !== -1) return prev
|
2024-05-14 09:27:05 +00:00
|
|
|
|
|
|
|
return [
|
|
|
|
...prev,
|
|
|
|
{
|
2024-05-17 08:34:56 +00:00
|
|
|
pubkey: hexKey!,
|
2024-05-14 10:37:55 +00:00
|
|
|
role: UserRole.viewer
|
2024-05-14 09:27:05 +00:00
|
|
|
}
|
|
|
|
]
|
|
|
|
})
|
|
|
|
})
|
2024-05-22 06:19:40 +00:00
|
|
|
}, [signers, viewers])
|
2024-05-14 09:27:05 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const metadataController = new MetadataController()
|
|
|
|
|
2024-05-17 08:34:56 +00:00
|
|
|
const hexKeys: string[] = [
|
2024-05-22 06:19:40 +00:00
|
|
|
npubToHex(submittedBy)!,
|
2024-05-17 08:34:56 +00:00
|
|
|
...users.map((user) => user.pubkey)
|
|
|
|
]
|
2024-05-14 09:27:05 +00:00
|
|
|
|
2024-05-17 08:34:56 +00:00
|
|
|
hexKeys.forEach((key) => {
|
|
|
|
if (!(key in metadata)) {
|
2024-05-30 17:28:40 +00:00
|
|
|
const handleMetadataEvent = (event: Event) => {
|
|
|
|
const metadataContent =
|
|
|
|
metadataController.extractProfileMetadataContent(event)
|
|
|
|
|
|
|
|
if (metadataContent)
|
|
|
|
setMetadata((prev) => ({
|
|
|
|
...prev,
|
|
|
|
[key]: metadataContent
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
|
|
|
metadataController.on(key, (kind: number, event: Event) => {
|
|
|
|
if (kind === kinds.Metadata) {
|
|
|
|
handleMetadataEvent(event)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2024-05-14 09:27:05 +00:00
|
|
|
metadataController
|
2024-05-17 08:34:56 +00:00
|
|
|
.findMetadata(key)
|
2024-05-14 09:27:05 +00:00
|
|
|
.then((metadataEvent) => {
|
2024-05-30 17:28:40 +00:00
|
|
|
if (metadataEvent) handleMetadataEvent(metadataEvent)
|
2024-05-14 09:27:05 +00:00
|
|
|
})
|
|
|
|
.catch((err) => {
|
2024-05-17 08:34:56 +00:00
|
|
|
console.error(`error occurred in finding metadata for: ${key}`, err)
|
2024-05-14 09:27:05 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
2024-05-22 06:19:40 +00:00
|
|
|
}, [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)
|
|
|
|
}
|
2024-05-14 09:27:05 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<List
|
|
|
|
sx={{
|
|
|
|
bgcolor: 'background.paper',
|
|
|
|
marginTop: 2
|
|
|
|
}}
|
|
|
|
subheader={
|
2024-05-22 06:19:40 +00:00
|
|
|
<ListSubheader className={styles.subHeader}>Meta Info</ListSubheader>
|
2024-05-14 09:27:05 +00:00
|
|
|
}
|
|
|
|
>
|
|
|
|
<ListItem
|
|
|
|
sx={{
|
|
|
|
marginTop: 1,
|
|
|
|
gap: '15px'
|
|
|
|
}}
|
|
|
|
>
|
2024-05-16 06:25:30 +00:00
|
|
|
<Typography variant="h6" sx={{ color: textColor }}>
|
2024-05-14 09:27:05 +00:00
|
|
|
Submitted By
|
|
|
|
</Typography>
|
2024-05-17 08:34:56 +00:00
|
|
|
{(function () {
|
2024-05-22 06:19:40 +00:00
|
|
|
const profile = metadata[submittedBy]
|
2024-05-17 08:34:56 +00:00
|
|
|
return (
|
|
|
|
<UserComponent
|
2024-05-22 06:19:40 +00:00
|
|
|
pubkey={submittedBy}
|
2024-05-17 08:34:56 +00:00
|
|
|
name={
|
2024-05-31 04:09:48 +00:00
|
|
|
profile?.display_name ||
|
|
|
|
profile?.name ||
|
|
|
|
shorten(hexToNpub(submittedBy))
|
2024-05-17 08:34:56 +00:00
|
|
|
}
|
2024-05-22 06:19:40 +00:00
|
|
|
image={profile?.picture}
|
2024-05-17 08:34:56 +00:00
|
|
|
/>
|
|
|
|
)
|
|
|
|
})()}
|
2024-05-14 09:27:05 +00:00
|
|
|
</ListItem>
|
|
|
|
<ListItem
|
|
|
|
sx={{
|
|
|
|
marginTop: 1,
|
|
|
|
flexDirection: 'column',
|
|
|
|
alignItems: 'flex-start'
|
|
|
|
}}
|
|
|
|
>
|
2024-05-16 06:25:30 +00:00
|
|
|
<Typography variant="h6" sx={{ color: textColor }}>
|
2024-05-14 09:27:05 +00:00
|
|
|
Files
|
|
|
|
</Typography>
|
2024-05-22 06:19:40 +00:00
|
|
|
<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>
|
2024-05-24 06:39:28 +00:00
|
|
|
{isValidHash && (
|
|
|
|
<Tooltip title="File integrity check passed" arrow>
|
|
|
|
<CheckCircle sx={{ color: theme.palette.success.light }} />
|
|
|
|
</Tooltip>
|
|
|
|
)}
|
|
|
|
{!isValidHash && (
|
|
|
|
<Tooltip title="File integrity check failed" arrow>
|
|
|
|
<Cancel sx={{ color: theme.palette.error.main }} />
|
|
|
|
</Tooltip>
|
|
|
|
)}
|
2024-05-22 06:19:40 +00:00
|
|
|
</Box>
|
|
|
|
)
|
|
|
|
})}
|
|
|
|
</Box>
|
2024-05-14 09:27:05 +00:00
|
|
|
</ListItem>
|
|
|
|
<ListItem sx={{ marginTop: 1 }}>
|
|
|
|
<Table>
|
|
|
|
<TableHead>
|
|
|
|
<TableRow>
|
|
|
|
<TableCell className={styles.tableCell}>User</TableCell>
|
|
|
|
<TableCell className={styles.tableCell}>Role</TableCell>
|
|
|
|
<TableCell>Signed Status</TableCell>
|
|
|
|
</TableRow>
|
|
|
|
</TableHead>
|
|
|
|
<TableBody>
|
2024-05-24 06:39:28 +00:00
|
|
|
{users.map((user) => (
|
|
|
|
<DisplayUser
|
|
|
|
key={user.pubkey}
|
|
|
|
meta={meta}
|
|
|
|
user={user}
|
|
|
|
metadata={metadata}
|
|
|
|
signedBy={signedBy}
|
|
|
|
nextSigner={nextSigner}
|
|
|
|
getPrevSignersSig={getPrevSignersSig}
|
|
|
|
/>
|
|
|
|
))}
|
2024-05-14 09:27:05 +00:00
|
|
|
</TableBody>
|
|
|
|
</Table>
|
|
|
|
</ListItem>
|
|
|
|
</List>
|
|
|
|
)
|
|
|
|
}
|
2024-05-24 06:39:28 +00:00
|
|
|
|
|
|
|
enum PrevSignatureValidationEnum {
|
|
|
|
Pending,
|
|
|
|
Valid,
|
|
|
|
Invalid
|
|
|
|
}
|
|
|
|
|
|
|
|
enum UserStatus {
|
|
|
|
Viewer = 'Viewer',
|
|
|
|
Awaiting = 'Awaiting Signature',
|
|
|
|
Signed = 'Signed',
|
|
|
|
Pending = 'Pending'
|
|
|
|
}
|
|
|
|
|
|
|
|
type DisplayUserProps = {
|
|
|
|
meta: Meta
|
|
|
|
user: User
|
|
|
|
metadata: { [key: string]: ProfileMetadata }
|
|
|
|
signedBy: `npub1${string}`[]
|
|
|
|
nextSigner?: string
|
|
|
|
getPrevSignersSig: (usersNpub: string) => string | null
|
|
|
|
}
|
|
|
|
|
|
|
|
const DisplayUser = ({
|
|
|
|
meta,
|
|
|
|
user,
|
|
|
|
metadata,
|
|
|
|
signedBy,
|
|
|
|
nextSigner,
|
|
|
|
getPrevSignersSig
|
|
|
|
}: DisplayUserProps) => {
|
|
|
|
const theme = useTheme()
|
|
|
|
|
|
|
|
const userMeta = metadata[user.pubkey]
|
|
|
|
const [userStatus, setUserStatus] = useState<UserStatus>(UserStatus.Pending)
|
|
|
|
const [prevSignatureStatus, setPreviousSignatureStatus] =
|
|
|
|
useState<PrevSignatureValidationEnum>(PrevSignatureValidationEnum.Pending)
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (user.role === UserRole.viewer) {
|
|
|
|
setUserStatus(UserStatus.Viewer)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// check if user has signed the document
|
|
|
|
const usersNpub = hexToNpub(user.pubkey)
|
|
|
|
if (signedBy.includes(usersNpub)) {
|
|
|
|
setUserStatus(UserStatus.Signed)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// check if user is the next signer
|
|
|
|
if (user.pubkey === nextSigner) {
|
|
|
|
setUserStatus(UserStatus.Awaiting)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}, [user, nextSigner, signedBy])
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const validatePrevSignature = async () => {
|
|
|
|
const handleNullCase = () => {
|
|
|
|
setPreviousSignatureStatus(PrevSignatureValidationEnum.Invalid)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// get previous signers sig from the content of current signers signed event
|
|
|
|
const npub = hexToNpub(user.pubkey)
|
|
|
|
const signedEvent = await parseJson<Event>(
|
|
|
|
meta.docSignatures[npub]
|
|
|
|
).catch((err) => {
|
|
|
|
console.log(`err in parsing the singed event for ${npub}:>> `, err)
|
|
|
|
toast.error(
|
|
|
|
err.message ||
|
|
|
|
'error occurred in parsing the signed event signature event'
|
|
|
|
)
|
|
|
|
return null
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!signedEvent) return handleNullCase()
|
|
|
|
|
|
|
|
// now that we have signed event of current signer, we'll extract prevSig from its content
|
|
|
|
const parsedContent = await parseJson<SignedEventContent>(
|
|
|
|
signedEvent.content
|
|
|
|
).catch((err) => {
|
|
|
|
console.log(
|
|
|
|
`an error occurred in parsing the content of signedEvent of ${npub}`,
|
|
|
|
err
|
|
|
|
)
|
|
|
|
toast.error(
|
|
|
|
err.message ||
|
|
|
|
`an error occurred in parsing the content of signedEvent of ${npub}`
|
|
|
|
)
|
|
|
|
return null
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!parsedContent) return handleNullCase()
|
|
|
|
|
|
|
|
const prevSignersSignature = getPrevSignersSig(npub)
|
|
|
|
|
|
|
|
if (!prevSignersSignature) return handleNullCase()
|
|
|
|
|
|
|
|
setPreviousSignatureStatus(
|
|
|
|
parsedContent.prevSig === prevSignersSignature
|
|
|
|
? PrevSignatureValidationEnum.Valid
|
|
|
|
: PrevSignatureValidationEnum.Invalid
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (userStatus === UserStatus.Signed) {
|
|
|
|
validatePrevSignature()
|
|
|
|
}
|
|
|
|
}, [userStatus, meta.docSignatures, user.pubkey, getPrevSignersSig])
|
|
|
|
|
|
|
|
return (
|
|
|
|
<TableRow>
|
|
|
|
<TableCell className={styles.tableCell}>
|
|
|
|
<UserComponent
|
|
|
|
pubkey={user.pubkey}
|
|
|
|
name={
|
|
|
|
userMeta?.display_name ||
|
|
|
|
userMeta?.name ||
|
|
|
|
shorten(hexToNpub(user.pubkey))
|
|
|
|
}
|
|
|
|
image={userMeta?.picture}
|
|
|
|
/>
|
|
|
|
</TableCell>
|
|
|
|
<TableCell className={styles.tableCell}>{user.role}</TableCell>
|
|
|
|
<TableCell>
|
|
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
|
|
|
<Typography component="label">{userStatus}</Typography>
|
|
|
|
{userStatus === UserStatus.Signed && (
|
|
|
|
<>
|
|
|
|
{prevSignatureStatus === PrevSignatureValidationEnum.Valid && (
|
|
|
|
<Tooltip title="Contains valid signature of prev signer" arrow>
|
|
|
|
<CheckCircle sx={{ color: theme.palette.success.light }} />
|
|
|
|
</Tooltip>
|
|
|
|
)}
|
|
|
|
{prevSignatureStatus === PrevSignatureValidationEnum.Invalid && (
|
|
|
|
<Tooltip
|
|
|
|
title="Contains invalid signature of prev signer"
|
|
|
|
arrow
|
|
|
|
>
|
|
|
|
<Cancel sx={{ color: theme.palette.error.main }} />
|
|
|
|
</Tooltip>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
{userStatus === UserStatus.Awaiting && (
|
|
|
|
<Tooltip title="Waiting for user's sign" arrow>
|
|
|
|
<HourglassTop />
|
|
|
|
</Tooltip>
|
|
|
|
)}
|
|
|
|
</Box>
|
|
|
|
</TableCell>
|
|
|
|
</TableRow>
|
|
|
|
)
|
|
|
|
}
|