From 9551750cbe0d84abc983e8746dcf67aedf99c525 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 15 Nov 2024 17:51:11 +0100 Subject: [PATCH] feat(signature): signature pad encrypt, upload, fetch, decrypt, render, add to pdf --- package-lock.json | 20 ++++- package.json | 1 - src/components/PDFView/PdfPageItem.tsx | 6 +- src/components/getMarkComponents.tsx | 78 ++++++++++++++++- src/hooks/useSigitMeta.tsx | 34 +++++++- src/pages/sign/index.tsx | 67 ++++++++++++++- src/pages/verify/index.tsx | 111 ++++++++++++------------- src/types/mark.ts | 4 +- src/utils/file.ts | 44 +++++++++- src/utils/mark.ts | 21 +++++ src/utils/pdf.ts | 51 ++++++++---- 11 files changed, 352 insertions(+), 85 deletions(-) diff --git a/package-lock.json b/package-lock.json index 71ab84b..a03e759 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,6 @@ "react-toastify": "10.0.4", "redux": "5.0.1", "signature_pad": "^5.0.4", - "svgo": "^3.3.2", "tseep": "1.2.1" }, "devDependencies": { @@ -2215,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" @@ -2936,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": { @@ -3635,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", @@ -3651,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", @@ -3664,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" @@ -3676,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" @@ -3689,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", @@ -3703,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": { @@ -3950,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", @@ -3977,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", @@ -3989,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" @@ -4004,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", @@ -4053,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" @@ -5980,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": { @@ -6561,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" @@ -6920,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": { @@ -7978,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" @@ -8204,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", @@ -8229,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 02e1946..ab49da0 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "react-toastify": "10.0.4", "redux": "5.0.1", "signature_pad": "^5.0.4", - "svgo": "^3.3.2", "tseep": "1.2.1" }, "devDependencies": { diff --git a/src/components/PDFView/PdfPageItem.tsx b/src/components/PDFView/PdfPageItem.tsx index 5d4be7d..9f56550 100644 --- a/src/components/PDFView/PdfPageItem.tsx +++ b/src/components/PDFView/PdfPageItem.tsx @@ -6,6 +6,7 @@ import { useEffect, useRef } from 'react' import pdfViewStyles from './style.module.scss' import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts' import { useScale } from '../../hooks/useScale.tsx' +import { MARK_TYPE_CONFIG } from '../getMarkComponents.tsx' interface PdfPageProps { fileName: string pageIndex: number @@ -60,6 +61,7 @@ const PdfPageItem = ({ /> ))} {otherUserMarks.map((m, i) => { + const { render: MarkRenderComponent } = MARK_TYPE_CONFIG[m.type] || {} return (
- {m.value} + {typeof MarkRenderComponent !== 'undefined' && ( + + )}
) })} diff --git a/src/components/getMarkComponents.tsx b/src/components/getMarkComponents.tsx index 507f388..e81bdd4 100644 --- a/src/components/getMarkComponents.tsx +++ b/src/components/getMarkComponents.tsx @@ -1,8 +1,17 @@ +import { toast } from 'react-toastify' import { MarkType } from '../types/drawing' import { MarkConfigs } from '../types/mark' +import { + decryptArrayBuffer, + encryptArrayBuffer, + getHash, + isOnline, + uploadToFileStorage +} from '../utils' import { MarkInputSignature } from './MarkInputs/Signature' import { MarkInputText } from './MarkInputs/Text' import { MarkRenderSignature } from './MarkRender/Signature' +import axios from 'axios' export const MARK_TYPE_CONFIG: MarkConfigs = { [MarkType.TEXT]: { @@ -11,6 +20,73 @@ export const MARK_TYPE_CONFIG: MarkConfigs = { }, [MarkType.SIGNATURE]: { input: MarkInputSignature, - render: MarkRenderSignature + render: MarkRenderSignature, + encryptAndUpload: async (value, encryptionKey) => { + // Value is the stringified signature object + // Encode it as text to the arrayBuffer + const encoder = new TextEncoder() + const uint8Array = encoder.encode(value) + const hash = await getHash(uint8Array) + + if (!hash) { + throw new Error("Can't get file hash.") + } + + if (!encryptionKey) { + throw new Error('Signature requires an encryption key') + } + + // Encrypt the file contents with the same encryption key from the create signature + const encryptedArrayBuffer = await encryptArrayBuffer( + uint8Array, + encryptionKey + ) + + // Create the encrypted json file from array buffer and hash + const file = new File([encryptedArrayBuffer], `${hash}.json`) + + if (await isOnline()) { + try { + const url = await uploadToFileStorage(file) + toast.success('files.zip uploaded to file storage') + return url + } catch (error) { + if (error instanceof Error) { + toast.error(error.message || 'Error occurred in uploading file') + } + } + } else { + // Handle offline? + } + + return value + }, + fetchAndDecrypt: async (value, encryptionKey) => { + if (!encryptionKey) { + throw new Error('Signature requires an encryption key') + } + + const encryptedArrayBuffer = await axios.get(value, { + responseType: 'arraybuffer' + }) + + const arrayBuffer = await decryptArrayBuffer( + encryptedArrayBuffer.data, + encryptionKey + ).catch((err) => { + console.log('err in decryption:>> ', err) + return null + }) + + if (arrayBuffer) { + // decode json + const decoder = new TextDecoder() + const value = decoder.decode(arrayBuffer) + return value + } + + // Handle offline? + return value + } } } diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx index 8f79dc6..d37ae1e 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/getMarkComponents' /** * 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..847ce2a 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/getMarkComponents.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..7eafc78 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -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/types/mark.ts b/src/types/mark.ts index ec1c162..0a54d3e 100644 --- a/src/types/mark.ts +++ b/src/types/mark.ts @@ -43,7 +43,9 @@ export interface MarkRenderProps { export interface MarkConfig { input: React.FC - render?: React.FC + render: React.FC + encryptAndUpload?: (value: string, key?: string) => Promise + fetchAndDecrypt?: (value: string, key?: string) => Promise } export type MarkConfigs = { diff --git a/src/utils/file.ts b/src/utils/file.ts index c08d5e7..38dd0f1 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -1,7 +1,11 @@ +import { MARK_TYPE_CONFIG } from '../components/getMarkComponents.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,45 @@ 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 + ) { + // TODO + // extract draw with proper values + // save both pdf with marking and original hash files for signature + // ... + const decrypted = await fetchAndDecrypt(m.value, encryptionKey) + 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/mark.ts b/src/utils/mark.ts index a5eb974..061f3f0 100644 --- a/src/utils/mark.ts +++ b/src/utils/mark.ts @@ -24,6 +24,7 @@ import { faStamp, faTableCellsLarge } from '@fortawesome/free-solid-svg-icons' +import { MARK_TYPE_CONFIG } from '../components/getMarkComponents.tsx' /** * Takes in an array of Marks already filtered by User. @@ -279,6 +280,26 @@ export const getOptimizedPathsWithStrokeWidth = (svgString: string) => { return tuples } +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' + + 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 { getCurrentUserMarks, filterMarksByPubkey, diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts index 41e1d02..88cdc67 100644 --- a/src/utils/pdf.ts +++ b/src/utils/pdf.ts @@ -1,5 +1,5 @@ import { MarkType, PdfPage } from '../types/drawing.ts' -import { LineCapStyle, PDFDocument, PDFFont, PDFPage, rgb } from 'pdf-lib' +import { PDFDocument, PDFFont, PDFPage, rgb } from 'pdf-lib' import { Mark } from '../types/mark.ts' import * as PDFJS from 'pdfjs-dist' import PDFJSWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?worker' @@ -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 @@ -136,7 +139,7 @@ export const addMarks = async ( const mark = marksPerPage[i][j] switch (mark.type) { case MarkType.SIGNATURE: - drawSignatureText(mark, pages[i]) + await embedSignaturePng(mark, pages[i], pdf) break default: @@ -255,23 +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 segments: string[][] = JSON.parse(mark.value!) - segments.forEach(([d, w]) => { - page.drawSvgPath(d, { - x, - y, - borderWidth: parseFloat(w), - borderLineCap: LineCapStyle.Round - }) + 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 }) } }