diff --git a/package-lock.json b/package-lock.json index a156375..a03e759 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,7 @@ "react-singleton-hook": "^4.0.1", "react-toastify": "10.0.4", "redux": "5.0.1", - "svgo": "^3.3.2", + "signature_pad": "^5.0.4", "tseep": "1.2.1" }, "devDependencies": { @@ -2214,6 +2214,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, "license": "ISC", "engines": { "node": ">=10.13.0" @@ -2935,6 +2936,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, "license": "ISC" }, "node_modules/brace-expansion": { @@ -3634,6 +3636,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", @@ -3650,6 +3653,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, "license": "MIT", "dependencies": { "mdn-data": "2.0.30", @@ -3663,6 +3667,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">= 6" @@ -3675,6 +3680,7 @@ "version": "5.0.5", "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, "license": "MIT", "dependencies": { "css-tree": "~2.2.0" @@ -3688,6 +3694,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, "license": "MIT", "dependencies": { "mdn-data": "2.0.28", @@ -3702,6 +3709,7 @@ "version": "2.0.28", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true, "license": "CC0-1.0" }, "node_modules/csstype": { @@ -3949,6 +3957,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", @@ -3976,6 +3985,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, "funding": [ { "type": "github", @@ -3988,6 +3998,7 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" @@ -4003,6 +4014,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", @@ -4020,9 +4032,9 @@ "dev": true }, "node_modules/elliptic": { - "version": "6.5.7", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz", - "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.0.tgz", + "integrity": "sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==", "dev": true, "license": "MIT", "dependencies": { @@ -4052,6 +4064,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -5979,6 +5992,7 @@ "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true, "license": "CC0-1.0" }, "node_modules/merge-stream": { @@ -6560,6 +6574,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" @@ -6919,6 +6934,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -7866,6 +7882,12 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "optional": true }, + "node_modules/signature_pad": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/signature_pad/-/signature_pad-5.0.4.tgz", + "integrity": "sha512-nngOixbwLAUOuH3QnZwlgwmynQblxmo4iWacKFwfymJfiY+Qt+9icNtcIe/okqXKun4hJ5QTFmHyC7dmv6lf2w==", + "license": "MIT" + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -7971,6 +7993,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -8197,6 +8220,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "dev": true, "license": "MIT", "dependencies": { "@trysound/sax": "0.2.0", @@ -8222,6 +8246,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 10" diff --git a/package.json b/package.json index 3eaa670..ab49da0 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "react-singleton-hook": "^4.0.1", "react-toastify": "10.0.4", "redux": "5.0.1", - "svgo": "^3.3.2", + "signature_pad": "^5.0.4", "tseep": "1.2.1" }, "devDependencies": { diff --git a/src/App.tsx b/src/App.tsx index 5dbb867..a17e708 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,14 +3,13 @@ import { useAppSelector } from './hooks/store' import { Navigate, Route, Routes } from 'react-router-dom' import { AuthController } from './controllers' import { MainLayout } from './layouts/Main' +import { appPrivateRoutes, appPublicRoutes } from './routes' +import './App.scss' import { - appPrivateRoutes, - appPublicRoutes, privateRoutes, publicRoutes, recursiveRouteRenderer -} from './routes' -import './App.scss' +} from './routes/util' const App = () => { const authState = useAppSelector((state) => state.auth) diff --git a/src/components/MarkFormField/index.tsx b/src/components/MarkFormField/index.tsx index 7328065..18bbf31 100644 --- a/src/components/MarkFormField/index.tsx +++ b/src/components/MarkFormField/index.tsx @@ -7,7 +7,7 @@ import { isCurrentValueLast } from '../../utils' import React, { useState } from 'react' -import { MARK_TYPE_CONFIG } from '../getMarkComponents.tsx' +import { MarkInput } from '../MarkTypeStrategy/MarkInput.tsx' interface MarkFormFieldProps { currentUserMarks: CurrentUserMark[] @@ -52,8 +52,7 @@ const MarkFormField = ({ } const toggleActions = () => setDisplayActions(!displayActions) const markLabel = getToolboxLabelByMarkType(selectedMark.mark.type) - const { input: MarkInputComponent } = - MARK_TYPE_CONFIG[selectedMark.mark.type] || {} + return (
@@ -84,14 +83,14 @@ const MarkFormField = ({
handleFormSubmit(e)}> - {typeof MarkInputComponent !== 'undefined' && ( - - )} +
) })} diff --git a/src/components/getMarkComponents.tsx b/src/components/getMarkComponents.tsx deleted file mode 100644 index 507f388..0000000 --- a/src/components/getMarkComponents.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { MarkType } from '../types/drawing' -import { MarkConfigs } from '../types/mark' -import { MarkInputSignature } from './MarkInputs/Signature' -import { MarkInputText } from './MarkInputs/Text' -import { MarkRenderSignature } from './MarkRender/Signature' - -export const MARK_TYPE_CONFIG: MarkConfigs = { - [MarkType.TEXT]: { - input: MarkInputText, - render: ({ value }) => <>{value} - }, - [MarkType.SIGNATURE]: { - input: MarkInputSignature, - render: MarkRenderSignature - } -} diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx index 8f79dc6..85841f2 100644 --- a/src/hooks/useSigitMeta.tsx +++ b/src/hooks/useSigitMeta.tsx @@ -21,6 +21,7 @@ import { Event } from 'nostr-tools' import store from '../store/store' import { NostrController } from '../controllers' import { MetaParseError } from '../types/errors/MetaParseError' +import { MARK_TYPE_CONFIG } from '../components/MarkTypeStrategy/MarkStrategy' /** * Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`, @@ -142,6 +143,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { setMarkConfig(markConfig) setZipUrl(zipUrl) + let encryptionKey: string | null = null if (meta.keys) { const { sender, keys } = meta.keys // Retrieve the user's public key from the state @@ -162,6 +164,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { return null }) + encryptionKey = decrypted setEncryptionKey(decrypted) } } @@ -206,13 +209,40 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { } } - parsedSignatureEventsMap.forEach((event, npub) => { + for (const [npub, event] of parsedSignatureEventsMap) { const isValidSignature = verifyEvent(event) if (isValidSignature) { // get the signature of prev signer from the content of current signers signedEvent const prevSignersSig = getPrevSignerSig(npub) + try { const obj: SignedEventContent = JSON.parse(event.content) + + // Signature object can include values that need to be fetched and decrypted + for (let i = 0; i < obj.marks.length; i++) { + const m = obj.marks[i] + + try { + const { fetchAndDecrypt } = MARK_TYPE_CONFIG[m.type] || {} + if ( + typeof fetchAndDecrypt === 'function' && + m.value && + encryptionKey + ) { + const decrypted = await fetchAndDecrypt( + m.value, + encryptionKey + ) + obj.marks[i].value = decrypted + } + } catch (error) { + console.error( + `Error during mark fetchAndDecrypt phase`, + error + ) + } + } + parsedSignatureEventsMap.set(npub, { ...event, parsedContent: obj @@ -228,7 +258,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { signerStatusMap.set(npub as `npub1${string}`, SignStatus.Invalid) } } - }) + } signers .filter((s) => !parsedSignatureEventsMap.has(s)) diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 0a022eb..f30ecdd 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -33,7 +33,8 @@ import { signEventForMetaFile, updateUsersAppData, findOtherUserMarks, - timeout + timeout, + processMarks } from '../../utils' import { Container } from '../../components/Container' import { DisplayMeta } from './internal/displayMeta' @@ -54,6 +55,7 @@ import { } from '../../utils/file.ts' import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts' import { generateTimestamp } from '../../utils/opentimestamps.ts' +import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx' enum SignedStatus { Fully_Signed, @@ -237,6 +239,43 @@ export const SignPage = () => { const signedMarks = extractMarksFromSignedMeta(meta) const currentUserMarks = getCurrentUserMarks(metaMarks, signedMarks) const otherUserMarks = findOtherUserMarks(signedMarks, usersPubkey!) + + if (meta.keys) { + for (let i = 0; i < otherUserMarks.length; i++) { + const m = otherUserMarks[i] + const { sender, keys } = meta.keys + const usersNpub = hexToNpub(usersPubkey) + if (usersNpub in keys) { + const encryptionKey = await nostrController + .nip04Decrypt(sender, keys[usersNpub]) + .catch((err) => { + console.log( + 'An error occurred in decrypting encryption key', + err + ) + return null + }) + + try { + const { fetchAndDecrypt } = MARK_TYPE_CONFIG[m.type] || {} + if ( + typeof fetchAndDecrypt === 'function' && + m.value && + encryptionKey + ) { + const decrypted = await fetchAndDecrypt( + m.value, + encryptionKey + ) + otherUserMarks[i].value = decrypted + } + } catch (error) { + console.error(`Error during mark fetchAndDecrypt phase`, error) + } + } + } + } + setOtherUserMarks(otherUserMarks) setCurrentUserMarks(currentUserMarks) setIsMarksCompleted(isCurrentUserMarksComplete(currentUserMarks)) @@ -248,6 +287,7 @@ export const SignPage = () => { if (meta) { handleUpdatedMeta(meta) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [meta, usersPubkey]) const handleDownload = async () => { @@ -552,8 +592,8 @@ export const SignPage = () => { setIsLoading(true) setLoadingSpinnerDesc('Signing nostr event') - - const prevSig = getPrevSignersSig(hexToNpub(usersPubkey!)) + const usersNpub = hexToNpub(usersPubkey!) + const prevSig = getPrevSignersSig(usersNpub) if (!prevSig) { setIsLoading(false) toast.error('Previous signature is invalid') @@ -562,7 +602,26 @@ export const SignPage = () => { const marks = getSignerMarksForMeta() || [] - const signedEvent = await signEventForMeta({ prevSig, marks }) + let encryptionKey: string | undefined + if (meta.keys) { + const { sender, keys } = meta.keys + encryptionKey = await nostrController + .nip04Decrypt(sender, keys[usersNpub]) + .catch((err) => { + // Log and display an error message if decryption fails + console.log('An error occurred in decrypting encryption key', err) + toast.error('An error occurred in decrypting encryption key') + return undefined + }) + } + + const processedMarks = await processMarks(marks, encryptionKey) + + const signedEvent = await signEventForMeta({ + prevSig, + marks: processedMarks + }) + if (!signedEvent) return const updatedMeta = updateMetaSignatures(meta, signedEvent) diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 6ce4c83..29af7e8 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -55,7 +55,7 @@ import { } from '@fortawesome/free-solid-svg-icons' import { upgradeAndVerifyTimestamp } from '../../utils/opentimestamps.ts' import _ from 'lodash' -import { MARK_TYPE_CONFIG } from '../../components/getMarkComponents.tsx' +import { MarkRender } from '../../components/MarkTypeStrategy/MarkRender.tsx' interface PdfViewProps { files: CurrentUserFile[] @@ -115,8 +115,6 @@ const SlimPdfView = ({ alt={`page ${i} of ${file.name}`} /> {marks.map((m) => { - const { render: MarkRenderComponent } = - MARK_TYPE_CONFIG[m.type] || {} return (
- {typeof MarkRenderComponent !== 'undefined' && ( - - )} +
) })} @@ -367,82 +367,77 @@ export const VerifyPage = () => { setIsLoading(true) setLoadingSpinnerDesc('Fetching file from file server') - axios - .get(zipUrl, { + try { + const res = await axios.get(zipUrl, { responseType: 'arraybuffer' }) - .then(async (res) => { - const fileName = zipUrl.split('/').pop() - const file = new File([res.data], fileName!) - const encryptedArrayBuffer = await file.arrayBuffer() - const arrayBuffer = await decryptArrayBuffer( - encryptedArrayBuffer, - encryptionKey - ).catch((err) => { - console.log('err in decryption:>> ', err) + const fileName = zipUrl.split('/').pop() + const file = new File([res.data], fileName!) + + const encryptedArrayBuffer = await file.arrayBuffer() + const arrayBuffer = await decryptArrayBuffer( + encryptedArrayBuffer, + encryptionKey + ).catch((err) => { + console.log('err in decryption:>> ', err) + toast.error(err.message || 'An error occurred in decrypting file.') + return null + }) + + if (arrayBuffer) { + const zip = await JSZip.loadAsync(arrayBuffer).catch((err) => { + console.log('err in loading zip file :>> ', err) toast.error( - err.message || 'An error occurred in decrypting file.' + err.message || 'An error occurred in loading zip file.' ) return null }) - if (arrayBuffer) { - const zip = await JSZip.loadAsync(arrayBuffer).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 - if (!zip) return + const files: { [fileName: string]: SigitFile } = {} + const fileHashes: { [key: string]: string | null } = {} + const fileNames = Object.values(zip.files).map( + (entry) => entry.name + ) - const files: { [fileName: string]: SigitFile } = {} - const fileHashes: { [key: string]: string | null } = {} - const fileNames = Object.values(zip.files).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' ) - // generate hashes for all entries in files folder of zipArchive - // these hashes can be used to verify the originality of files - for (const fileName of fileNames) { - const arrayBuffer = await readContentOfZipEntry( - zip, - fileName, - 'arraybuffer' + if (arrayBuffer) { + files[fileName] = await convertToSigitFile( + arrayBuffer, + fileName! ) + const hash = await getHash(arrayBuffer) - if (arrayBuffer) { - files[fileName] = await convertToSigitFile( - arrayBuffer, - fileName! - ) - const hash = await getHash(arrayBuffer) - - if (hash) { - fileHashes[fileName.replace(/^files\//, '')] = hash - } - } else { - fileHashes[fileName.replace(/^files\//, '')] = null + if (hash) { + fileHashes[fileName.replace(/^files\//, '')] = hash } + } else { + fileHashes[fileName.replace(/^files\//, '')] = null } - - setCurrentFileHashes(fileHashes) - setFiles(files) - - setIsLoading(false) } - }) - .catch((err) => { - console.error(`error occurred in getting file from ${zipUrl}`, err) - toast.error( - err.message || `error occurred in getting file from ${zipUrl}` - ) - }) - .finally(() => { + + setCurrentFileHashes(fileHashes) + setFiles(files) setIsLoading(false) - }) + } + } catch (err) { + const message = `error occurred in getting file from ${zipUrl}` + console.error(message, err) + if (err instanceof Error) toast.error(err.message) + else toast.error(message) + } finally { + setIsLoading(false) + } } processSigit() diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 97d66c3..f3580f9 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,16 +1,4 @@ -import { CreatePage } from '../pages/create' -import { HomePage } from '../pages/home' -import { LandingPage } from '../pages/landing' -import { ProfilePage } from '../pages/profile' -import { SettingsPage } from '../pages/settings/Settings' -import { CacheSettingsPage } from '../pages/settings/cache' -import { NostrLoginPage } from '../pages/settings/nostrLogin' -import { ProfileSettingsPage } from '../pages/settings/profile' -import { RelaysPage } from '../pages/settings/relays' -import { SignPage } from '../pages/sign' -import { VerifyPage } from '../pages/verify' import { hexToNpub } from '../utils' -import { Route, RouteProps } from 'react-router-dom' export const appPrivateRoutes = { homePage: '/', @@ -39,93 +27,3 @@ export const getProfileRoute = (hexKey: string) => export const getProfileSettingsRoute = (hexKey: string) => appPrivateRoutes.profileSettings.replace(':npub', hexToNpub(hexKey)) - -/** - * Helper type allows for extending react-router-dom's **RouteProps** with generic type - */ -type CustomRouteProps = T & - Omit & { - children?: Array> - } - -/** - * This function maps over nested routes with optional condition for rendering - * @param {CustomRouteProps[]} routes - routes list - * @param {RenderConditionCallbackFn} renderConditionCallbackFn - render condition callback (default true) - */ -export function recursiveRouteRenderer( - routes?: CustomRouteProps[], - renderConditionCallbackFn: (route: CustomRouteProps) => boolean = () => - true -) { - if (!routes) return null - - // Callback allows us to pass arbitrary conditions for each route's rendering - // Skipping the callback will by default evaluate to true (show route) - return routes.map((route, index) => - renderConditionCallbackFn(route) ? ( - - {recursiveRouteRenderer(route.children, renderConditionCallbackFn)} - - ) : null - ) -} - -type PublicRouteProps = CustomRouteProps<{ - hiddenWhenLoggedIn?: boolean -}> - -export const publicRoutes: PublicRouteProps[] = [ - { - path: appPublicRoutes.landingPage, - hiddenWhenLoggedIn: true, - element: - }, - { - path: appPublicRoutes.profile, - element: - }, - { - path: appPublicRoutes.verify, - element: - } -] - -export const privateRoutes = [ - { - path: appPrivateRoutes.homePage, - element: - }, - { - path: appPrivateRoutes.create, - element: - }, - { - path: `${appPrivateRoutes.sign}/:id?`, - element: - }, - { - path: appPrivateRoutes.settings, - element: - }, - { - path: appPrivateRoutes.profileSettings, - element: - }, - { - path: appPrivateRoutes.cacheSettings, - element: - }, - { - path: appPrivateRoutes.relays, - element: - }, - { - path: appPrivateRoutes.nostrLogin, - element: - } -] diff --git a/src/routes/util.tsx b/src/routes/util.tsx new file mode 100644 index 0000000..efde6c5 --- /dev/null +++ b/src/routes/util.tsx @@ -0,0 +1,103 @@ +import { Route, RouteProps } from 'react-router-dom' +import { appPrivateRoutes, appPublicRoutes } from '.' +import { CreatePage } from '../pages/create' +import { HomePage } from '../pages/home' +import { LandingPage } from '../pages/landing' +import { ProfilePage } from '../pages/profile' +import { CacheSettingsPage } from '../pages/settings/cache' +import { NostrLoginPage } from '../pages/settings/nostrLogin' +import { ProfileSettingsPage } from '../pages/settings/profile' +import { RelaysPage } from '../pages/settings/relays' +import { SettingsPage } from '../pages/settings/Settings' +import { SignPage } from '../pages/sign' +import { VerifyPage } from '../pages/verify' + +/** + * Helper type allows for extending react-router-dom's **RouteProps** with generic type + */ +type CustomRouteProps = T & + Omit & { + children?: Array> + } + +/** + * This function maps over nested routes with optional condition for rendering + * @param {CustomRouteProps[]} routes - routes list + * @param {RenderConditionCallbackFn} renderConditionCallbackFn - render condition callback (default true) + */ +export function recursiveRouteRenderer( + routes?: CustomRouteProps[], + renderConditionCallbackFn: (route: CustomRouteProps) => boolean = () => + true +) { + if (!routes) return null + + // Callback allows us to pass arbitrary conditions for each route's rendering + // Skipping the callback will by default evaluate to true (show route) + return routes.map((route, index) => + renderConditionCallbackFn(route) ? ( + + {recursiveRouteRenderer(route.children, renderConditionCallbackFn)} + + ) : null + ) +} + +type PublicRouteProps = CustomRouteProps<{ + hiddenWhenLoggedIn?: boolean +}> + +export const publicRoutes: PublicRouteProps[] = [ + { + path: appPublicRoutes.landingPage, + hiddenWhenLoggedIn: true, + element: + }, + { + path: appPublicRoutes.profile, + element: + }, + { + path: appPublicRoutes.verify, + element: + } +] + +export const privateRoutes = [ + { + path: appPrivateRoutes.homePage, + element: + }, + { + path: appPrivateRoutes.create, + element: + }, + { + path: `${appPrivateRoutes.sign}/:id?`, + element: + }, + { + path: appPrivateRoutes.settings, + element: + }, + { + path: appPrivateRoutes.profileSettings, + element: + }, + { + path: appPrivateRoutes.cacheSettings, + element: + }, + { + path: appPrivateRoutes.relays, + element: + }, + { + path: appPrivateRoutes.nostrLogin, + element: + } +] diff --git a/src/types/mark.ts b/src/types/mark.ts index ec1c162..df733d6 100644 --- a/src/types/mark.ts +++ b/src/types/mark.ts @@ -28,24 +28,3 @@ export interface MarkRect { width: number height: number } - -export interface MarkInputProps { - value: string - handler: (value: string) => void - placeholder?: string - userMark?: CurrentUserMark -} - -export interface MarkRenderProps { - value?: string - mark: Mark -} - -export interface MarkConfig { - input: React.FC - render?: React.FC -} - -export type MarkConfigs = { - [key in MarkType]?: MarkConfig -} diff --git a/src/utils/const.ts b/src/utils/const.ts index 38f138e..bf38404 100644 --- a/src/utils/const.ts +++ b/src/utils/const.ts @@ -112,3 +112,13 @@ export const MOST_COMMON_MEDIA_TYPES = new Map([ ['3g2', 'video/3gpp2'], // 3GPP2 audio/video container ['7z', 'application/x-7z-compressed'] // 7-zip archive ]) + +export const SIGNATURE_PAD_OPTIONS = { + minWidth: 0.5, + maxWidth: 3 +} as const + +export const SIGNATURE_PAD_SIZE = { + width: 600, + height: 300 +} diff --git a/src/utils/file.ts b/src/utils/file.ts index c08d5e7..e8c4e33 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -1,7 +1,11 @@ +import { MARK_TYPE_CONFIG } from '../components/MarkTypeStrategy/MarkStrategy.tsx' +import { NostrController } from '../controllers/NostrController.ts' +import store from '../store/store.ts' import { Meta } from '../types' import { PdfPage } from '../types/drawing.ts' import { MOST_COMMON_MEDIA_TYPES } from './const.ts' import { extractMarksFromSignedMeta } from './mark.ts' +import { hexToNpub } from './nostr.ts' import { addMarks, groupMarksByFileNamePage, @@ -21,7 +25,49 @@ export const getZipWithFiles = async ( for (const [fileName, file] of Object.entries(files)) { // Handle PDF Files, add marks if (file.isPdf && fileName in marksByFileNamePage) { - const blob = await addMarks(file, marksByFileNamePage[fileName]) + const marksToAdd = marksByFileNamePage[fileName] + if (meta.keys) { + for (let i = 0; i < marks.length; i++) { + const m = marks[i] + const { sender, keys } = meta.keys + const usersPubkey = store.getState().auth.usersPubkey! + const usersNpub = hexToNpub(usersPubkey) + if (usersNpub in keys) { + const encryptionKey = await NostrController.getInstance() + .nip04Decrypt(sender, keys[usersNpub]) + .catch((err) => { + console.log( + 'An error occurred in decrypting encryption key', + err + ) + return null + }) + + try { + const { fetchAndDecrypt } = MARK_TYPE_CONFIG[m.type] || {} + if ( + typeof fetchAndDecrypt === 'function' && + m.value && + encryptionKey + ) { + // Fetch and decrypt the original file + const link = m.value.split('/') + const decrypted = await fetchAndDecrypt(m.value, encryptionKey) + + // Save decrypted + zip.file( + `signatures/${link[link.length - 1]}.json`, + new Blob([decrypted]) + ) + marks[i].value = decrypted + } + } catch (error) { + console.error(`Error during mark fetchAndDecrypt phase`, error) + } + } + } + } + const blob = await addMarks(file, marksToAdd) zip.file(`marked/${fileName}`, blob) } diff --git a/src/utils/index.ts b/src/utils/index.ts index accc008..791c39b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -11,3 +11,4 @@ export * from './string' export * from './url' export * from './utils' export * from './zip' +export * from './const' diff --git a/src/utils/mark.ts b/src/utils/mark.ts index 4eca3a8..2c0b339 100644 --- a/src/utils/mark.ts +++ b/src/utils/mark.ts @@ -1,4 +1,4 @@ -import { CurrentUserMark, Mark, MarkLocation } from '../types/mark.ts' +import { CurrentUserMark, Mark } from '../types/mark.ts' import { hexToNpub } from './nostr.ts' import { Meta, SignedEventContent } from '../types' import { Event } from 'nostr-tools' @@ -24,7 +24,7 @@ import { faStamp, faTableCellsLarge } from '@fortawesome/free-solid-svg-icons' -import { Config, optimize } from 'svgo' +import { MARK_TYPE_CONFIG } from '../components/MarkTypeStrategy/MarkStrategy.tsx' /** * Takes in an array of Marks already filtered by User. @@ -266,22 +266,38 @@ export const getToolboxLabelByMarkType = (markType: MarkType) => { return DEFAULT_TOOLBOX.find((t) => t.identifier === markType)?.label } -export const optimizeSVG = (location: MarkLocation, paths: string[]) => { - const svgContent = `${paths.map((path) => ``).join('')}` - const optimizedSVG = optimize(svgContent, { - multipass: true, // Optimize multiple times if needed - floatPrecision: 2 // Adjust precision - } as Config) +export const getOptimizedPathsWithStrokeWidth = (svgString: string) => { + const parser = new DOMParser() + const xmlDoc = parser.parseFromString(svgString, 'image/svg+xml') + const paths = xmlDoc.querySelectorAll('path') + const tuples: string[][] = [] + paths.forEach((path) => { + const d = path.getAttribute('d') ?? '' + const strokeWidth = path.getAttribute('stroke-width') ?? '' + tuples.push([d, strokeWidth]) + }) - return optimizedSVG.data + return tuples } -export const getOptimizedPaths = (svgString: string) => { - const regex = / d="([^"]*)"/g - const matches = [...svgString.matchAll(regex)] - const pathValues = matches.map((match) => match[1]) +export const processMarks = async (marks: Mark[], encryptionKey?: string) => { + const _marks = [...marks] + for (let i = 0; i < _marks.length; i++) { + const mark = _marks[i] + const hasProcess = + mark.type in MARK_TYPE_CONFIG && + typeof MARK_TYPE_CONFIG[mark.type]?.encryptAndUpload === 'function' - return pathValues + if (hasProcess) { + const value = mark.value! + const processFn = MARK_TYPE_CONFIG[mark.type]?.encryptAndUpload + if (processFn) { + mark.value = await processFn(value, encryptionKey) + } + } + } + + return _marks } export { diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts index b66bc73..88cdc67 100644 --- a/src/utils/pdf.ts +++ b/src/utils/pdf.ts @@ -11,6 +11,9 @@ if (!PDFJS.GlobalWorkerOptions.workerPort) { import fontkit from '@pdf-lib/fontkit' import defaultFont from '../assets/fonts/roboto-regular.ttf' +import { BasicPoint } from 'signature_pad/dist/types/point' +import SignaturePad from 'signature_pad' +import { SIGNATURE_PAD_OPTIONS, SIGNATURE_PAD_SIZE } from './const.ts' /** * Defined font size used when generating a PDF. Currently it is difficult to fully @@ -132,17 +135,18 @@ export const addMarks = async ( for (let i = 0; i < pages.length; i++) { if (marksPerPage && Object.hasOwn(marksPerPage, i)) { - marksPerPage[i]?.forEach((mark) => { + for (let j = 0; j < marksPerPage[i].length; j++) { + const mark = marksPerPage[i][j] switch (mark.type) { case MarkType.SIGNATURE: - drawSignatureText(mark, pages[i]) + await embedSignaturePng(mark, pages[i], pdf) break default: drawMarkText(mark, pages[i], robotoFont) break } - }) + } } } @@ -254,18 +258,41 @@ async function embedFont(pdf: PDFDocument) { return embeddedFont } -const drawSignatureText = (mark: Mark, page: PDFPage) => { +const embedSignaturePng = async ( + mark: Mark, + page: PDFPage, + pdf: PDFDocument +) => { const { location } = mark const { height } = page.getSize() - // Convert the mark location origin (top, left) to PDF origin (bottom, left) - const x = location.left - const y = height - location.top - if (hasValue(mark)) { - const paths = JSON.parse(mark.value!) - paths.forEach((d: string) => { - page.drawSvgPath(d, { x, y }) + const data = JSON.parse(mark.value!).map((p: BasicPoint[]) => ({ + points: p + })) + const canvas = document.createElement('canvas') + canvas.width = SIGNATURE_PAD_SIZE.width + canvas.height = SIGNATURE_PAD_SIZE.height + const pad = new SignaturePad(canvas, SIGNATURE_PAD_OPTIONS) + pad.fromData(data) + const signatureImage = await pdf.embedPng(pad.toDataURL()) + + const scaled = signatureImage.scaleToFit(location.width, location.height) + + // Convert the mark location origin (top, left) to PDF origin (bottom, left) + // and center the image + const x = location.left + (location.width - scaled.width) / 2 + const y = + height - + location.top - + location.height + + (location.height - scaled.height) / 2 + + page.drawImage(signatureImage, { + x, + y, + width: scaled.width, + height: scaled.height }) } }