From 6f8830a77ccf40eed0c11ccc65ae5d736dfb981d Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Sat, 8 Jun 2024 00:37:03 +0500 Subject: [PATCH 01/35] fix: home screen style fixed for mobile view --- src/pages/home/index.tsx | 124 +++++++++++++++++++++++-------- src/pages/home/style.module.scss | 6 +- 2 files changed, 95 insertions(+), 35 deletions(-) diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index 0afb321..9cf4508 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -5,7 +5,7 @@ import { PersonOutline, Upload } 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 { appPrivateRoutes } from '../../routes' import styles from './style.module.scss' @@ -19,7 +19,16 @@ export const HomePage = () => { Sigits - + {/* This is for desktop view */} + + {/* This is for mobile view */} + + + + + + + + + + + + + - - - ) } const PlaceHolder = () => { return ( - - - - - - Title - - - - Sigit - - - - 07 Jun 10:23 AM + + + + + Title + + + + Sigit + + + + 07 Jun 10:23 AM + + + + + + Sent + placeholder@sigit.io - - - - Sent - - placeholder@sigit.io - - - - Awaiting - - placeholder@sigit.io - + + + Awaiting + + placeholder@sigit.io diff --git a/src/pages/home/style.module.scss b/src/pages/home/style.module.scss index 69c5019..e722d70 100644 --- a/src/pages/home/style.module.scss +++ b/src/pages/home/style.module.scss @@ -15,7 +15,6 @@ } .actionButtons { - display: flex; justify-content: center; align-items: center; gap: 10px; @@ -25,6 +24,7 @@ .submissions { display: flex; flex-direction: column; + gap: 10px; .item { display: flex; @@ -33,10 +33,10 @@ .titleBox { display: flex; - flex-direction: column; align-items: flex-start; + justify-content: space-between; padding: 10px; - background-color: #e7e2df99; + background-color: #cdc8c499; border-top-left-radius: inherit; border-bottom-left-radius: inherit; -- 2.34.1 From c530abd298e9340169b7bde8cae160d9c579bb63 Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Mon, 10 Jun 2024 18:10:43 +0500 Subject: [PATCH 02/35] chore(refactor): refactor handle create function --- src/pages/create/index.tsx | 298 ++++++++++++++++++++++++------------- 1 file changed, 194 insertions(+), 104 deletions(-) diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 3789411..98525a4 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -221,57 +221,67 @@ export const CreatePage = () => { ) } - const handleCreate = async () => { + // Validate inputs before proceeding + const validateInputs = (): boolean => { if (!title.trim()) { toast.error('Title can not be empty') - return + return false } if (users.length === 0) { 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) { toast.error('No file is selected. Select at least 1 file') - return + return false } - setIsLoading(true) - setLoadingSpinnerDesc('Generating hashes for files') + return true + } + // Handle errors during file arrayBuffer conversion + const handleFileError = (file: File) => (err: any) => { + console.log( + `Error while getting arrayBuffer of file ${file.name} :>> `, + err + ) + toast.error( + err.message || `Error while getting arrayBuffer of file ${file.name}` + ) + return null + } + + // Generate hash for each selected file + const generateFileHashes = async (): Promise<{ + [key: string]: string + } | null> => { const fileHashes: { [key: string]: string } = {} - // generating file hashes for (const file of selectedFiles) { - const arraybuffer = await file.arrayBuffer().catch((err) => { - console.log( - `err while getting arrayBuffer of file ${file.name} :>> `, - err - ) - toast.error( - err.message || `err while getting arrayBuffer of file ${file.name}` - ) - return null - }) - - if (!arraybuffer) return + const arraybuffer = await file.arrayBuffer().catch(handleFileError(file)) + if (!arraybuffer) return null const hash = await getHash(arraybuffer) - if (!hash) { - setIsLoading(false) - return + return null } 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() - // zipping files selectedFiles.forEach((file) => { zip.file(`files/${file.name}`, file) }) @@ -280,6 +290,7 @@ export const CreatePage = () => { const viewers = users.filter((user) => user.role === UserRole.viewer) setLoadingSpinnerDesc('Signing nostr event') + const createSignature = await signEventForMetaFile( JSON.stringify({ signers: signers.map((signer) => hexToNpub(signer.pubkey)), @@ -290,12 +301,27 @@ export const CreatePage = () => { 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 => { // create content for meta file const meta: Meta = { title, - createSignature: JSON.stringify(createSignature, null, 2), + createSignature, docSignatures: {} } @@ -304,112 +330,176 @@ export const CreatePage = () => { zip.file('meta.json', stringifiedMeta) const metaHash = await getHash(stringifiedMeta) - if (!metaHash) return + if (!metaHash) return null const metaHashJson = { [usersPubkey!]: metaHash } zip.file('hashes.json', JSON.stringify(metaHashJson, null, 2)) + return metaHash } catch (err) { console.error(err) 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 => { setLoadingSpinnerDesc('Generating zip file') const arraybuffer = await zip .generateAsync({ type: 'arraybuffer', compression: 'DEFLATE', - compressionOptions: { - level: 6 - } - }) - .catch((err) => { - console.log('err in zip:>> ', err) - setIsLoading(false) - toast.error(err.message || 'Error occurred in generating zip file') - return null + compressionOptions: { level: 6 } }) + .catch(handleZipError) + return arraybuffer + } + + // Encrypt the zip file with the generated encryption key + const encryptZipFile = async ( + arraybuffer: ArrayBuffer, + encryptionKey: string + ): Promise => { + setLoadingSpinnerDesc('Encrypting zip file') + return encryptArrayBuffer(arraybuffer, encryptionKey).finally(() => + setIsLoading(false) + ) + } + + // Handle file upload and further actions based on online/offline status + const handleFileUpload = async (blob: Blob, encryptionKey: string) => { + if (await isOnline()) { + const fileUrl = await uploadFile(blob) + + if (!fileUrl) return + + await sendDMs(fileUrl, encryptionKey) + setIsLoading(false) + + navigate( + `${appPrivateRoutes.sign}?file=${encodeURIComponent(fileUrl)}&key=${encodeURIComponent(encryptionKey)}` + ) + } else { + handleOffline(blob, 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 zip file') + return null + } + + // Upload the file to the storage and send DMs to signers/viewers + const uploadFile = async (blob: Blob): Promise => { + setIsLoading(true) + 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(handleUploadError) + + return fileUrl + } + + // Send DMs to signers and viewers with the file URL and encryption key + const sendDMs = async (fileUrl: string, encryptionKey: string) => { + setLoadingSpinnerDesc('Sending DM to signers/viewers') + + const signers = users.filter((user) => user.role === UserRole.signer) + const viewers = users.filter((user) => user.role === UserRole.viewer) + + if (signers.length > 0) { + await sendDM( + fileUrl, + encryptionKey, + signers[0].pubkey, + nostrController, + true, + setAuthUrl + ) + } else { + for (const viewer of viewers) { + await sendDM( + fileUrl, + encryptionKey, + viewer.pubkey, + nostrController, + false, + setAuthUrl + ) + } + } + } + + // Manage offline scenarios for signing or viewing the file + const handleOffline = (blob: Blob, encryptionKey: string) => { + const signers = users.filter((user) => user.role === UserRole.signer) + + if (signers[0] && signers[0].pubkey === usersPubkey) { + // Create a File object with the Blob data for offline signing + const file = new File([blob], `compressed.sigit`, { + type: 'application/sigit' + }) + navigate(appPrivateRoutes.sign, { state: { file, encryptionKey } }) + } else { + // Save the file and show encryption key for offline viewing + saveAs(blob, 'request.sigit') + setTextToCopy(encryptionKey) + setOpenCopyModel(true) + } + } + + 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 encryptArrayBuffer( + const encryptedArrayBuffer = await encryptZipFile( arraybuffer, encryptionKey - ).finally(() => setIsLoading(false)) - + ) const blob = new Blob([encryptedArrayBuffer]) - if (await isOnline()) { - setIsLoading(true) - 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 - }) - - if (!fileUrl) return - - setLoadingSpinnerDesc('Sending DM to signers/viewers') - - // send DM to first signer if exists - if (signers.length > 0) { - await sendDM( - fileUrl, - encryptionKey, - signers[0].pubkey, - nostrController, - true, - setAuthUrl - ) - } else { - // send DM to all viewers if no signer - for (const viewer of viewers) { - // todo: execute in parallel - await sendDM( - fileUrl, - encryptionKey, - viewer.pubkey, - nostrController, - false, - setAuthUrl - ) - } - } - setIsLoading(false) - - navigate( - `${appPrivateRoutes.sign}?file=${encodeURIComponent( - fileUrl - )}&key=${encodeURIComponent(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 } }) - } else { - saveAs(blob, 'request.sigit') - setTextToCopy(encryptionKey) - setOpenCopyModel(true) - } - } + return await handleFileUpload(blob, encryptionKey) } if (authUrl) { -- 2.34.1 From b145624f4cf34c86cbde5c9d0aff76c00547bf27 Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Tue, 11 Jun 2024 16:49:10 +0500 Subject: [PATCH 03/35] chore(refactor): break handle sign function into smaller chunks --- src/pages/sign/index.tsx | 298 +++++++++++++++++++++++---------------- 1 file changed, 179 insertions(+), 119 deletions(-) diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 1833c77..7ffc1ec 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -34,6 +34,7 @@ import { CreateSignatureEventContent, Meta, ProfileMetadata, + SignedEvent, SignedEventContent, User, UserRole @@ -366,26 +367,15 @@ export const SignPage = () => { setIsLoading(true) setLoadingSpinnerDesc('parsing hashes.json file') - const hashesFileContent = await readContentOfZipEntry( - zip, - 'hashes.json', - 'string' - ) + const hashesFileContent = await readHashesFile() + if (!hashesFileContent) return if (!hashesFileContent) { setIsLoading(false) return } - let hashes = await parseJson(hashesFileContent).catch((err) => { - console.log('err in parsing the content of hashes.json :>> ', err) - toast.error( - err.message || 'error occurred in parsing the content of hashes.json' - ) - setIsLoading(false) - return null - }) - + const hashes = await parseHashes(hashesFileContent) if (!hashes) return setLoadingSpinnerDesc('Generating hashes for files') @@ -395,51 +385,21 @@ export const SignPage = () => { const prevSig = getPrevSignersSig(hexToNpub(usersPubkey!)) if (!prevSig) return - const signedEvent = await signEventForMetaFile( - JSON.stringify({ - prevSig - }), - nostrController, - setIsLoading - ) - + const signedEvent = await signEventForMeta(prevSig) if (!signedEvent) return - const metaCopy = _.cloneDeep(meta) + const updatedMeta = updateMetaSignatures(meta, signedEvent) - metaCopy.docSignatures = { - ...metaCopy.docSignatures, - [hexToNpub(signedEvent.pubkey)]: JSON.stringify(signedEvent, null, 2) - } - - const stringifiedMeta = JSON.stringify(metaCopy, null, 2) + const stringifiedMeta = JSON.stringify(updatedMeta, null, 2) zip.file('meta.json', stringifiedMeta) const metaHash = await getHash(stringifiedMeta) if (!metaHash) return - hashes = { - ...hashes, - [usersPubkey!]: metaHash - } - - zip.file('hashes.json', JSON.stringify(hashes, null, 2)) - - const arrayBuffer = await zip - .generateAsync({ - type: 'arraybuffer', - compression: 'DEFLATE', - compressionOptions: { - level: 6 - } - }) - .catch((err) => { - console.log('err in zip:>> ', err) - setIsLoading(false) - toast.error(err.message || 'Error occurred in generating zip file') - return null - }) + const updatedHashes = updateHashes(hashes, metaHash) + zip.file('hashes.json', JSON.stringify(updatedHashes, null, 2)) + const arrayBuffer = await generateZipArrayBuffer(zip) if (!arrayBuffer) return const key = await generateEncryptionKey() @@ -450,80 +410,180 @@ export const SignPage = () => { const blob = new Blob([encryptedArrayBuffer]) if (await isOnline()) { - 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 - }) - - if (!fileUrl) return - - // check if the current user is the last signer - const usersNpub = hexToNpub(usersPubkey!) - const lastSignerIndex = signers.length - 1 - const signerIndex = signers.indexOf(usersNpub) - const isLastSigner = signerIndex === lastSignerIndex - - // if current user is the last signer, then send DMs to all signers and viewers - if (isLastSigner) { - const userSet = new Set<`npub1${string}`>() - - if (submittedBy) { - userSet.add(hexToNpub(submittedBy)) - } - - signers.forEach((signer) => { - userSet.add(signer) - }) - - viewers.forEach((viewer) => { - userSet.add(viewer) - }) - - const users = Array.from(userSet) - - for (const user of users) { - // todo: execute in parallel - await sendDM( - fileUrl, - key, - npubToHex(user)!, - nostrController, - false, - setAuthUrl - ) - } - } else { - const nextSigner = signers[signerIndex + 1] - await sendDM( - fileUrl, - key, - npubToHex(nextSigner)!, - nostrController, - true, - setAuthUrl - ) - } - - setIsLoading(false) - - // update search params with updated file url and encryption key - setSearchParams({ - file: fileUrl, - key: key - }) + await handleOnlineFlow(blob, key) } else { handleDecryptedArrayBuffer(arrayBuffer).finally(() => setIsLoading(false)) } } + // Read the content of the hashes.json file + const readHashesFile = async (): Promise => { + return await readContentOfZipEntry(zip!, 'hashes.json', 'string').catch( + (err) => { + console.log('Error reading hashes.json file:', err) + setIsLoading(false) + return null + } + ) + } + + // Parse the JSON content of the hashes file + const parseHashes = async ( + hashesFileContent: string + ): Promise | null> => { + return await parseJson>(hashesFileContent).catch( + (err) => { + console.log('Error parsing hashes.json content:', err) + toast.error(err.message || 'Error parsing hashes.json content') + setIsLoading(false) + return null + } + ) + } + + // Sign the event for the meta file + const signEventForMeta = async (prevSig: string) => { + return await signEventForMetaFile( + JSON.stringify({ prevSig }), + nostrController, + setIsLoading + ) + } + + // Update the meta signatures + const updateMetaSignatures = (meta: Meta, signedEvent: SignedEvent): Meta => { + const metaCopy = _.cloneDeep(meta) + metaCopy.docSignatures = { + ...metaCopy.docSignatures, + [hexToNpub(signedEvent.pubkey)]: JSON.stringify(signedEvent, null, 2) + } + return metaCopy + } + + // Update the hashes with the new meta hash + const updateHashes = ( + hashes: Record, + metaHash: string + ): Record => { + return { + ...hashes, + [usersPubkey!]: metaHash + } + } + + // Generate the zip array buffer + const generateZipArrayBuffer = async ( + zip: JSZip + ): Promise => { + return await zip + .generateAsync({ + type: 'arraybuffer', + compression: 'DEFLATE', + compressionOptions: { + level: 6 + } + }) + .catch((err) => { + console.log('Error generating zip file:', err) + setIsLoading(false) + toast.error(err.message || 'Error generating zip file') + return null + }) + } + + // Handle the online flow: upload file and send DMs + const handleOnlineFlow = async (blob: Blob, key: string) => { + const fileUrl = await uploadZipFile(blob) + if (!fileUrl) return + + const isLastSigner = checkIsLastSigner(signers) + + if (isLastSigner) { + await sendDMToAllUsers(fileUrl, key) + } else { + await sendDMToNextSigner(fileUrl, key) + } + + setIsLoading(false) + + // Update search params with updated file URL and encryption key + setSearchParams({ + file: fileUrl, + key: key + }) + } + + // Upload the zip file to file storage + const uploadZipFile = async (blob: Blob): Promise => { + 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('Error uploading file:', err) + setIsLoading(false) + toast.error(err.message || 'Error uploading file') + return null + }) + + return fileUrl + } + + // Check if the current user is the last signer + const checkIsLastSigner = (signers: string[]): boolean => { + const usersNpub = hexToNpub(usersPubkey!) + const lastSignerIndex = signers.length - 1 + const signerIndex = signers.indexOf(usersNpub) + return signerIndex === lastSignerIndex + } + + // Send DM to all users (signers and viewers) + const sendDMToAllUsers = async (fileUrl: string, key: string) => { + const userSet = new Set<`npub1${string}`>() + + if (submittedBy) { + userSet.add(hexToNpub(submittedBy)) + } + + signers.forEach((signer) => { + userSet.add(signer) + }) + + viewers.forEach((viewer) => { + userSet.add(viewer) + }) + + const users = Array.from(userSet) + + for (const user of users) { + await sendDM( + fileUrl, + key, + npubToHex(user)!, + nostrController, + false, + setAuthUrl + ) + } + } + + // Send DM to the next signer + const sendDMToNextSigner = async (fileUrl: string, key: string) => { + const usersNpub = hexToNpub(usersPubkey!) + const signerIndex = signers.indexOf(usersNpub) + const nextSigner = signers[signerIndex + 1] + await sendDM( + fileUrl, + key, + npubToHex(nextSigner)!, + nostrController, + true, + setAuthUrl + ) + } + const handleExport = async () => { if (!meta || !zip || !usersPubkey) return -- 2.34.1 From ded8304c669c257b5b782829ffb37be101af9cdd Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Wed, 12 Jun 2024 15:02:26 +0500 Subject: [PATCH 04/35] fix: sigit's wrapper zip should contain keys.json file --- src/controllers/NostrController.ts | 48 ++++++ src/pages/create/index.tsx | 119 +++++++------ src/pages/sign/index.tsx | 258 +++++++++++++++++++++-------- src/utils/misc.ts | 69 ++++++-- 4 files changed, 364 insertions(+), 130 deletions(-) diff --git a/src/controllers/NostrController.ts b/src/controllers/NostrController.ts index 049f99a..920d894 100644 --- a/src/controllers/NostrController.ts +++ b/src/controllers/NostrController.ts @@ -378,6 +378,54 @@ export class NostrController extends EventEmitter { throw new Error('Login method is undefined') } + 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 wil capture the public key from the local storage diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 98525a4..c445936 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -33,6 +33,7 @@ import { Meta, ProfileMetadata, User, UserRole } from '../../types' import { encryptArrayBuffer, generateEncryptionKey, + generateKeysFile, getHash, hexToNpub, isOnline, @@ -49,15 +50,12 @@ import { HTML5Backend } from 'react-dnd-html5-backend' import type { Identifier, XYCoord } from 'dnd-core' import { useDrag, useDrop } from 'react-dnd' import saveAs from 'file-saver' -import CopyModal from '../../components/copyModal' import { Event, kinds } from 'nostr-tools' export const CreatePage = () => { const navigate = useNavigate() const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') - const [openCopyModal, setOpenCopyModel] = useState(false) - const [textToCopy, setTextToCopy] = useState('') const [authUrl, setAuthUrl] = useState() @@ -374,26 +372,68 @@ export const CreatePage = () => { encryptionKey: string ): Promise => { setLoadingSpinnerDesc('Encrypting zip file') - return encryptArrayBuffer(arraybuffer, encryptionKey).finally(() => - setIsLoading(false) + return encryptArrayBuffer(arraybuffer, encryptionKey) + } + + // create final zip file + const createFinalZipFile = async ( + encryptedArrayBuffer: ArrayBuffer, + encryptionKey: string + ): Promise => { + // Get the current timestamp in seconds + 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.sigit`, { + type: 'application/sigit' + }) + + 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 } // Handle file upload and further actions based on online/offline status - const handleFileUpload = async (blob: Blob, encryptionKey: string) => { + const handleFileUpload = async (file: File, arrayBuffer: ArrayBuffer) => { if (await isOnline()) { - const fileUrl = await uploadFile(blob) + const fileUrl = await uploadFile(file) if (!fileUrl) return - await sendDMs(fileUrl, encryptionKey) + await sendDMs(fileUrl) setIsLoading(false) - navigate( - `${appPrivateRoutes.sign}?file=${encodeURIComponent(fileUrl)}&key=${encodeURIComponent(encryptionKey)}` - ) + navigate(appPrivateRoutes.sign, { state: { arrayBuffer } }) } else { - handleOffline(blob, encryptionKey) + handleOffline(file, arrayBuffer) } } @@ -406,11 +446,11 @@ export const CreatePage = () => { } // Upload the file to the storage and send DMs to signers/viewers - const uploadFile = async (blob: Blob): Promise => { + const uploadFile = async (file: File): Promise => { setIsLoading(true) setLoadingSpinnerDesc('Uploading zip file to file storage.') - const fileUrl = await uploadToFileStorage(blob, nostrController) + const fileUrl = await uploadToFileStorage(file, nostrController) .then((url) => { toast.success('zip file uploaded to file storage') return url @@ -420,8 +460,8 @@ export const CreatePage = () => { return fileUrl } - // Send DMs to signers and viewers with the file URL and encryption key - const sendDMs = async (fileUrl: string, encryptionKey: string) => { + // Send DMs to signers and viewers with the file URL + const sendDMs = async (fileUrl: string) => { setLoadingSpinnerDesc('Sending DM to signers/viewers') const signers = users.filter((user) => user.role === UserRole.signer) @@ -430,7 +470,6 @@ export const CreatePage = () => { if (signers.length > 0) { await sendDM( fileUrl, - encryptionKey, signers[0].pubkey, nostrController, true, @@ -438,34 +477,15 @@ export const CreatePage = () => { ) } else { for (const viewer of viewers) { - await sendDM( - fileUrl, - encryptionKey, - viewer.pubkey, - nostrController, - false, - setAuthUrl - ) + await sendDM(fileUrl, viewer.pubkey, nostrController, false, setAuthUrl) } } } // Manage offline scenarios for signing or viewing the file - const handleOffline = (blob: Blob, encryptionKey: string) => { - const signers = users.filter((user) => user.role === UserRole.signer) - - if (signers[0] && signers[0].pubkey === usersPubkey) { - // Create a File object with the Blob data for offline signing - const file = new File([blob], `compressed.sigit`, { - type: 'application/sigit' - }) - navigate(appPrivateRoutes.sign, { state: { file, encryptionKey } }) - } else { - // Save the file and show encryption key for offline viewing - saveAs(blob, 'request.sigit') - setTextToCopy(encryptionKey) - setOpenCopyModel(true) - } + const handleOffline = (file: File, arrayBuffer: ArrayBuffer) => { + saveAs(file, 'request.sigit.zip') + navigate(appPrivateRoutes.sign, { state: { arrayBuffer } }) } const handleCreate = async () => { @@ -497,9 +517,15 @@ export const CreatePage = () => { arraybuffer, encryptionKey ) - const blob = new Blob([encryptedArrayBuffer]) - return await handleFileUpload(blob, encryptionKey) + const finalZipFile = await createFinalZipFile( + encryptedArrayBuffer, + encryptionKey + ) + + if (!finalZipFile) return + + return await handleFileUpload(finalZipFile, arraybuffer) } if (authUrl) { @@ -596,15 +622,6 @@ export const CreatePage = () => { - { - setOpenCopyModel(false) - navigate(appPrivateRoutes.sign) - }} - title="Decryption key for Sigit file" - textToCopy={textToCopy} - /> ) } diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 7ffc1ec..715bdcc 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -10,7 +10,6 @@ import { TableCell, TableHead, TableRow, - TextField, Tooltip, Typography, useTheme @@ -52,7 +51,8 @@ import { shorten, signEventForMetaFile, uploadToFileStorage, - isOnline + isOnline, + generateKeysFile } from '../../utils' import styles from './style.module.scss' import { @@ -71,14 +71,13 @@ enum SignedStatus { export const SignPage = () => { const navigate = useNavigate() const location = useLocation() - const { file, encryptionKey: encKey } = location.state || {} + const { arrayBuffer: decryptedArrayBuffer } = location.state || {} - const [searchParams, setSearchParams] = useSearchParams() + const [searchParams] = useSearchParams() const [displayInput, setDisplayInput] = useState(false) const [selectedFile, setSelectedFile] = useState(null) - const [encryptionKey, setEncryptionKey] = useState('') const [zip, setZip] = useState() @@ -194,9 +193,8 @@ export const SignPage = () => { useEffect(() => { const fileUrl = searchParams.get('file') - const key = searchParams.get('key') - if (fileUrl && key) { + if (fileUrl) { setIsLoading(true) setLoadingSpinnerDesc('Fetching file from file server') @@ -208,7 +206,7 @@ export const SignPage = () => { const fileName = fileUrl.split('/').pop() const file = new File([res.data], fileName!) - decrypt(file, decodeURIComponent(key)).then((arrayBuffer) => { + decrypt(file).then((arrayBuffer) => { if (arrayBuffer) handleDecryptedArrayBuffer(arrayBuffer) }) }) @@ -221,40 +219,109 @@ export const SignPage = () => { .finally(() => { setIsLoading(false) }) - } else if (file && encKey) { - decrypt(file, decodeURIComponent(encKey)) - .then((arrayBuffer) => { - if (arrayBuffer) handleDecryptedArrayBuffer(arrayBuffer) - }) - .catch((err) => { - console.error(`error occurred in decryption`, err) - toast.error(err.message || `error occurred in decryption`) - }) - .finally(() => { - setIsLoading(false) - }) + } else if (decryptedArrayBuffer) { + handleDecryptedArrayBuffer(decryptedArrayBuffer).finally(() => + setIsLoading(false) + ) } else { setIsLoading(false) setDisplayInput(true) } - }, [searchParams, file, encKey]) + }, [searchParams, decryptedArrayBuffer]) - const decrypt = async (file: File, key: string) => { + const parseKeysJson = async (zip: JSZip) => { + const keysFileContent = await readContentOfZipEntry( + zip, + 'keys.json', + 'string' + ) + + if (!keysFileContent) return null + + const parsedJSON = await parseJson<{ sender: string; keys: string[] }>( + keysFileContent + ).catch((err) => { + console.log(`Error parsing content of keys.json:`, err) + toast.error(err.message || `Error parsing content of keys.json`) + return null + }) + + return parsedJSON + } + + const decrypt = async (file: File) => { setLoadingSpinnerDesc('Decrypting file') - const encryptedArrayBuffer = await file.arrayBuffer() + 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 - const arrayBuffer = await decryptArrayBuffer(encryptedArrayBuffer, key) - .catch((err) => { - console.log('err in decryption:>> ', err) - toast.error(err.message || 'An error occurred in decrypting file.') - return null - }) - .finally(() => { - setIsLoading(false) + const parsedKeysJson = await parseKeysJson(zip) + if (!parsedKeysJson) return + + const encryptedArrayBuffer = await readContentOfZipEntry( + zip, + 'compressed.sigit', + 'arraybuffer' + ) + + if (!encryptedArrayBuffer) return + + const { keys, sender } = parsedKeysJson + + for (const key of keys) { + // Set up event listener for authentication event + nostrController.on('nsecbunker-auth', (url) => { + setAuthUrl(url) }) - return arrayBuffer + // Set up timeout promise to handle encryption timeout + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('Timeout occurred')) + }, 60000) // Timeout duration = 60 seconds + }) + + // decrypt the encryptionKey, with timeout + const encryptionKey = await Promise.race([ + nostrController.nip04Decrypt(sender, key), + timeoutPromise + ]) + .then((res) => { + return res + }) + .catch((err) => { + console.log('err :>> ', err) + return null + }) + .finally(() => { + setAuthUrl(undefined) // Clear authentication URL + }) + + console.log('encryptionKey :>> ', encryptionKey) + + // Return if encryption failed + if (!encryptionKey) continue + + const arrayBuffer = await decryptArrayBuffer( + encryptedArrayBuffer, + encryptionKey + ) + .catch((err) => { + console.log('err in decryption:>> ', err) + return null + }) + .finally(() => { + setIsLoading(false) + }) + + if (arrayBuffer) return arrayBuffer + } + + return null } const handleDecryptedArrayBuffer = async (arrayBuffer: ArrayBuffer) => { @@ -348,13 +415,10 @@ export const SignPage = () => { } const handleDecrypt = async () => { - if (!selectedFile || !encryptionKey) return + if (!selectedFile) return setIsLoading(true) - const arrayBuffer = await decrypt( - selectedFile, - decodeURIComponent(encryptionKey) - ) + const arrayBuffer = await decrypt(selectedFile) if (!arrayBuffer) return @@ -407,13 +471,15 @@ export const SignPage = () => { setLoadingSpinnerDesc('Encrypting zip file') const encryptedArrayBuffer = await encryptArrayBuffer(arrayBuffer, key) - const blob = new Blob([encryptedArrayBuffer]) + const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key) + + if (!finalZipFile) return if (await isOnline()) { - await handleOnlineFlow(blob, key) - } else { - handleDecryptedArrayBuffer(arrayBuffer).finally(() => setIsLoading(false)) + await handleOnlineFlow(finalZipFile) } + + handleDecryptedArrayBuffer(arrayBuffer).finally(() => setIsLoading(false)) } // Read the content of the hashes.json file @@ -491,32 +557,101 @@ export const SignPage = () => { }) } + // create final zip file + const createFinalZipFile = async ( + encryptedArrayBuffer: ArrayBuffer, + encryptionKey: string + ): Promise => { + // Get the current timestamp in seconds + 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.sigit`, { + type: 'application/sigit' + }) + + const isLastSigner = checkIsLastSigner(signers) + + const userSet = new Set() + + if (isLastSigner) { + if (submittedBy) { + userSet.add(submittedBy) + } + + signers.forEach((signer) => { + userSet.add(npubToHex(signer)!) + }) + + viewers.forEach((viewer) => { + userSet.add(npubToHex(viewer)!) + }) + } else { + const usersNpub = hexToNpub(usersPubkey!) + const signerIndex = signers.indexOf(usersNpub) + const nextSigner = signers[signerIndex + 1] + userSet.add(npubToHex(nextSigner)!) + } + + const keysFileContent = await generateKeysFile( + Array.from(userSet), + 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 + } + + // 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 + } + // Handle the online flow: upload file and send DMs - const handleOnlineFlow = async (blob: Blob, key: string) => { - const fileUrl = await uploadZipFile(blob) + const handleOnlineFlow = async (file: File) => { + const fileUrl = await uploadZipFile(file) if (!fileUrl) return const isLastSigner = checkIsLastSigner(signers) if (isLastSigner) { - await sendDMToAllUsers(fileUrl, key) + await sendDMToAllUsers(fileUrl) } else { - await sendDMToNextSigner(fileUrl, key) + await sendDMToNextSigner(fileUrl) } setIsLoading(false) - - // Update search params with updated file URL and encryption key - setSearchParams({ - file: fileUrl, - key: key - }) } // Upload the zip file to file storage - const uploadZipFile = async (blob: Blob): Promise => { + const uploadZipFile = async (file: File): Promise => { setLoadingSpinnerDesc('Uploading zip file to file storage.') - const fileUrl = await uploadToFileStorage(blob, nostrController) + const fileUrl = await uploadToFileStorage(file, nostrController) .then((url) => { toast.success('Zip file uploaded to file storage') return url @@ -540,7 +675,7 @@ export const SignPage = () => { } // Send DM to all users (signers and viewers) - const sendDMToAllUsers = async (fileUrl: string, key: string) => { + const sendDMToAllUsers = async (fileUrl: string) => { const userSet = new Set<`npub1${string}`>() if (submittedBy) { @@ -560,7 +695,6 @@ export const SignPage = () => { for (const user of users) { await sendDM( fileUrl, - key, npubToHex(user)!, nostrController, false, @@ -570,13 +704,12 @@ export const SignPage = () => { } // Send DM to the next signer - const sendDMToNextSigner = async (fileUrl: string, key: string) => { + const sendDMToNextSigner = async (fileUrl: string) => { const usersNpub = hexToNpub(usersPubkey!) const signerIndex = signers.indexOf(usersNpub) const nextSigner = signers[signerIndex + 1] await sendDM( fileUrl, - key, npubToHex(nextSigner)!, nostrController, true, @@ -771,18 +904,9 @@ export const SignPage = () => { value={selectedFile} onChange={(value) => setSelectedFile(value)} /> - - {selectedFile && ( - setEncryptionKey(e.target.value)} - /> - )} - {selectedFile && encryptionKey && ( + {selectedFile && ( - - )} + {/* todo: In offline mode export sigit is not visible after last signer has signed*/} + {isSignerOrCreator && ( + + + + )} )} diff --git a/src/utils/misc.ts b/src/utils/misc.ts index a0e1d30..fc53dff 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -56,6 +56,7 @@ export const uploadToFileStorage = async ( /** * Sends a Direct Message (DM) to a recipient, encrypting the content and handling authentication. * @param fileUrl The URL of the encrypted zip file to be included in the DM. + * @param encryptionKey The encryption key used to decrypt the zip file to be included in the DM. * @param pubkey The public key of the recipient. * @param nostrController The NostrController instance for handling authentication and encryption. * @param isSigner Boolean indicating whether the recipient is a signer or viewer. @@ -63,6 +64,7 @@ export const uploadToFileStorage = async ( */ export const sendDM = async ( fileUrl: string, + encryptionKey: string, pubkey: string, nostrController: NostrController, isSigner: boolean, @@ -75,7 +77,9 @@ export const sendDM = async ( const decryptionUrl = `${window.location.origin}/#${ appPrivateRoutes.sign - }?file=${encodeURIComponent(fileUrl)}` + }?file=${encodeURIComponent(fileUrl)}&key=${encodeURIComponent( + encryptionKey + )}` const content = `${initialLine}\n\n${decryptionUrl}` -- 2.34.1 From 92b62a3cbed8461cbbb25cb841bec9063f11e90d Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Thu, 13 Jun 2024 11:47:28 +0500 Subject: [PATCH 08/35] feat: navigate to different pages based on uploaded file --- src/pages/create/index.tsx | 11 ++++++- src/pages/home/index.tsx | 60 ++++++++++++++++++++++++++++++++++++-- src/pages/sign/index.tsx | 20 +++++++++++-- src/pages/verify/index.tsx | 13 +++++++-- 4 files changed, 96 insertions(+), 8 deletions(-) diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 24538fb..84ad3bf 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -22,7 +22,7 @@ import JSZip from 'jszip' import { MuiFileInput } from 'mui-file-input' import { useEffect, useRef, useState } from 'react' import { useSelector } from 'react-redux' -import { useNavigate } from 'react-router-dom' +import { useLocation, useNavigate } from 'react-router-dom' import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' import { UserComponent } from '../../components/username' @@ -54,6 +54,9 @@ import { Event, kinds } from 'nostr-tools' export const CreatePage = () => { const navigate = useNavigate() + const location = useLocation() + const { uploadedFile } = location.state || {} + const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') @@ -72,6 +75,12 @@ export const CreatePage = () => { const nostrController = NostrController.getInstance() + useEffect(() => { + if (uploadedFile) { + setSelectedFiles([uploadedFile]) + } + }, [uploadedFile]) + useEffect(() => { if (usersPubkey) { setUsers((prev) => { diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index 9cf4508..181b67d 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -7,11 +7,61 @@ import { } from '@mui/icons-material' import { Box, Button, Tooltip, Typography } from '@mui/material' import { useNavigate } from 'react-router-dom' -import { appPrivateRoutes } from '../../routes' +import { appPrivateRoutes, appPublicRoutes } from '../../routes' import styles from './style.module.scss' +import { useRef } from 'react' +import JSZip from 'jszip' +import { toast } from 'react-toastify' export const HomePage = () => { const navigate = useNavigate() + const fileInputRef = useRef(null) + + const handleUploadClick = () => { + if (fileInputRef.current) { + fileInputRef.current.click() + } + } + + const handleFileChange = async ( + event: React.ChangeEvent + ) => { + 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 ( @@ -29,10 +79,16 @@ export const HomePage = () => { } }} > + diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 2a8ca9a..b49d11e 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -40,7 +40,8 @@ enum SignedStatus { export const SignPage = () => { const navigate = useNavigate() const location = useLocation() - const { arrayBuffer: decryptedArrayBuffer } = location.state || {} + const { arrayBuffer: decryptedArrayBuffer, uploadedZip } = + location.state || {} const [searchParams, setSearchParams] = useSearchParams() @@ -200,11 +201,23 @@ export const SignPage = () => { handleDecryptedArrayBuffer(decryptedArrayBuffer).finally(() => setIsLoading(false) ) + } else if (uploadedZip) { + decrypt(uploadedZip) + .then((arrayBuffer) => { + if (arrayBuffer) handleDecryptedArrayBuffer(arrayBuffer) + }) + .catch((err) => { + console.error(`error occurred in decryption`, err) + toast.error(err.message || `error occurred in decryption`) + }) + .finally(() => { + setIsLoading(false) + }) } else { setIsLoading(false) setDisplayInput(true) } - }, [searchParams, decryptedArrayBuffer]) + }, [searchParams, decryptedArrayBuffer, uploadedZip]) const parseKeysJson = async (zip: JSZip) => { const keysFileContent = await readContentOfZipEntry( @@ -768,7 +781,8 @@ export const SignPage = () => { if (!arrayBuffer) return const blob = new Blob([arrayBuffer]) - saveAs(blob, 'exported.zip') + const unixNow = Math.floor(Date.now() / 1000) + saveAs(blob, `exported-${unixNow}.sigit.zip`) setIsLoading(false) diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index a8e0d1f..53aea4a 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -32,14 +32,17 @@ import { } from '../../utils' import styles from './style.module.scss' import { Cancel, CheckCircle } from '@mui/icons-material' +import { useLocation } from 'react-router-dom' export const VerifyPage = () => { const theme = useTheme() - const textColor = theme.palette.getContrastText( theme.palette.background.paper ) + const location = useLocation() + const { uploadedZip } = location.state || {} + const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') @@ -62,6 +65,12 @@ export const VerifyPage = () => { {} ) + useEffect(() => { + if (uploadedZip) { + setSelectedFile(uploadedZip) + } + }, [uploadedZip]) + useEffect(() => { if (zip) { const generateCurrentFileHashes = async () => { @@ -364,7 +373,7 @@ export const VerifyPage = () => { onChange={(value) => setSelectedFile(value)} InputProps={{ inputProps: { - accept: '.zip' + accept: '.sigit.zip' } }} /> -- 2.34.1 From 29654a9b910f54dd25acade07ba1eb01ed8cf563 Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Thu, 13 Jun 2024 14:58:54 +0500 Subject: [PATCH 09/35] chore: add comments --- src/controllers/NostrController.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/controllers/NostrController.ts b/src/controllers/NostrController.ts index 920d894..ecc9cc9 100644 --- a/src/controllers/NostrController.ts +++ b/src/controllers/NostrController.ts @@ -378,6 +378,13 @@ export class NostrController extends EventEmitter { 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 -- 2.34.1 From bb37a27321cd9b26537b8fbbe2b39902b6c85fc4 Mon Sep 17 00:00:00 2001 From: SIGit Date: Tue, 18 Jun 2024 15:00:21 +0100 Subject: [PATCH 10/35] feat: nostr.json --- public/.well-known/nostr.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 public/.well-known/nostr.json diff --git a/public/.well-known/nostr.json b/public/.well-known/nostr.json new file mode 100644 index 0000000..6dd4dd9 --- /dev/null +++ b/public/.well-known/nostr.json @@ -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" + ] + } +} \ No newline at end of file -- 2.34.1 From 970c5f5e8bfc0129283fe14c7e6fa3c9a8a35ea4 Mon Sep 17 00:00:00 2001 From: SIGit Date: Tue, 18 Jun 2024 15:21:17 +0100 Subject: [PATCH 11/35] fix: include hidden folders in surfer upload --- .gitea/workflows/release-production.yaml | 2 +- .gitea/workflows/release-staging.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/release-production.yaml b/.gitea/workflows/release-production.yaml index 334eb11..a7d470c 100644 --- a/.gitea/workflows/release-production.yaml +++ b/.gitea/workflows/release-production.yaml @@ -29,4 +29,4 @@ jobs: - name: Release Build run: | npm -g install cloudron-surfer - surfer put --token ${{ secrets.CLOUDRON_SURFER_TOKEN }} --server sigit.io dist/* / + surfer put --token ${{ secrets.CLOUDRON_SURFER_TOKEN }} --server sigit.io dist/. / diff --git a/.gitea/workflows/release-staging.yaml b/.gitea/workflows/release-staging.yaml index 0467f63..e3783e9 100644 --- a/.gitea/workflows/release-staging.yaml +++ b/.gitea/workflows/release-staging.yaml @@ -29,4 +29,4 @@ jobs: - name: Release Build run: | npm -g install cloudron-surfer - surfer put --token ${{ secrets.STAGING_CLOUDRON_SURFER_TOKEN }} --server staging.sigit.io dist/* / + surfer put --token ${{ secrets.STAGING_CLOUDRON_SURFER_TOKEN }} --server staging.sigit.io dist/. / -- 2.34.1 From 24916c58068bbe9e5dfc76cbf00c955add6744e4 Mon Sep 17 00:00:00 2001 From: SIGit Date: Tue, 18 Jun 2024 15:23:22 +0100 Subject: [PATCH 12/35] fix: push all files take 2 --- .gitea/workflows/release-production.yaml | 2 +- .gitea/workflows/release-staging.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/release-production.yaml b/.gitea/workflows/release-production.yaml index a7d470c..f1e7184 100644 --- a/.gitea/workflows/release-production.yaml +++ b/.gitea/workflows/release-production.yaml @@ -29,4 +29,4 @@ jobs: - name: Release Build run: | npm -g install cloudron-surfer - surfer put --token ${{ secrets.CLOUDRON_SURFER_TOKEN }} --server sigit.io dist/. / + surfer put --token ${{ secrets.CLOUDRON_SURFER_TOKEN }} --server sigit.io dist/ / diff --git a/.gitea/workflows/release-staging.yaml b/.gitea/workflows/release-staging.yaml index e3783e9..47a99fb 100644 --- a/.gitea/workflows/release-staging.yaml +++ b/.gitea/workflows/release-staging.yaml @@ -29,4 +29,4 @@ jobs: - name: Release Build run: | npm -g install cloudron-surfer - surfer put --token ${{ secrets.STAGING_CLOUDRON_SURFER_TOKEN }} --server staging.sigit.io dist/. / + surfer put --token ${{ secrets.STAGING_CLOUDRON_SURFER_TOKEN }} --server staging.sigit.io dist/ / -- 2.34.1 From 02f250c76eb6d1b7fb410394c88397e37dd527e4 Mon Sep 17 00:00:00 2001 From: SIGit Date: Tue, 18 Jun 2024 15:30:16 +0100 Subject: [PATCH 13/35] fix: take 3 all files --- .gitea/workflows/release-production.yaml | 3 ++- .gitea/workflows/release-staging.yaml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/release-production.yaml b/.gitea/workflows/release-production.yaml index f1e7184..e2dd110 100644 --- a/.gitea/workflows/release-production.yaml +++ b/.gitea/workflows/release-production.yaml @@ -29,4 +29,5 @@ jobs: - name: Release Build run: | npm -g install cloudron-surfer - surfer put --token ${{ secrets.CLOUDRON_SURFER_TOKEN }} --server sigit.io dist/ / + cd dist + surfer put --token ${{ secrets.CLOUDRON_SURFER_TOKEN }} --server sigit.io . / diff --git a/.gitea/workflows/release-staging.yaml b/.gitea/workflows/release-staging.yaml index 47a99fb..c93f053 100644 --- a/.gitea/workflows/release-staging.yaml +++ b/.gitea/workflows/release-staging.yaml @@ -29,4 +29,5 @@ jobs: - name: Release Build run: | npm -g install cloudron-surfer - surfer put --token ${{ secrets.STAGING_CLOUDRON_SURFER_TOKEN }} --server staging.sigit.io dist/ / + cd dist + surfer put --token ${{ secrets.STAGING_CLOUDRON_SURFER_TOKEN }} --server staging.sigit.io . / -- 2.34.1 From abf9c3e4fd7b61d5f8794714484cb8a0c542d6e7 Mon Sep 17 00:00:00 2001 From: SIGit Date: Tue, 18 Jun 2024 15:36:27 +0100 Subject: [PATCH 14/35] fix: take 4 (all files) --- .gitea/workflows/release-production.yaml | 4 ++-- .gitea/workflows/release-staging.yaml | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/release-production.yaml b/.gitea/workflows/release-production.yaml index e2dd110..98c3c31 100644 --- a/.gitea/workflows/release-production.yaml +++ b/.gitea/workflows/release-production.yaml @@ -29,5 +29,5 @@ jobs: - name: Release Build run: | npm -g install cloudron-surfer - cd dist - surfer put --token ${{ secrets.CLOUDRON_SURFER_TOKEN }} --server sigit.io . / + surfer config --token ${{ secrets.CLOUDRON_SURFER_TOKEN }} --server sigit.io + surfer put . diff --git a/.gitea/workflows/release-staging.yaml b/.gitea/workflows/release-staging.yaml index c93f053..3b32404 100644 --- a/.gitea/workflows/release-staging.yaml +++ b/.gitea/workflows/release-staging.yaml @@ -30,4 +30,5 @@ jobs: run: | npm -g install cloudron-surfer cd dist - surfer put --token ${{ secrets.STAGING_CLOUDRON_SURFER_TOKEN }} --server staging.sigit.io . / + surfer config --token ${{ secrets.STAGING_CLOUDRON_SURFER_TOKEN }} --server staging.sigit.io + surfer put . -- 2.34.1 From ea3f61897c7ab8601547e3b61a6bdd833a19ad12 Mon Sep 17 00:00:00 2001 From: SIGit Date: Tue, 18 Jun 2024 15:39:35 +0100 Subject: [PATCH 15/35] fix: take 5 files --- .gitea/workflows/release-production.yaml | 2 +- .gitea/workflows/release-staging.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/release-production.yaml b/.gitea/workflows/release-production.yaml index 98c3c31..a8dda7c 100644 --- a/.gitea/workflows/release-production.yaml +++ b/.gitea/workflows/release-production.yaml @@ -30,4 +30,4 @@ jobs: run: | npm -g install cloudron-surfer surfer config --token ${{ secrets.CLOUDRON_SURFER_TOKEN }} --server sigit.io - surfer put . + surfer put . / diff --git a/.gitea/workflows/release-staging.yaml b/.gitea/workflows/release-staging.yaml index 3b32404..42f2562 100644 --- a/.gitea/workflows/release-staging.yaml +++ b/.gitea/workflows/release-staging.yaml @@ -31,4 +31,4 @@ jobs: npm -g install cloudron-surfer cd dist surfer config --token ${{ secrets.STAGING_CLOUDRON_SURFER_TOKEN }} --server staging.sigit.io - surfer put . + surfer put . / -- 2.34.1 From 400d192fb0441bbe772d44a457f4e96a4d42d11f Mon Sep 17 00:00:00 2001 From: SIGit Date: Wed, 19 Jun 2024 11:53:16 +0100 Subject: [PATCH 16/35] fix: take 6 --- .gitea/workflows/release-production.yaml | 3 ++- .gitea/workflows/release-staging.yaml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/release-production.yaml b/.gitea/workflows/release-production.yaml index a8dda7c..d9529ad 100644 --- a/.gitea/workflows/release-production.yaml +++ b/.gitea/workflows/release-production.yaml @@ -30,4 +30,5 @@ jobs: run: | npm -g install cloudron-surfer surfer config --token ${{ secrets.CLOUDRON_SURFER_TOKEN }} --server sigit.io - surfer put . / + surfer put dist/* / --all -d + surfer put dist/.well-known / --all diff --git a/.gitea/workflows/release-staging.yaml b/.gitea/workflows/release-staging.yaml index 42f2562..0b2434a 100644 --- a/.gitea/workflows/release-staging.yaml +++ b/.gitea/workflows/release-staging.yaml @@ -31,4 +31,5 @@ jobs: npm -g install cloudron-surfer cd dist surfer config --token ${{ secrets.STAGING_CLOUDRON_SURFER_TOKEN }} --server staging.sigit.io - surfer put . / + surfer put dist/* / --all -d + surfer put dist/.well-known / --all -- 2.34.1 From 3f944bdf73103e6a0152c32bc788d363c323f42b Mon Sep 17 00:00:00 2001 From: SIGit Date: Wed, 19 Jun 2024 12:03:25 +0100 Subject: [PATCH 17/35] fix: take 7 --- .gitea/workflows/release-staging.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitea/workflows/release-staging.yaml b/.gitea/workflows/release-staging.yaml index 0b2434a..793c70c 100644 --- a/.gitea/workflows/release-staging.yaml +++ b/.gitea/workflows/release-staging.yaml @@ -29,7 +29,6 @@ jobs: - name: Release Build run: | npm -g install cloudron-surfer - cd dist surfer config --token ${{ secrets.STAGING_CLOUDRON_SURFER_TOKEN }} --server staging.sigit.io surfer put dist/* / --all -d surfer put dist/.well-known / --all -- 2.34.1 From b91abe1e681e51e16e064172405e74a26a249c8e Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Thu, 20 Jun 2024 21:56:47 +0500 Subject: [PATCH 18/35] chore: npm audit fix --- package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index e4a521d..8fbc648 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2507,12 +2507,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -3380,9 +3380,9 @@ "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" -- 2.34.1 From 5850ef7dce5139dc06dcc9b9ccb8f906b1b4f613 Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Fri, 28 Jun 2024 14:24:14 +0500 Subject: [PATCH 19/35] chore: push unfinished changes --- package-lock.json | 30 +- package.json | 6 +- src/components/username.tsx | 33 +- src/controllers/MetadataController.ts | 49 ++- src/controllers/NostrController.ts | 190 +++++++++- src/layouts/Main.tsx | 5 +- src/pages/create/index.tsx | 152 ++++---- src/pages/home/index.tsx | 401 +++++++++++++++++----- src/pages/home/style.module.scss | 25 +- src/pages/sign/index.tsx | 477 +++++++++++--------------- src/services/cache/index.ts | 41 ++- src/services/cache/schema.ts | 11 +- src/types/cache.ts | 2 +- src/types/core.ts | 8 + src/types/nostr.ts | 4 + src/types/system/index.d.ts | 7 + src/utils/misc.ts | 96 +++++- src/utils/nostr.ts | 438 ++++++++++++++++++++++- src/utils/string.ts | 18 + 19 files changed, 1473 insertions(+), 520 deletions(-) create mode 100644 src/types/system/index.d.ts diff --git a/package-lock.json b/package-lock.json index 8fbc648..c000deb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "jszip": "3.10.1", "lodash": "4.17.21", "mui-file-input": "4.0.4", - "nostr-tools": "2.3.1", + "nostr-tools": "2.7.0", "react": "^18.2.0", "react-dnd": "16.0.1", "react-dnd-html5-backend": "16.0.1", @@ -34,7 +34,8 @@ "react-router-dom": "6.22.1", "react-toastify": "10.0.4", "redux": "5.0.1", - "tseep": "1.2.1" + "tseep": "1.2.1", + "uuid": "10.0.0" }, "devDependencies": { "@types/crypto-js": "^4.2.2", @@ -42,6 +43,7 @@ "@types/lodash": "4.14.202", "@types/react": "^18.2.56", "@types/react-dom": "^18.2.19", + "@types/uuid": "9.0.8", "@typescript-eslint/eslint-plugin": "^7.0.2", "@typescript-eslint/parser": "^7.0.2", "@vitejs/plugin-react": "^4.2.1", @@ -2138,6 +2140,12 @@ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.0.tgz", @@ -4137,9 +4145,9 @@ } }, "node_modules/nostr-tools": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.3.1.tgz", - "integrity": "sha512-qjKx2C3EzwiQOe2LPSPyCnp07pGz1pWaWjDXcm+L2y2c8iTECbvlzujDANm3nJUjWL5+LVRUVDovTZ1a/DC4Bg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.7.0.tgz", + "integrity": "sha512-jJoL2J1CBiKDxaXZww27nY/Wsuxzx7AULxmGKFce4sskDu1tohNyfnzYQ8BvDyvkstU8kNZUAXPL32tre33uig==", "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", @@ -5211,6 +5219,18 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/package.json b/package.json index dc00ccf..658fafb 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "jszip": "3.10.1", "lodash": "4.17.21", "mui-file-input": "4.0.4", - "nostr-tools": "2.3.1", + "nostr-tools": "2.7.0", "react": "^18.2.0", "react-dnd": "16.0.1", "react-dnd-html5-backend": "16.0.1", @@ -40,7 +40,8 @@ "react-router-dom": "6.22.1", "react-toastify": "10.0.4", "redux": "5.0.1", - "tseep": "1.2.1" + "tseep": "1.2.1", + "uuid": "10.0.0" }, "devDependencies": { "@types/crypto-js": "^4.2.2", @@ -48,6 +49,7 @@ "@types/lodash": "4.14.202", "@types/react": "^18.2.56", "@types/react-dom": "^18.2.19", + "@types/uuid": "9.0.8", "@typescript-eslint/eslint-plugin": "^7.0.2", "@typescript-eslint/parser": "^7.0.2", "@vitejs/plugin-react": "^4.2.1", diff --git a/src/components/username.tsx b/src/components/username.tsx index 2945126..404e4cb 100644 --- a/src/components/username.tsx +++ b/src/components/username.tsx @@ -1,9 +1,9 @@ -import { Typography, IconButton, Box, useTheme } from '@mui/material' +import { Box, IconButton, Typography, useTheme } from '@mui/material' import { useSelector } from 'react-redux' +import { useNavigate } from 'react-router-dom' +import { getProfileRoute } from '../routes' import { State } from '../store/rootReducer' import { hexToNpub } from '../utils' -import { Link } from 'react-router-dom' -import { getProfileRoute } from '../routes' type Props = { username: string @@ -60,6 +60,7 @@ type UserProps = { */ export const UserComponent = ({ pubkey, name, image }: UserProps) => { const theme = useTheme() + const navigate = useNavigate() const npub = hexToNpub(pubkey) const roboImage = `https://robohash.org/${npub}.png?set=set3` @@ -67,6 +68,10 @@ export const UserComponent = ({ pubkey, name, image }: UserProps) => { return ( { + e.stopPropagation() + navigate(getProfileRoute(pubkey)) + }} > { borderColor: `#${pubkey.substring(0, 6)}` }} /> - - - {name} - - + + {name} + ) } diff --git a/src/controllers/MetadataController.ts b/src/controllers/MetadataController.ts index 301f21d..613ec44 100644 --- a/src/controllers/MetadataController.ts +++ b/src/controllers/MetadataController.ts @@ -46,6 +46,7 @@ export class MetadataController extends EventEmitter { const pool = new SimplePool() + // todo: use nostrController to get event // Try to get the metadata event from a special relay (wss://purplepag.es) const metadataEvent = await pool .get([this.specialMetadataRelay], eventFilter) @@ -68,6 +69,7 @@ export class MetadataController extends EventEmitter { } } + // todo use nostr controller to find event from connected relays // If no valid metadata event is found from the special relay, get the most popular relays const mostPopularRelays = await this.nostrController.getMostPopularRelays() @@ -141,6 +143,22 @@ export class MetadataController extends EventEmitter { } public findRelayListMetadata = async (hexKey: string) => { + let relayEvent: Event | null = null + + // Attempt to retrieve the metadata event from the local cache + const cachedRelayListMetadataEvent = + await localCache.getUserRelayListMetadata(hexKey) + + if (cachedRelayListMetadataEvent) { + const oneWeekInMS = 7 * 24 * 60 * 60 * 1000 // Number of milliseconds in one week + + // Check if the cached event is not older than one week + if (Date.now() - cachedRelayListMetadataEvent.cachedAt < oneWeekInMS) { + relayEvent = cachedRelayListMetadataEvent.event + } + } + + // define filter for relay list const eventFilter: Filter = { kinds: [kinds.RelayList], authors: [hexKey] @@ -148,19 +166,38 @@ export class MetadataController extends EventEmitter { const pool = new SimplePool() - let relayEvent = await pool - .get([this.specialMetadataRelay], eventFilter) - .catch((err) => { - console.error(err) - return null - }) + // Try to get the relayList event from a special relay (wss://purplepag.es) + if (!relayEvent) { + relayEvent = await pool + .get([this.specialMetadataRelay], eventFilter) + .then((event) => { + if (event) { + // update the event in local cache + localCache.addUserRelayListMetadata(event) + } + return event + }) + .catch((err) => { + console.error(err) + return null + }) + } if (!relayEvent) { + // If no valid relayList event is found from the special relay, get the most popular relays const mostPopularRelays = await this.nostrController.getMostPopularRelays() + // Query the most popular relays for relayList event relayEvent = await pool .get(mostPopularRelays, eventFilter) + .then((event) => { + if (event) { + // update the event in local cache + localCache.addUserRelayListMetadata(event) + } + return event + }) .catch((err) => { console.error(err) return null diff --git a/src/controllers/NostrController.ts b/src/controllers/NostrController.ts index ecc9cc9..65c9897 100644 --- a/src/controllers/NostrController.ts +++ b/src/controllers/NostrController.ts @@ -2,46 +2,50 @@ import NDK, { NDKEvent, NDKNip46Signer, NDKPrivateKeySigner, + NDKSubscription, NDKUser, - NostrEvent, - NDKSubscription + NostrEvent } from '@nostr-dev-kit/ndk' +import axios from 'axios' import { Event, EventTemplate, - SimplePool, - UnsignedEvent, Filter, Relay, + SimplePool, + UnsignedEvent, finalizeEvent, + kinds, nip04, nip19, - kinds + nip44 } from 'nostr-tools' +import { toast } from 'react-toastify' import { EventEmitter } from 'tseep' import { - updateNsecbunkerPubkey, setMostPopularRelaysAction, + setRelayConnectionStatusAction, setRelayInfoAction, - setRelayConnectionStatusAction + updateNsecbunkerPubkey } from '../store/actions' import { AuthState, LoginMethods } from '../store/auth/types' import store from '../store/store' import { - SignedEvent, - RelayMap, - RelayStats, - RelayReadStats, - RelayInfoObject, + RelayConnectionState, RelayConnectionStatus, - RelayConnectionState + RelayInfoObject, + RelayMap, + RelayReadStats, + RelayStats, + Rumor, + SignedEvent } from '../types' import { compareObjects, getNsecBunkerDelegatedKey, + randomNow, verifySignedEvent } from '../utils' -import axios from 'axios' export class NostrController extends EventEmitter { private static instance: NostrController @@ -56,7 +60,8 @@ export class NostrController extends EventEmitter { } private getNostrObject = () => { - if (window.nostr) return window.nostr + // fix: this is not picking up type declaration from src/system/index.d.ts + if (window.nostr) return window.nostr as any throw new Error( `window.nostr object not present. Make sure you have an nostr extension installed/working properly.` @@ -260,6 +265,157 @@ export class NostrController extends EventEmitter { return publishedRelays } + /** + * Asynchronously retrieves an event from a set of relays based on a provided filter. + * If no relays are specified, it defaults to using connected relays. + * + * @param {Filter} filter - The filter criteria to find the event. + * @param {string[]} [relays] - An optional array of relay URLs to search for the event. + * @returns {Promise} - Returns a promise that resolves to the found event or null if not found. + */ + getEvent = async ( + filter: Filter, + relays?: string[] + ): Promise => { + // If no relays are provided or the provided array is empty, use connected relays if available. + if (!relays || relays.length === 0) { + relays = this.connectedRelays + ? this.connectedRelays.map((relay) => relay.url) + : [] + } + + // If still no relays are available, reject the promise with an error message. + if (relays.length === 0) { + return Promise.reject('Provide some relays to find the event') + } + + // Create a new instance of SimplePool to handle the relay connections and event retrieval. + const pool = new SimplePool() + + // Attempt to retrieve the event from the specified relays using the filter criteria. + const event = await pool.get(relays, filter).catch((err) => { + // Log any errors that occur during the event retrieval process. + console.log('An error occurred in finding the event', err) + // Show an error toast notification to the user. + toast.error('An error occurred in finding the event') + // Return null if an error occurs, indicating that no event was found. + return null + }) + + // Return the found event, or null if an error occurred. + return event + } + + createSeal = async (rumor: Rumor, receiver: string) => { + const encryptedContent = await this.nip44Encrypt( + receiver, + JSON.stringify(rumor) + ) + + const unsignedEvent: UnsignedEvent = { + kind: 13, + content: encryptedContent, + created_at: randomNow(), + tags: [], + pubkey: (store.getState().auth as AuthState).usersPubkey! + } + + const signedEvent = await this.signEvent(unsignedEvent) + return signedEvent + } + + nip44Encrypt = async (receiver: string, content: string) => { + const loginMethod = (store.getState().auth as AuthState).loginMethod + + if (loginMethod === LoginMethods.extension) { + const nostr = this.getNostrObject() + + if (!nostr.nip44) { + throw new Error( + `Your nostr extension does not support nip44 encryption & decryption` + ) + } + + const encrypted = await nostr.nip44.encrypt(receiver, content) + return encrypted as string + } + + 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 nip44ConversationKey = nip44.v2.utils.getConversationKey( + privateKey, + receiver + ) + const encrypted = nip44.v2.encrypt(content, nip44ConversationKey) + + return encrypted + } + + if (loginMethod === LoginMethods.nsecBunker) { + throw new Error( + `nip44 encryption is not yet supported for login method '${LoginMethods.nsecBunker}'` + ) + } + + throw new Error('Login method is undefined') + } + + nip44Decrypt = async (sender: string, content: string) => { + const loginMethod = (store.getState().auth as AuthState).loginMethod + + if (loginMethod === LoginMethods.extension) { + const nostr = this.getNostrObject() + + if (!nostr.nip44) { + throw new Error( + `Your nostr extension does not support nip44 encryption & decryption` + ) + } + + const decrypted = await nostr.nip44.decrypt(sender, content) + return decrypted as string + } + + 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 nip44ConversationKey = nip44.v2.utils.getConversationKey( + privateKey, + sender + ) + const decrypted = nip44.v2.decrypt(content, nip44ConversationKey) + + return decrypted + } + + if (loginMethod === LoginMethods.nsecBunker) { + throw new Error( + `nip44 decryption is not yet supported for login method '${LoginMethods.nsecBunker}'` + ) + } + + throw new Error('Login method is undefined') + } + /** * Signs an event with private key (if it is present in local storage) or * with browser extension (if it is present) or @@ -330,7 +486,7 @@ export class NostrController extends EventEmitter { } } - nip04Encrypt = async (receiver: string, content: string) => { + nip04Encrypt = async (receiver: string, content: string): Promise => { const loginMethod = (store.getState().auth as AuthState).loginMethod if (loginMethod === LoginMethods.extension) { @@ -385,7 +541,7 @@ export class NostrController extends EventEmitter { * @param content - The encrypted content to decrypt. * @returns A promise that resolves to the decrypted content. */ - nip04Decrypt = async (sender: string, content: string) => { + nip04Decrypt = async (sender: string, content: string): Promise => { const loginMethod = (store.getState().auth as AuthState).loginMethod if (loginMethod === LoginMethods.extension) { diff --git a/src/layouts/Main.tsx b/src/layouts/Main.tsx index c63e74d..2c13cf2 100644 --- a/src/layouts/Main.tsx +++ b/src/layouts/Main.tsx @@ -10,7 +10,8 @@ import { clearState, getRoboHashPicture, loadState, - saveNsecBunkerDelegatedKey + saveNsecBunkerDelegatedKey, + subscribeForSigits } from '../utils' import { LoadingSpinner } from '../components/LoadingSpinner' import { Dispatch } from '../store/store' @@ -100,6 +101,8 @@ export const MainLayout = () => { if (pubkey) { dispatch(setUserRobotImage(getRoboHashPicture(pubkey))) + + subscribeForSigits(pubkey) } } }, [authState]) diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 84ad3bf..962afd6 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -18,12 +18,18 @@ import { Tooltip, Typography } from '@mui/material' +import type { Identifier, XYCoord } from 'dnd-core' +import saveAs from 'file-saver' import JSZip from 'jszip' import { MuiFileInput } from 'mui-file-input' -import { useEffect, useRef, useState } from 'react' +import { Event, kinds } from 'nostr-tools' +import { useEffect, useMemo, useRef, useState } from 'react' +import { DndProvider, useDrag, useDrop } from 'react-dnd' +import { HTML5Backend } from 'react-dnd-html5-backend' import { useSelector } from 'react-redux' import { useLocation, useNavigate } from 'react-router-dom' import { toast } from 'react-toastify' +import { v4 as uuidV4 } from 'uuid' import { LoadingSpinner } from '../../components/LoadingSpinner' import { UserComponent } from '../../components/username' import { MetadataController, NostrController } from '../../controllers' @@ -33,24 +39,21 @@ import { Meta, ProfileMetadata, User, UserRole } from '../../types' import { encryptArrayBuffer, generateEncryptionKey, + generateKeys, generateKeysFile, getHash, hexToNpub, isOnline, + now, npubToHex, queryNip05, - sendDM, + sendNotification, shorten, signEventForMetaFile, + updateUsersAppData, uploadToFileStorage } from '../../utils' import styles from './style.module.scss' -import { DndProvider } from 'react-dnd' -import { HTML5Backend } from 'react-dnd-html5-backend' -import type { Identifier, XYCoord } from 'dnd-core' -import { useDrag, useDrop } from 'react-dnd' -import saveAs from 'file-saver' -import { Event, kinds } from 'nostr-tools' export const CreatePage = () => { const navigate = useNavigate() @@ -75,6 +78,15 @@ export const CreatePage = () => { const nostrController = NostrController.getInstance() + // Set up event listener for authentication event + nostrController.on('nsecbunker-auth', (url) => { + setAuthUrl(url) + }) + + const uuid = useMemo(() => { + return uuidV4() + }, []) + useEffect(() => { if (uploadedFile) { setSelectedFiles([uploadedFile]) @@ -283,16 +295,28 @@ export const CreatePage = () => { 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> => { + // initialize a zip file with the selected files and generate creator's signature + const initZipFileAndCreatorSignature = async ( + encryptionKey: string, + fileHashes: { + [key: string]: string + } + ): Promise<{ zip: JSZip; createSignature: string } | null> => { const zip = new JSZip() selectedFiles.forEach((file) => { zip.file(`files/${file.name}`, file) }) + // generate key pairs for decryption + const pubkeys = users.map((user) => user.pubkey) + // also add creator in the list + if (pubkeys.includes(usersPubkey!)) { + pubkeys.push(usersPubkey!) + } + + const keys = await generateKeys(pubkeys, encryptionKey) + const signers = users.filter((user) => user.role === UserRole.signer) const viewers = users.filter((user) => user.role === UserRole.viewer) @@ -302,7 +326,8 @@ export const CreatePage = () => { JSON.stringify({ signers: signers.map((signer) => hexToNpub(signer.pubkey)), viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)), - fileHashes + fileHashes, + keys }), nostrController, setIsLoading @@ -320,38 +345,6 @@ export const CreatePage = () => { } } - // Add metadata and file hashes to the zip file - const addMetaToZip = async ( - zip: JSZip, - createSignature: string - ): Promise => { - // create content for meta file - const meta: Meta = { - title, - createSignature, - docSignatures: {} - } - - try { - const stringifiedMeta = JSON.stringify(meta, null, 2) - zip.file('meta.json', stringifiedMeta) - - const metaHash = await getHash(stringifiedMeta) - if (!metaHash) return null - - const metaHashJson = { - [usersPubkey!]: metaHash - } - - zip.file('hashes.json', JSON.stringify(metaHashJson, null, 2)) - return metaHash - } catch (err) { - console.error(err) - toast.error('An error occurred in converting meta json to string') - return null - } - } - // Handle errors during zip file generation const handleZipError = (err: any) => { console.log('Error in zip:>> ', err) @@ -390,7 +383,7 @@ export const CreatePage = () => { encryptionKey: string ): Promise => { // Get the current timestamp in seconds - const unixNow = Math.floor(Date.now() / 1000) + const unixNow = now() const blob = new Blob([encryptedArrayBuffer]) // Create a File object with the Blob data const file = new File([blob], `compressed.sigit`, { @@ -432,9 +425,9 @@ export const CreatePage = () => { const handleOnlineFlow = async ( encryptedArrayBuffer: ArrayBuffer, - encryptionKey: string + meta: Meta ) => { - const unixNow = Math.floor(Date.now() / 1000) + const unixNow = now() const blob = new Blob([encryptedArrayBuffer]) // Create a File object with the Blob data const file = new File([blob], `compressed-${unixNow}.sigit`, { @@ -444,7 +437,12 @@ export const CreatePage = () => { const fileUrl = await uploadFile(file) if (!fileUrl) return - await sendDMs(fileUrl, encryptionKey) + const updatedEvent = await updateUsersAppData(fileUrl, meta) + if (!updatedEvent) return + + await sendDMs(fileUrl, meta) + + navigate(appPrivateRoutes.sign, { state: { sigit: { fileUrl, meta } } }) } // Handle errors during file upload @@ -471,33 +469,21 @@ export const CreatePage = () => { } // Send DMs to signers and viewers with the file URL - const sendDMs = async (fileUrl: string, encryptionKey: string) => { + const sendDMs = async (fileUrl: string, meta: Meta) => { setLoadingSpinnerDesc('Sending DM to signers/viewers') const signers = users.filter((user) => user.role === UserRole.signer) const viewers = users.filter((user) => user.role === UserRole.viewer) - if (signers.length > 0) { - await sendDM( - fileUrl, - encryptionKey, - signers[0].pubkey, - nostrController, - true, - setAuthUrl - ) - } else { - for (const viewer of viewers) { - await sendDM( - fileUrl, - encryptionKey, - viewer.pubkey, - nostrController, - false, - setAuthUrl - ) - } - } + const receivers = + signers.length > 0 + ? [signers[0].pubkey] + : viewers.map((viewer) => viewer.pubkey) + + const promises = receivers.map((receiver) => + sendNotification(receiver, meta, fileUrl) + ) + await Promise.allSettled(promises) } // Manage offline scenarios for signing or viewing the file @@ -524,21 +510,30 @@ export const CreatePage = () => { const fileHashes = await generateFileHashes() if (!fileHashes) return - const createZipResponse = await createZipFile(fileHashes) + const encryptionKey = await generateEncryptionKey() + + const createZipResponse = await initZipFileAndCreatorSignature( + encryptionKey, + fileHashes + ) if (!createZipResponse) return const { zip, createSignature } = createZipResponse - const metaHash = await addMetaToZip(zip, createSignature) - if (!metaHash) return + // create content for meta file + const meta: Meta = { + uuid, + title, + modifiedAt: now(), + createSignature, + docSignatures: {} + } 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, @@ -546,12 +541,11 @@ export const CreatePage = () => { ) if (await isOnline()) { - await handleOnlineFlow(encryptedArrayBuffer, encryptionKey) + await handleOnlineFlow(encryptedArrayBuffer, meta) } else { + // todo: fix offline flow await handleOfflineFlow(encryptedArrayBuffer, encryptionKey) } - - navigate(appPrivateRoutes.sign, { state: { arrayBuffer } }) } if (authUrl) { diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index 181b67d..30d39db 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -1,21 +1,59 @@ -import { - Add, - CalendarMonth, - Description, - PersonOutline, - Upload -} from '@mui/icons-material' +import { Add, CalendarMonth, Description, Upload } from '@mui/icons-material' import { Box, Button, Tooltip, Typography } from '@mui/material' +import JSZip from 'jszip' +import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' +import { toast } from 'react-toastify' import { appPrivateRoutes, appPublicRoutes } from '../../routes' import styles from './style.module.scss' -import { useRef } from 'react' -import JSZip from 'jszip' -import { toast } from 'react-toastify' +import { MetadataController, NostrController } from '../../controllers' +import { + formatTimestamp, + getUsersAppData, + hexToNpub, + npubToHex, + parseJson, + shorten +} from '../../utils' +import { LoadingSpinner } from '../../components/LoadingSpinner' +import { + CreateSignatureEventContent, + Meta, + ProfileMetadata, + Sigit +} from '../../types' +import { Event, kinds, verifyEvent } from 'nostr-tools' +import { UserComponent } from '../../components/username' export const HomePage = () => { const navigate = useNavigate() const fileInputRef = useRef(null) + const [isLoading, setIsLoading] = useState(true) + const [loadingSpinnerDesc] = useState(`Finding user's app data`) + const [authUrl, setAuthUrl] = useState() + const [sigits, setSigits] = useState([]) + + const [profiles, setProfiles] = useState<{ [key: string]: ProfileMetadata }>( + {} + ) + + useEffect(() => { + const nostrController = NostrController.getInstance() + // Set up event listener for authentication event + nostrController.on('nsecbunker-auth', (url) => { + setAuthUrl(url) + }) + + getUsersAppData() + .then((res) => { + if (res) { + setSigits(Object.values(res)) + } + }) + .finally(() => { + setIsLoading(false) + }) + }, []) const handleUploadClick = () => { if (fileInputRef.current) { @@ -63,81 +101,193 @@ export const HomePage = () => { } } + if (authUrl) { + return ( +