PDF Markings #114
@ -29,4 +29,6 @@ jobs:
|
|||||||
- name: Release Build
|
- name: Release Build
|
||||||
run: |
|
run: |
|
||||||
npm -g install cloudron-surfer
|
npm -g install cloudron-surfer
|
||||||
surfer put --token ${{ secrets.CLOUDRON_SURFER_TOKEN }} --server sigit.io dist/* /
|
surfer config --token ${{ secrets.CLOUDRON_SURFER_TOKEN }} --server sigit.io
|
||||||
|
surfer put dist/* / --all -d
|
||||||
|
surfer put dist/.well-known / --all
|
||||||
|
@ -29,4 +29,6 @@ jobs:
|
|||||||
- name: Release Build
|
- name: Release Build
|
||||||
run: |
|
run: |
|
||||||
npm -g install cloudron-surfer
|
npm -g install cloudron-surfer
|
||||||
surfer put --token ${{ secrets.STAGING_CLOUDRON_SURFER_TOKEN }} --server staging.sigit.io dist/* /
|
surfer config --token ${{ secrets.STAGING_CLOUDRON_SURFER_TOKEN }} --server staging.sigit.io
|
||||||
|
surfer put dist/* / --all -d
|
||||||
|
surfer put dist/.well-known / --all
|
||||||
|
15
public/.well-known/nostr.json
Normal file
15
public/.well-known/nostr.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"names": {
|
||||||
|
"_": "6bf1024c6336093b632db2da21b65a44c7d82830454fb4d75634ba281e161c90"
|
||||||
|
},
|
||||||
|
"relays": {
|
||||||
|
"6bf1024c6336093b632db2da21b65a44c7d82830454fb4d75634ba281e161c90": [
|
||||||
|
"wss://brb.io",
|
||||||
|
"wss://nostr.v0l.io",
|
||||||
|
"wss://nostr.coinos.io",
|
||||||
|
"wss://rsslay.nostr.net",
|
||||||
|
"wss://relay.current.fyi",
|
||||||
|
"wss://nos.io"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -378,6 +378,61 @@ export class NostrController extends EventEmitter {
|
|||||||
throw new Error('Login method is undefined')
|
throw new Error('Login method is undefined')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts a given content based on the current login method.
|
||||||
|
*
|
||||||
|
* @param sender - The sender's public key.
|
||||||
|
* @param content - The encrypted content to decrypt.
|
||||||
|
* @returns A promise that resolves to the decrypted content.
|
||||||
|
*/
|
||||||
|
nip04Decrypt = async (sender: string, content: string) => {
|
||||||
|
const loginMethod = (store.getState().auth as AuthState).loginMethod
|
||||||
|
|
||||||
|
if (loginMethod === LoginMethods.extension) {
|
||||||
|
const nostr = this.getNostrObject()
|
||||||
|
|
||||||
|
if (!nostr.nip04) {
|
||||||
|
throw new Error(
|
||||||
|
`Your nostr extension does not support nip04 encryption & decryption`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const decrypted = await nostr.nip04.decrypt(sender, content)
|
||||||
|
return decrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginMethod === LoginMethods.privateKey) {
|
||||||
|
const keys = (store.getState().auth as AuthState).keyPair
|
||||||
|
|
||||||
|
if (!keys) {
|
||||||
|
throw new Error(
|
||||||
|
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { private: nsec } = keys
|
||||||
|
const privateKey = nip19.decode(nsec).data as Uint8Array
|
||||||
|
|
||||||
|
const decrypted = await nip04.decrypt(privateKey, sender, content)
|
||||||
|
return decrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginMethod === LoginMethods.nsecBunker) {
|
||||||
|
const user = new NDKUser({ pubkey: sender })
|
||||||
|
|
||||||
|
this.remoteSigner?.on('authUrl', (authUrl) => {
|
||||||
|
this.emit('nsecbunker-auth', authUrl)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!this.remoteSigner) throw new Error('Remote signer is undefined.')
|
||||||
|
const decrypted = await this.remoteSigner.decrypt(user, content)
|
||||||
|
|
||||||
|
return decrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Login method is undefined')
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function will capture the public key from the nostr extension or if no extension present
|
* Function will capture the public key from the nostr extension or if no extension present
|
||||||
* function wil capture the public key from the local storage
|
* function wil capture the public key from the local storage
|
||||||
|
@ -22,7 +22,7 @@ import JSZip from 'jszip'
|
|||||||
import { MuiFileInput } from 'mui-file-input'
|
import { MuiFileInput } from 'mui-file-input'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
import { UserComponent } from '../../components/username'
|
import { UserComponent } from '../../components/username'
|
||||||
@ -33,6 +33,7 @@ import { Meta, ProfileMetadata, User, UserRole } from '../../types'
|
|||||||
import {
|
import {
|
||||||
encryptArrayBuffer,
|
encryptArrayBuffer,
|
||||||
generateEncryptionKey,
|
generateEncryptionKey,
|
||||||
|
generateKeysFile,
|
||||||
getHash,
|
getHash,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
isOnline,
|
isOnline,
|
||||||
@ -49,16 +50,16 @@ import { HTML5Backend } from 'react-dnd-html5-backend'
|
|||||||
import type { Identifier, XYCoord } from 'dnd-core'
|
import type { Identifier, XYCoord } from 'dnd-core'
|
||||||
import { useDrag, useDrop } from 'react-dnd'
|
import { useDrag, useDrop } from 'react-dnd'
|
||||||
import saveAs from 'file-saver'
|
import saveAs from 'file-saver'
|
||||||
import CopyModal from '../../components/copyModal'
|
|
||||||
import { Event, kinds } from 'nostr-tools'
|
import { Event, kinds } from 'nostr-tools'
|
||||||
import { DrawPDFFields } from '../../components/DrawPDFFields'
|
import { DrawPDFFields } from '../../components/DrawPDFFields'
|
||||||
|
|
||||||
export const CreatePage = () => {
|
export const CreatePage = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
|
const { uploadedFile } = location.state || {}
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||||
const [openCopyModal, setOpenCopyModel] = useState(false)
|
|
||||||
const [textToCopy, setTextToCopy] = useState('')
|
|
||||||
|
|
||||||
const [authUrl, setAuthUrl] = useState<string>()
|
const [authUrl, setAuthUrl] = useState<string>()
|
||||||
|
|
||||||
@ -75,6 +76,12 @@ export const CreatePage = () => {
|
|||||||
|
|
||||||
const nostrController = NostrController.getInstance()
|
const nostrController = NostrController.getInstance()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (uploadedFile) {
|
||||||
|
setSelectedFiles([uploadedFile])
|
||||||
|
}
|
||||||
|
}, [uploadedFile])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (usersPubkey) {
|
if (usersPubkey) {
|
||||||
setUsers((prev) => {
|
setUsers((prev) => {
|
||||||
@ -222,57 +229,67 @@ export const CreatePage = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCreate = async () => {
|
// Validate inputs before proceeding
|
||||||
|
const validateInputs = (): boolean => {
|
||||||
if (!title.trim()) {
|
if (!title.trim()) {
|
||||||
toast.error('Title can not be empty')
|
toast.error('Title can not be empty')
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (users.length === 0) {
|
if (users.length === 0) {
|
||||||
toast.error(
|
toast.error(
|
||||||
'No signer/viewer is provided. At least add one signer or viewer.'
|
'No signer/viewer is provided. At least add one signer or viewer.'
|
||||||
)
|
)
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedFiles.length === 0) {
|
if (selectedFiles.length === 0) {
|
||||||
toast.error('No file is selected. Select at least 1 file')
|
toast.error('No file is selected. Select at least 1 file')
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true)
|
return true
|
||||||
setLoadingSpinnerDesc('Generating hashes for files')
|
}
|
||||||
|
|
||||||
const fileHashes: { [key: string]: string } = {}
|
// Handle errors during file arrayBuffer conversion
|
||||||
|
const handleFileError = (file: File) => (err: any) => {
|
||||||
// generating file hashes
|
|
||||||
for (const file of selectedFiles) {
|
|
||||||
const arraybuffer = await file.arrayBuffer().catch((err) => {
|
|
||||||
console.log(
|
console.log(
|
||||||
`err while getting arrayBuffer of file ${file.name} :>> `,
|
`Error while getting arrayBuffer of file ${file.name} :>> `,
|
||||||
err
|
err
|
||||||
)
|
)
|
||||||
toast.error(
|
toast.error(
|
||||||
err.message || `err while getting arrayBuffer of file ${file.name}`
|
err.message || `Error while getting arrayBuffer of file ${file.name}`
|
||||||
)
|
)
|
||||||
return null
|
return null
|
||||||
})
|
}
|
||||||
|
|
||||||
if (!arraybuffer) return
|
// Generate hash for each selected file
|
||||||
|
const generateFileHashes = async (): Promise<{
|
||||||
|
[key: string]: string
|
||||||
|
} | null> => {
|
||||||
|
const fileHashes: { [key: string]: string } = {}
|
||||||
|
|
||||||
|
for (const file of selectedFiles) {
|
||||||
|
const arraybuffer = await file.arrayBuffer().catch(handleFileError(file))
|
||||||
|
if (!arraybuffer) return null
|
||||||
|
|
||||||
const hash = await getHash(arraybuffer)
|
const hash = await getHash(arraybuffer)
|
||||||
|
|
||||||
if (!hash) {
|
if (!hash) {
|
||||||
setIsLoading(false)
|
return null
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileHashes[file.name] = hash
|
fileHashes[file.name] = hash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return fileHashes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a zip file with the selected files and sign the event
|
||||||
|
const createZipFile = async (fileHashes: {
|
||||||
|
[key: string]: string
|
||||||
|
}): Promise<{ zip: JSZip; createSignature: string } | null> => {
|
||||||
const zip = new JSZip()
|
const zip = new JSZip()
|
||||||
|
|
||||||
// zipping files
|
|
||||||
selectedFiles.forEach((file) => {
|
selectedFiles.forEach((file) => {
|
||||||
zip.file(`files/${file.name}`, file)
|
zip.file(`files/${file.name}`, file)
|
||||||
})
|
})
|
||||||
@ -281,6 +298,7 @@ export const CreatePage = () => {
|
|||||||
const viewers = users.filter((user) => user.role === UserRole.viewer)
|
const viewers = users.filter((user) => user.role === UserRole.viewer)
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Signing nostr event')
|
setLoadingSpinnerDesc('Signing nostr event')
|
||||||
|
|
||||||
const createSignature = await signEventForMetaFile(
|
const createSignature = await signEventForMetaFile(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
signers: signers.map((signer) => hexToNpub(signer.pubkey)),
|
signers: signers.map((signer) => hexToNpub(signer.pubkey)),
|
||||||
@ -291,12 +309,27 @@ export const CreatePage = () => {
|
|||||||
setIsLoading
|
setIsLoading
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!createSignature) return
|
if (!createSignature) return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
zip,
|
||||||
|
createSignature: JSON.stringify(createSignature, null, 2)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add metadata and file hashes to the zip file
|
||||||
|
const addMetaToZip = async (
|
||||||
|
zip: JSZip,
|
||||||
|
createSignature: string
|
||||||
|
): Promise<string | null> => {
|
||||||
// create content for meta file
|
// create content for meta file
|
||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
title,
|
title,
|
||||||
createSignature: JSON.stringify(createSignature, null, 2),
|
createSignature,
|
||||||
docSignatures: {}
|
docSignatures: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,68 +338,146 @@ export const CreatePage = () => {
|
|||||||
zip.file('meta.json', stringifiedMeta)
|
zip.file('meta.json', stringifiedMeta)
|
||||||
|
|
||||||
const metaHash = await getHash(stringifiedMeta)
|
const metaHash = await getHash(stringifiedMeta)
|
||||||
if (!metaHash) return
|
if (!metaHash) return null
|
||||||
|
|
||||||
const metaHashJson = {
|
const metaHashJson = {
|
||||||
[usersPubkey!]: metaHash
|
[usersPubkey!]: metaHash
|
||||||
}
|
}
|
||||||
|
|
||||||
zip.file('hashes.json', JSON.stringify(metaHashJson, null, 2))
|
zip.file('hashes.json', JSON.stringify(metaHashJson, null, 2))
|
||||||
|
return metaHash
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
toast.error('An error occurred in converting meta json to string')
|
toast.error('An error occurred in converting meta json to string')
|
||||||
return
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle errors during zip file generation
|
||||||
|
const handleZipError = (err: any) => {
|
||||||
|
console.log('Error in zip:>> ', err)
|
||||||
|
setIsLoading(false)
|
||||||
|
toast.error(err.message || 'Error occurred in generating zip file')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the zip file
|
||||||
|
const generateZipFile = async (zip: JSZip): Promise<ArrayBuffer | null> => {
|
||||||
setLoadingSpinnerDesc('Generating zip file')
|
setLoadingSpinnerDesc('Generating zip file')
|
||||||
|
|
||||||
const arraybuffer = await zip
|
const arraybuffer = await zip
|
||||||
.generateAsync({
|
.generateAsync({
|
||||||
type: 'arraybuffer',
|
type: 'arraybuffer',
|
||||||
compression: 'DEFLATE',
|
compression: 'DEFLATE',
|
||||||
compressionOptions: {
|
compressionOptions: { level: 6 }
|
||||||
level: 6
|
})
|
||||||
|
.catch(handleZipError)
|
||||||
|
|
||||||
|
return arraybuffer
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.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 encryptionKey = await generateEncryptionKey()
|
|
||||||
|
|
||||||
|
// Encrypt the zip file with the generated encryption key
|
||||||
|
const encryptZipFile = async (
|
||||||
|
arraybuffer: ArrayBuffer,
|
||||||
|
encryptionKey: string
|
||||||
|
): Promise<ArrayBuffer> => {
|
||||||
setLoadingSpinnerDesc('Encrypting zip file')
|
setLoadingSpinnerDesc('Encrypting zip file')
|
||||||
const encryptedArrayBuffer = await encryptArrayBuffer(
|
return encryptArrayBuffer(arraybuffer, encryptionKey)
|
||||||
arraybuffer,
|
}
|
||||||
encryptionKey
|
|
||||||
).finally(() => setIsLoading(false))
|
|
||||||
|
|
||||||
|
// create final zip file
|
||||||
|
const createFinalZipFile = async (
|
||||||
|
encryptedArrayBuffer: ArrayBuffer,
|
||||||
|
encryptionKey: string
|
||||||
|
): Promise<File | null> => {
|
||||||
|
// Get the current timestamp in seconds
|
||||||
|
const unixNow = Math.floor(Date.now() / 1000)
|
||||||
const blob = new Blob([encryptedArrayBuffer])
|
const blob = new Blob([encryptedArrayBuffer])
|
||||||
|
// Create a File object with the Blob data
|
||||||
if (await isOnline()) {
|
const file = new File([blob], `compressed.sigit`, {
|
||||||
setIsLoading(true)
|
type: 'application/sigit'
|
||||||
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
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const firstSigner = users.filter((user) => user.role === UserRole.signer)[0]
|
||||||
|
|
||||||
|
const keysFileContent = await generateKeysFile(
|
||||||
|
[firstSigner.pubkey],
|
||||||
|
encryptionKey
|
||||||
|
)
|
||||||
|
if (!keysFileContent) return null
|
||||||
|
|
||||||
|
const zip = new JSZip()
|
||||||
|
zip.file(`compressed.sigit`, file)
|
||||||
|
zip.file('keys.json', keysFileContent)
|
||||||
|
|
||||||
|
const arraybuffer = await zip
|
||||||
|
.generateAsync({
|
||||||
|
type: 'arraybuffer',
|
||||||
|
compression: 'DEFLATE',
|
||||||
|
compressionOptions: { level: 6 }
|
||||||
|
})
|
||||||
|
.catch(handleZipError)
|
||||||
|
|
||||||
|
if (!arraybuffer) return null
|
||||||
|
|
||||||
|
const finalZipFile = new File(
|
||||||
|
[new Blob([arraybuffer])],
|
||||||
|
`${unixNow}.sigit.zip`,
|
||||||
|
{
|
||||||
|
type: 'application/zip'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return finalZipFile
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOnlineFlow = async (
|
||||||
|
encryptedArrayBuffer: ArrayBuffer,
|
||||||
|
encryptionKey: string
|
||||||
|
) => {
|
||||||
|
const unixNow = Math.floor(Date.now() / 1000)
|
||||||
|
const blob = new Blob([encryptedArrayBuffer])
|
||||||
|
// Create a File object with the Blob data
|
||||||
|
const file = new File([blob], `compressed-${unixNow}.sigit`, {
|
||||||
|
type: 'application/sigit'
|
||||||
|
})
|
||||||
|
|
||||||
|
const fileUrl = await uploadFile(file)
|
||||||
if (!fileUrl) return
|
if (!fileUrl) return
|
||||||
|
|
||||||
|
await sendDMs(fileUrl, encryptionKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle errors during file upload
|
||||||
|
const handleUploadError = (err: any) => {
|
||||||
|
console.log('Error in upload:>> ', err)
|
||||||
|
setIsLoading(false)
|
||||||
|
toast.error(err.message || 'Error occurred in uploading file')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload the file to the storage
|
||||||
|
const uploadFile = async (file: File): Promise<string | null> => {
|
||||||
|
setIsLoading(true)
|
||||||
|
setLoadingSpinnerDesc('Uploading sigit to file storage.')
|
||||||
|
|
||||||
|
const fileUrl = await uploadToFileStorage(file, nostrController)
|
||||||
|
.then((url) => {
|
||||||
|
toast.success('Sigit uploaded to file storage')
|
||||||
|
return url
|
||||||
|
})
|
||||||
|
.catch(handleUploadError)
|
||||||
|
|
||||||
|
return fileUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send DMs to signers and viewers with the file URL
|
||||||
|
const sendDMs = async (fileUrl: string, encryptionKey: string) => {
|
||||||
setLoadingSpinnerDesc('Sending DM to signers/viewers')
|
setLoadingSpinnerDesc('Sending DM to signers/viewers')
|
||||||
|
|
||||||
// send DM to first signer if exists
|
const signers = users.filter((user) => user.role === UserRole.signer)
|
||||||
|
const viewers = users.filter((user) => user.role === UserRole.viewer)
|
||||||
|
|
||||||
if (signers.length > 0) {
|
if (signers.length > 0) {
|
||||||
await sendDM(
|
await sendDM(
|
||||||
fileUrl,
|
fileUrl,
|
||||||
@ -377,9 +488,7 @@ export const CreatePage = () => {
|
|||||||
setAuthUrl
|
setAuthUrl
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// send DM to all viewers if no signer
|
|
||||||
for (const viewer of viewers) {
|
for (const viewer of viewers) {
|
||||||
// todo: execute in parallel
|
|
||||||
await sendDM(
|
await sendDM(
|
||||||
fileUrl,
|
fileUrl,
|
||||||
encryptionKey,
|
encryptionKey,
|
||||||
@ -389,29 +498,61 @@ export const CreatePage = () => {
|
|||||||
setAuthUrl
|
setAuthUrl
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
setIsLoading(false)
|
}
|
||||||
|
|
||||||
navigate(
|
// Manage offline scenarios for signing or viewing the file
|
||||||
`${appPrivateRoutes.sign}?file=${encodeURIComponent(
|
const handleOfflineFlow = async (
|
||||||
fileUrl
|
encryptedArrayBuffer: ArrayBuffer,
|
||||||
)}&key=${encodeURIComponent(encryptionKey)}`
|
encryptionKey: string
|
||||||
|
) => {
|
||||||
|
const finalZipFile = await createFinalZipFile(
|
||||||
|
encryptedArrayBuffer,
|
||||||
|
encryptionKey
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
if (signers[0] && signers[0].pubkey === usersPubkey) {
|
|
||||||
// Create a File object with the Blob data
|
|
||||||
const file = new File([blob], `compressed.sigit`, {
|
|
||||||
type: 'application/sigit'
|
|
||||||
})
|
|
||||||
|
|
||||||
navigate(appPrivateRoutes.sign, { state: { file, encryptionKey } })
|
if (!finalZipFile) return
|
||||||
|
|
||||||
|
saveAs(finalZipFile, 'request.sigit.zip')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
if (!validateInputs()) return
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
setLoadingSpinnerDesc('Generating hashes for files')
|
||||||
|
|
||||||
|
const fileHashes = await generateFileHashes()
|
||||||
|
if (!fileHashes) return
|
||||||
|
|
||||||
|
const createZipResponse = await createZipFile(fileHashes)
|
||||||
|
if (!createZipResponse) return
|
||||||
|
|
||||||
|
const { zip, createSignature } = createZipResponse
|
||||||
|
|
||||||
|
const metaHash = await addMetaToZip(zip, createSignature)
|
||||||
|
if (!metaHash) return
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Generating zip file')
|
||||||
|
|
||||||
|
const arrayBuffer = await generateZipFile(zip)
|
||||||
|
if (!arrayBuffer) return
|
||||||
|
|
||||||
|
const encryptionKey = await generateEncryptionKey()
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Encrypting zip file')
|
||||||
|
const encryptedArrayBuffer = await encryptZipFile(
|
||||||
|
arrayBuffer,
|
||||||
|
encryptionKey
|
||||||
|
)
|
||||||
|
|
||||||
|
if (await isOnline()) {
|
||||||
|
await handleOnlineFlow(encryptedArrayBuffer, encryptionKey)
|
||||||
} else {
|
} else {
|
||||||
saveAs(blob, 'request.sigit')
|
await handleOfflineFlow(encryptedArrayBuffer, encryptionKey)
|
||||||
setTextToCopy(encryptionKey)
|
|
||||||
setOpenCopyModel(true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navigate(appPrivateRoutes.sign, { state: { arrayBuffer } })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authUrl) {
|
if (authUrl) {
|
||||||
@ -512,15 +653,6 @@ export const CreatePage = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<CopyModal
|
|
||||||
open={openCopyModal}
|
|
||||||
handleClose={() => {
|
|
||||||
setOpenCopyModel(false)
|
|
||||||
navigate(appPrivateRoutes.sign)
|
|
||||||
}}
|
|
||||||
title="Decryption key for Sigit file"
|
|
||||||
textToCopy={textToCopy}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -5,13 +5,63 @@ import {
|
|||||||
PersonOutline,
|
PersonOutline,
|
||||||
Upload
|
Upload
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import { Box, Button, Typography } from '@mui/material'
|
import { Box, Button, Tooltip, Typography } from '@mui/material'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { appPrivateRoutes } from '../../routes'
|
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
|
import { useRef } from 'react'
|
||||||
|
import JSZip from 'jszip'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
|
||||||
export const HomePage = () => {
|
export const HomePage = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const handleUploadClick = () => {
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileChange = async (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
const file = event.target.files?.[0]
|
||||||
|
if (file) {
|
||||||
|
// Check if the file extension is .sigit.zip
|
||||||
|
const fileName = file.name
|
||||||
|
const fileExtension = fileName.slice(-10) // ".sigit.zip" has 10 characters
|
||||||
|
if (fileExtension === '.sigit.zip') {
|
||||||
|
const zip = await JSZip.loadAsync(file).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
|
||||||
|
|
||||||
|
// navigate to sign page if zip contains keys.json
|
||||||
|
if ('keys.json' in zip.files) {
|
||||||
|
return navigate(appPrivateRoutes.sign, {
|
||||||
|
state: { uploadedZip: file }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// navigate to verify page if zip contains meta.json
|
||||||
|
if ('meta.json' in zip.files) {
|
||||||
|
return navigate(appPublicRoutes.verify, {
|
||||||
|
state: { uploadedZip: file }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error('Invalid zip file')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// navigate to create page
|
||||||
|
navigate(appPrivateRoutes.create, { state: { uploadedFile: file } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={styles.container}>
|
<Box className={styles.container}>
|
||||||
@ -19,11 +69,26 @@ export const HomePage = () => {
|
|||||||
<Typography variant="h3" className={styles.title}>
|
<Typography variant="h3" className={styles.title}>
|
||||||
Sigits
|
Sigits
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box className={styles.actionButtons}>
|
{/* This is for desktop view */}
|
||||||
|
<Box
|
||||||
|
className={styles.actionButtons}
|
||||||
|
sx={{
|
||||||
|
display: {
|
||||||
|
xs: 'none',
|
||||||
|
md: 'flex'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
startIcon={<Upload />}
|
startIcon={<Upload />}
|
||||||
onClick={() => navigate(appPrivateRoutes.sign)}
|
onClick={handleUploadClick}
|
||||||
>
|
>
|
||||||
Upload
|
Upload
|
||||||
</Button>
|
</Button>
|
||||||
@ -35,19 +100,71 @@ export const HomePage = () => {
|
|||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
{/* This is for mobile view */}
|
||||||
|
<Box
|
||||||
|
className={styles.actionButtons}
|
||||||
|
sx={{
|
||||||
|
display: {
|
||||||
|
xs: 'flex',
|
||||||
|
md: 'none'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip title="Upload" arrow>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => navigate(appPrivateRoutes.sign)}
|
||||||
|
>
|
||||||
|
<Upload />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Create" arrow>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => navigate(appPrivateRoutes.create)}
|
||||||
|
>
|
||||||
|
<Add />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box className={styles.submissions}>
|
||||||
<PlaceHolder />
|
<PlaceHolder />
|
||||||
<PlaceHolder />
|
<PlaceHolder />
|
||||||
<PlaceHolder />
|
<PlaceHolder />
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const PlaceHolder = () => {
|
const PlaceHolder = () => {
|
||||||
return (
|
return (
|
||||||
<Box className={styles.submissions}>
|
<Box
|
||||||
<Box className={styles.item}>
|
className={styles.item}
|
||||||
<Box className={styles.titleBox}>
|
sx={{
|
||||||
|
flexDirection: {
|
||||||
|
xs: 'column',
|
||||||
|
md: 'row'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
className={styles.titleBox}
|
||||||
|
sx={{
|
||||||
|
flexDirection: {
|
||||||
|
xs: 'row',
|
||||||
|
md: 'column'
|
||||||
|
},
|
||||||
|
borderBottomLeftRadius: {
|
||||||
|
xs: 'initial',
|
||||||
|
md: 'inherit'
|
||||||
|
},
|
||||||
|
borderTopRightRadius: {
|
||||||
|
xs: 'inherit',
|
||||||
|
md: 'initial'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Typography variant="body1" className={styles.titleBoxItem}>
|
<Typography variant="body1" className={styles.titleBoxItem}>
|
||||||
<Description />
|
<Description />
|
||||||
Title
|
Title
|
||||||
@ -76,6 +193,5 @@ const PlaceHolder = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.actionButtons {
|
.actionButtons {
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@ -25,6 +24,7 @@
|
|||||||
.submissions {
|
.submissions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -33,10 +33,10 @@
|
|||||||
|
|
||||||
.titleBox {
|
.titleBox {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: #e7e2df99;
|
background-color: #cdc8c499;
|
||||||
border-top-left-radius: inherit;
|
border-top-left-radius: inherit;
|
||||||
border-bottom-left-radius: inherit;
|
border-bottom-left-radius: inherit;
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
426
src/pages/sign/internal/displayMeta.tsx
Normal file
426
src/pages/sign/internal/displayMeta.tsx
Normal file
@ -0,0 +1,426 @@
|
|||||||
|
import JSZip from 'jszip'
|
||||||
|
import {
|
||||||
|
Meta,
|
||||||
|
ProfileMetadata,
|
||||||
|
SignedEventContent,
|
||||||
|
User,
|
||||||
|
UserRole
|
||||||
|
} from '../../../types'
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
IconButton,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListSubheader,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
useTheme
|
||||||
|
} from '@mui/material'
|
||||||
|
import {
|
||||||
|
Download,
|
||||||
|
CheckCircle,
|
||||||
|
Cancel,
|
||||||
|
HourglassTop
|
||||||
|
} from '@mui/icons-material'
|
||||||
|
import saveAs from 'file-saver'
|
||||||
|
import { kinds, Event } from 'nostr-tools'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { UserComponent } from '../../../components/username'
|
||||||
|
import { MetadataController } from '../../../controllers'
|
||||||
|
import {
|
||||||
|
npubToHex,
|
||||||
|
readContentOfZipEntry,
|
||||||
|
shorten,
|
||||||
|
hexToNpub,
|
||||||
|
parseJson
|
||||||
|
} from '../../../utils'
|
||||||
|
import styles from '../style.module.scss'
|
||||||
|
|
||||||
|
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
|
||||||
|
getPrevSignersSig: (usersNpub: string) => string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DisplayMeta = ({
|
||||||
|
meta,
|
||||||
|
zip,
|
||||||
|
submittedBy,
|
||||||
|
signers,
|
||||||
|
viewers,
|
||||||
|
creatorFileHashes,
|
||||||
|
currentFileHashes,
|
||||||
|
signedBy,
|
||||||
|
nextSigner,
|
||||||
|
getPrevSignersSig
|
||||||
|
}: DisplayMetaProps) => {
|
||||||
|
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(() => {
|
||||||
|
signers.forEach((signer) => {
|
||||||
|
const hexKey = npubToHex(signer)
|
||||||
|
setUsers((prev) => {
|
||||||
|
if (prev.findIndex((user) => user.pubkey === hexKey) !== -1) return prev
|
||||||
|
|
||||||
|
return [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
pubkey: hexKey!,
|
||||||
|
role: UserRole.signer
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
viewers.forEach((viewer) => {
|
||||||
|
const hexKey = npubToHex(viewer)
|
||||||
|
setUsers((prev) => {
|
||||||
|
if (prev.findIndex((user) => user.pubkey === hexKey) !== -1) return prev
|
||||||
|
|
||||||
|
return [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
pubkey: hexKey!,
|
||||||
|
role: UserRole.viewer
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [signers, viewers])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const metadataController = new MetadataController()
|
||||||
|
|
||||||
|
const hexKeys: string[] = [
|
||||||
|
npubToHex(submittedBy)!,
|
||||||
|
...users.map((user) => user.pubkey)
|
||||||
|
]
|
||||||
|
|
||||||
|
hexKeys.forEach((key) => {
|
||||||
|
if (!(key in metadata)) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
metadataController
|
||||||
|
.findMetadata(key)
|
||||||
|
.then((metadataEvent) => {
|
||||||
|
if (metadataEvent) handleMetadataEvent(metadataEvent)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(`error occurred in finding metadata for: ${key}`, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [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
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
marginTop: 2
|
||||||
|
}}
|
||||||
|
subheader={
|
||||||
|
<ListSubheader className={styles.subHeader}>Meta Info</ListSubheader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItem
|
||||||
|
sx={{
|
||||||
|
marginTop: 1,
|
||||||
|
gap: '15px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" sx={{ color: textColor }}>
|
||||||
|
Submitted By
|
||||||
|
</Typography>
|
||||||
|
{(function () {
|
||||||
|
const profile = metadata[submittedBy]
|
||||||
|
return (
|
||||||
|
<UserComponent
|
||||||
|
pubkey={submittedBy}
|
||||||
|
name={
|
||||||
|
profile?.display_name ||
|
||||||
|
profile?.name ||
|
||||||
|
shorten(hexToNpub(submittedBy))
|
||||||
|
}
|
||||||
|
image={profile?.picture}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
sx={{
|
||||||
|
marginTop: 1,
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'flex-start'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" sx={{ color: textColor }}>
|
||||||
|
Files
|
||||||
|
</Typography>
|
||||||
|
<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>
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</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>
|
||||||
|
{users.map((user) => (
|
||||||
|
<DisplayUser
|
||||||
|
key={user.pubkey}
|
||||||
|
meta={meta}
|
||||||
|
user={user}
|
||||||
|
metadata={metadata}
|
||||||
|
signedBy={signedBy}
|
||||||
|
nextSigner={nextSigner}
|
||||||
|
getPrevSignersSig={getPrevSignersSig}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
@ -32,14 +32,17 @@ import {
|
|||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { Cancel, CheckCircle } from '@mui/icons-material'
|
import { Cancel, CheckCircle } from '@mui/icons-material'
|
||||||
|
import { useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
export const VerifyPage = () => {
|
export const VerifyPage = () => {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
const textColor = theme.palette.getContrastText(
|
const textColor = theme.palette.getContrastText(
|
||||||
theme.palette.background.paper
|
theme.palette.background.paper
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const location = useLocation()
|
||||||
|
const { uploadedZip } = location.state || {}
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||||
|
|
||||||
@ -62,6 +65,12 @@ export const VerifyPage = () => {
|
|||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (uploadedZip) {
|
||||||
|
setSelectedFile(uploadedZip)
|
||||||
|
}
|
||||||
|
}, [uploadedZip])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (zip) {
|
if (zip) {
|
||||||
const generateCurrentFileHashes = async () => {
|
const generateCurrentFileHashes = async () => {
|
||||||
@ -364,7 +373,7 @@ export const VerifyPage = () => {
|
|||||||
onChange={(value) => setSelectedFile(value)}
|
onChange={(value) => setSelectedFile(value)}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
inputProps: {
|
inputProps: {
|
||||||
accept: '.zip'
|
accept: '.sigit.zip'
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { EventTemplate } from 'nostr-tools'
|
import {
|
||||||
|
EventTemplate,
|
||||||
|
generateSecretKey,
|
||||||
|
getPublicKey,
|
||||||
|
nip04
|
||||||
|
} from 'nostr-tools'
|
||||||
import { MetadataController, NostrController } from '../controllers'
|
import { MetadataController, NostrController } from '../controllers'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { appPrivateRoutes } from '../routes'
|
import { appPrivateRoutes } from '../routes'
|
||||||
@ -11,17 +16,12 @@ import { appPrivateRoutes } from '../routes'
|
|||||||
* @returns The URL of the uploaded file.
|
* @returns The URL of the uploaded file.
|
||||||
*/
|
*/
|
||||||
export const uploadToFileStorage = async (
|
export const uploadToFileStorage = async (
|
||||||
blob: Blob,
|
file: File,
|
||||||
nostrController: NostrController
|
nostrController: NostrController
|
||||||
) => {
|
) => {
|
||||||
// Get the current timestamp in seconds
|
// Get the current timestamp in seconds
|
||||||
const unixNow = Math.floor(Date.now() / 1000)
|
const unixNow = Math.floor(Date.now() / 1000)
|
||||||
|
|
||||||
// Create a File object with the Blob data
|
|
||||||
const file = new File([blob], `compressed-${unixNow}.sigit`, {
|
|
||||||
type: 'application/sigit'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Define event metadata for authorization
|
// Define event metadata for authorization
|
||||||
const event: EventTemplate = {
|
const event: EventTemplate = {
|
||||||
kind: 24242,
|
kind: 24242,
|
||||||
@ -205,3 +205,52 @@ export const signEventForMetaFile = async (
|
|||||||
|
|
||||||
return signedEvent // Return the signed event
|
return signedEvent // Return the signed event
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the content for keys.json file.
|
||||||
|
*
|
||||||
|
* @param users - An array of public keys.
|
||||||
|
* @param key - The key that will be encrypted for each user.
|
||||||
|
* @returns A promise that resolves to a JSON string containing the sender's public key and encrypted keys, or null if an error occurs.
|
||||||
|
*/
|
||||||
|
export const generateKeysFile = async (
|
||||||
|
users: string[],
|
||||||
|
key: string
|
||||||
|
): Promise<string | null> => {
|
||||||
|
// Generate a random private key to act as the sender
|
||||||
|
const privateKey = generateSecretKey()
|
||||||
|
|
||||||
|
// Calculate the required length to be a multiple of 10
|
||||||
|
const requiredLength = Math.ceil(users.length / 10) * 10
|
||||||
|
const additionalKeysCount = requiredLength - users.length
|
||||||
|
|
||||||
|
if (additionalKeysCount > 0) {
|
||||||
|
// generate random public keys to make the keys array multiple of 10
|
||||||
|
const additionalPubkeys = Array.from({ length: additionalKeysCount }, () =>
|
||||||
|
getPublicKey(generateSecretKey())
|
||||||
|
)
|
||||||
|
|
||||||
|
users.push(...additionalPubkeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt the key for each user's public key
|
||||||
|
const promises = users.map((pubkey) => nip04.encrypt(privateKey, pubkey, key))
|
||||||
|
|
||||||
|
// Wait for all encryption promises to resolve
|
||||||
|
const keys = await Promise.all(promises).catch((err) => {
|
||||||
|
console.log('Error while generating keys :>> ', err)
|
||||||
|
toast.error(err.message || 'An error occurred while generating key')
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
// If any encryption promise failed, return null
|
||||||
|
if (!keys) return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Return a JSON string containing the sender's public key and encrypted keys
|
||||||
|
return JSON.stringify({ sender: getPublicKey(privateKey), keys })
|
||||||
|
} catch (error) {
|
||||||
|
// Return null if an error occurs during JSON stringification
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user