From df96cead938453eca77ae944cd8c8271a011546f Mon Sep 17 00:00:00 2001 From: ramvignesh-b Date: Wed, 29 Apr 2026 23:09:32 +0530 Subject: [PATCH] refactor: simplify letterlogic by removing object mutation --- frontend/src/api/apiClient.ts | 19 +-- frontend/src/hooks/useLetters.tsx | 15 +- frontend/src/pages/Editor.tsx | 112 +++++++----- frontend/src/utils/letterLogic.test.ts | 57 +++---- frontend/src/utils/letterLogic.ts | 227 ++++++++++++++++--------- 5 files changed, 266 insertions(+), 164 deletions(-) diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts index 93c3361..79b49ff 100644 --- a/frontend/src/api/apiClient.ts +++ b/frontend/src/api/apiClient.ts @@ -2,19 +2,19 @@ import axios from "axios"; import { endpoints } from "../config/endpoints"; import { useAuthStore } from "../store/useAuthStore"; +export const apiServerUrl = import.meta.env.VITE_API_URL; + // publicApi for endpoints that don't need authentication (login, refresh, register) export const publicApi = axios.create({ - baseURL: import.meta.env.VITE_API_URL, + baseURL: apiServerUrl, withCredentials: true, }); // api for all authenticated requests export const api = axios.create({ - baseURL: import.meta.env.VITE_API_URL, + baseURL: apiServerUrl, withCredentials: true, }); - -// auto-attach access token to authenticated requests api.interceptors.request.use((config) => { const token = useAuthStore.getState().accessToken; if (token) { @@ -22,29 +22,28 @@ api.interceptors.request.use((config) => { } return config; }); - -// Handle 401 errors by attempting a silent refresh +// auto handle 401 errors by attempting a silent refresh api.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; - // If 401 and we haven't tried refreshing yet + // if first time 401 and we haven't tried refreshing yet, we proceed with silent refresh + // else it could mean the refresh also 401'd if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; try { - // Attempt silent refresh const { data } = await publicApi.post(endpoints.REFRESH); const newAccessToken = data.access; - // Update store + // Update store with the latest accesstoken const { user, setAuth } = useAuthStore.getState(); if (user) { setAuth(newAccessToken, user); } - // Retry the original request with the new token + // retry the original request with the new token originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; return api(originalRequest); } catch (refreshError) { diff --git a/frontend/src/hooks/useLetters.tsx b/frontend/src/hooks/useLetters.tsx index 5f8e657..154c2d1 100644 --- a/frontend/src/hooks/useLetters.tsx +++ b/frontend/src/hooks/useLetters.tsx @@ -25,7 +25,7 @@ export interface ProcessedLetter extends Letter { metadata: LetterMetadata; } -async function decryptLetters( +async function decryptLettersMetadata( letters: Letter[], masterKey: CryptoKey, ): Promise { @@ -56,19 +56,22 @@ async function decryptLetters( export function useLetters() { const [letters, setLetters] = useState([]); const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); const [isAuthRequired, setIsAuthRequired] = useState(false); const { masterKey } = useKeyStore(); + // to fetch the letters and decryypt the metadata on load useEffect(() => { if (!masterKey) { setIsAuthRequired(true); return; } setIsAuthRequired(false); + setError(null); setLoading(true); api .get(endpoints.LETTERS) - .then((res) => decryptLetters(res.data, masterKey)) + .then((res) => decryptLettersMetadata(res.data, masterKey)) .then((decrypted) => { setLetters( decrypted.sort( @@ -78,7 +81,9 @@ export function useLetters() { ), ); }) - .catch((_err) => {}) + .catch((err) => { + setError(err); + }) .finally(() => setLoading(false)); }, [masterKey]); @@ -91,6 +96,10 @@ export function useLetters() { }; }, [letters]); + if (error) { + throw error; + } + return { ...drawerItems, loading, diff --git a/frontend/src/pages/Editor.tsx b/frontend/src/pages/Editor.tsx index 2de8ce2..bb0e047 100644 --- a/frontend/src/pages/Editor.tsx +++ b/frontend/src/pages/Editor.tsx @@ -33,11 +33,12 @@ import { CryptoUtils } from "../utils/crypto"; import { formatRelativeDate } from "../utils/dateFormat"; import { decryptCanvasImages, encryptCanvasImages } from "../utils/letterLogic"; -type SaveOverlay = "idle" | "saving" | "saved" | "error"; +type SaveOverlay = "IDLE" | "SAVING" | "SAVED" | "ERROR"; const OVERLAY_FADE_MS = 250; const SAVED_VISIBLE_MS = 1400; const ERROR_VISIBLE_MS = 2400; +const STOP_SAVE_DATE_PULSE_AFTER_MS = 10000; const toPlaceholderList = [ "Someone dear...", @@ -45,6 +46,7 @@ const toPlaceholderList = [ "Something to bear...", ]; +const MAX_FILE_SIZE = 10 * 1024 * 1024; export default function Editor() { const navigate = useNavigate(); const navigateRef = useRef(navigate); @@ -70,7 +72,14 @@ export default function Editor() { const [lastSavedPulseTick, setLastSavedPulseTick] = useState(0); const [sealBtnClicked, setSealBtnClicked] = useState(false); - const [saveOverlay, setSaveOverlay] = useState("idle"); + const [saveOverlay, setSaveOverlay] = useState("IDLE"); + const [logStatus, setLogStatus] = useState<{ + status: "WARN" | "ERROR" | "RESET"; + message: string; + }>({ + status: "RESET", + message: "", + }); const [showSaveOverlay, setShowSaveOverlay] = useState(false); const [confirmModal, setConfirmModal] = useState<"VAULT" | "SEAL" | null>( null, @@ -85,7 +94,7 @@ export default function Editor() { const canvasRef = useRef(null); const fileInputRef = useRef(null); - // Placeholder rotation + // to continuously rotate placeholder text of the recipient input useEffect(() => { const interval = setInterval(() => { setPlaceholderIndex((prev) => (prev + 1) % toPlaceholderList.length); @@ -94,13 +103,14 @@ export default function Editor() { return () => clearInterval(interval); }, []); + // to load existing letter when public_id param and masterKey is available + // NOTE: this has to trigger just once after each save useEffect(() => { if (!(public_id && masterKey)) return; if (justSavedRef.current) { justSavedRef.current = false; return; } - const loadExistingLetter = async () => { setIsInitialLoading(true); const cryptoUtils = new CryptoUtils(); @@ -139,26 +149,27 @@ export default function Editor() { ); const canvasData = JSON.parse(decryptedJsonStr); - const { isDecryptionPartialFailure, error } = await decryptCanvasImages( - canvasData, - letterData.images ?? [], - letterData.encrypted_dek, - masterKey, - cryptoUtils, - true, - ); + const { errors, isPartialFailure, canvasDataWithDecryptedImages } = + await decryptCanvasImages( + canvasData, + letterData.images ?? [], + letterData.encrypted_dek, + masterKey, + cryptoUtils, + true, + ); - if (isDecryptionPartialFailure) { + if (isPartialFailure) { setDecryptionStatus({ status: "WARN", message: "Failed to decrypt some elements. Please check the render.", - log: error, + log: errors.toString(), }); } if (canvasRef.current) { - await canvasRef.current.loadData(canvasData); + await canvasRef.current.loadData(canvasDataWithDecryptedImages); } } catch (_err) { setDecryptionStatus({ @@ -170,37 +181,36 @@ export default function Editor() { setIsInitialLoading(false); } }; - loadExistingLetter(); }, [public_id, masterKey]); + // to trigger short pulse animation for Last Saved AT element useEffect(() => { if (lastSavedPulseTick === 0) return; - setIsSaveDatePulsing(true); const timer = setTimeout(() => { setIsSaveDatePulsing(false); - }, 10000); + }, STOP_SAVE_DATE_PULSE_AFTER_MS); return () => clearTimeout(timer); }, [lastSavedPulseTick]); + // to fade in and fade out the save status overlay after each save operation + // Note: otherwise the fade efect is abrupt due to component's immediate unmount useEffect(() => { - if (saveOverlay === "idle" || saveOverlay === "saving") return; - + if (saveOverlay === "IDLE" || saveOverlay === "SAVING") return; const visibleTimer = setTimeout( () => { setShowSaveOverlay(false); }, - saveOverlay === "saved" ? SAVED_VISIBLE_MS : ERROR_VISIBLE_MS, + saveOverlay === "SAVED" ? SAVED_VISIBLE_MS : ERROR_VISIBLE_MS, ); - const unmountTimer = setTimeout( () => { - setSaveOverlay("idle"); + setSaveOverlay("IDLE"); }, - (saveOverlay === "saved" ? SAVED_VISIBLE_MS : ERROR_VISIBLE_MS) + + (saveOverlay === "SAVED" ? SAVED_VISIBLE_MS : ERROR_VISIBLE_MS) + OVERLAY_FADE_MS, ); @@ -212,9 +222,14 @@ export default function Editor() { const handleImageUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; - if (file) { + if (file && file.size < MAX_FILE_SIZE) { const url = URL.createObjectURL(file); canvasRef.current?.addImage(url, file); + } else { + setLogStatus({ + status: "WARN", + message: "Please upload images with size less than 10MB.", + }); } }; @@ -229,9 +244,9 @@ export default function Editor() { targetId = crypto.randomUUID(); } - if (saveOverlay === "saving" || !masterKey) return; + if (saveOverlay === "SAVING" || !masterKey) return; - setSaveOverlay("saving"); + setSaveOverlay("SAVING"); setShowSaveOverlay(true); const cryptoUtils = new CryptoUtils(); @@ -241,15 +256,16 @@ export default function Editor() { const canvasData = canvasRef.current?.getData() || { objects: [] }; const canvasImages = canvasRef.current?.getImages() || []; - const encImageFilesMap = await encryptCanvasImages( - canvasData, - canvasImages, - masterKey, - cryptoUtils, - ); + const { encryptedImageFiles, encryptedCanvasData } = + await encryptCanvasImages( + canvasData, + canvasImages, + masterKey, + cryptoUtils, + ); const encrypted_letter = await cryptoUtils.encryptLetter( - JSON.stringify(canvasData), + JSON.stringify(encryptedCanvasData), masterKey, ); @@ -278,7 +294,7 @@ export default function Editor() { encrypted_metadata.encrypted_content, ); - encImageFilesMap.forEach((blob, filename) => { + encryptedImageFiles.forEach((blob, filename) => { formData.append("image_files", blob, filename); }); @@ -297,10 +313,10 @@ export default function Editor() { if (status === "SEALED" || status === "VAULT") { setSealedTargetId(targetId); } - setSaveOverlay("saved"); + setSaveOverlay("SAVED"); setShowSaveOverlay(true); } catch (_error) { - setSaveOverlay("error"); + setSaveOverlay("ERROR"); setShowSaveOverlay(true); } }; @@ -356,9 +372,9 @@ export default function Editor() { )} - {saveOverlay !== "idle" && ( + {saveOverlay !== "IDLE" && ( - {saveOverlay === "saving" && ( + {saveOverlay === "SAVING" && (
)} - {saveOverlay === "saved" && ( + {saveOverlay === "SAVED" && (
)} - {saveOverlay === "error" && ( + {saveOverlay === "ERROR" && (
@@ -466,6 +482,18 @@ export default function Editor() {
+ + setLogStatus({ + status: "RESET", + message: "", + }) + } + isOpen={logStatus.status !== "RESET"} + /> ); } diff --git a/frontend/src/utils/letterLogic.test.ts b/frontend/src/utils/letterLogic.test.ts index 139c85e..0cf634f 100644 --- a/frontend/src/utils/letterLogic.test.ts +++ b/frontend/src/utils/letterLogic.test.ts @@ -12,6 +12,7 @@ vi.mock("../api/apiClient", () => ({ api: { get: vi.fn(), }, + apiServerUrl: "https://remote", })); vi.mock("./fileUtils", () => ({ @@ -21,7 +22,6 @@ vi.mock("./fileUtils", () => ({ describe("letterLogic image helpers", () => { let masterKey: CryptoKey; let crypto: CryptoUtils; - beforeEach(async () => { const keyBundle = await CryptoUtils.deriveKeyBundle( "password123", @@ -58,15 +58,13 @@ describe("letterLogic image helpers", () => { const encryptImageSpy = vi.spyOn(CryptoUtils.prototype, "encryptImage"); - const uploads = await encryptCanvasImages( - canvasData, - [], - masterKey, - crypto, - ); + const { encryptedImageFiles: uploads, encryptedCanvasData } = + await encryptCanvasImages(canvasData, [], masterKey, crypto); expect(encryptImageSpy).not.toHaveBeenCalled(); - expect(canvasData.objects[0].src).toBe("already-encrypted.png.bin"); + expect(encryptedCanvasData.objects[0].src).toBe( + "already-encrypted.png.bin", + ); expect(uploads.size).toBe(0); }); @@ -99,15 +97,11 @@ describe("letterLogic image helpers", () => { filename: "photo.png.bin", }); - const uploads = await encryptCanvasImages( - canvasData, - canvasImages, - masterKey, - crypto, - ); + const { encryptedImageFiles: uploads, encryptedCanvasData } = + await encryptCanvasImages(canvasData, canvasImages, masterKey, crypto); expect(CryptoUtils.prototype.encryptImage).toHaveBeenCalledTimes(1); - expect(canvasData.objects[0].src).toBe("photo.png.bin"); + expect(encryptedCanvasData.objects[0].src).toBe("photo.png.bin"); expect(uploads.size).toBe(1); expect(uploads.has("photo.png.bin")).toBe(true); }); @@ -136,7 +130,7 @@ describe("letterLogic image helpers", () => { ], }; const remoteImages = [ - { file_name: "photo.png.bin", file: "https://remote/photo.png.bin" }, + { file_name: "photo.png.bin", file: `https://remote/photo.png.bin` }, ]; vi.mocked(api.get).mockResolvedValue({ data: new Blob(["encrypted"]) }); @@ -144,7 +138,7 @@ describe("letterLogic image helpers", () => { "blob:http://localhost/decrypted", ); - await decryptCanvasImages( + const { canvasDataWithDecryptedImages } = await decryptCanvasImages( canvasData, remoteImages, "wrapped-dek", @@ -153,7 +147,7 @@ describe("letterLogic image helpers", () => { ); expect(api.get).toHaveBeenCalledWith( - "https://remote/photo.png.bin", + `https://remote/photo.png.bin`, expect.objectContaining({ responseType: "blob" }), ); expect(CryptoUtils.prototype.decryptImage).toHaveBeenCalledWith( @@ -161,8 +155,10 @@ describe("letterLogic image helpers", () => { "wrapped-dek", masterKey, ); - expect(canvasData.objects[0].src).toBe("blob:http://localhost/decrypted"); - expect(canvasData.objects[1].text).toBe("hello"); + expect(canvasDataWithDecryptedImages.objects[0].src).toBe( + "blob:http://localhost/decrypted", + ); + expect(canvasDataWithDecryptedImages.objects[1].text).toBe("hello"); }); it("should include raw file when includeRawFile is true", async () => { @@ -191,7 +187,7 @@ describe("letterLogic image helpers", () => { new File(["raw"], "photo.png.bin"), ); - await decryptCanvasImages( + const { canvasDataWithDecryptedImages } = await decryptCanvasImages( canvasData, remoteImages, "wrapped-dek", @@ -204,7 +200,9 @@ describe("letterLogic image helpers", () => { "blob:http://localhost/decrypted", "photo.png.bin", ); - expect(canvasData.objects[0]._customRawFile).toBeInstanceOf(File); + expect( + canvasDataWithDecryptedImages.objects[0]._customRawFile, + ).toBeInstanceOf(File); }); }); @@ -232,12 +230,13 @@ describe("letterLogic image helpers", () => { "decryptImageWithSharingKey", ).mockResolvedValue("blob:http://localhost/decrypted-shared"); - await decryptCanvasImagesWithSharingKey( - canvasData, - remoteImages, - "raw-sharing-key", - crypto, - ); + const { canvasDataWithDecryptedImages } = + await decryptCanvasImagesWithSharingKey( + canvasData, + remoteImages, + "raw-sharing-key", + crypto, + ); expect(api.get).toHaveBeenCalledWith( "https://remote/photo.png.bin", @@ -246,7 +245,7 @@ describe("letterLogic image helpers", () => { expect( CryptoUtils.prototype.decryptImageWithSharingKey, ).toHaveBeenCalledWith(expect.any(Blob), "raw-sharing-key"); - expect(canvasData.objects[0].src).toBe( + expect(canvasDataWithDecryptedImages.objects[0].src).toBe( "blob:http://localhost/decrypted-shared", ); }); diff --git a/frontend/src/utils/letterLogic.ts b/frontend/src/utils/letterLogic.ts index 43c7c99..acc1f17 100644 --- a/frontend/src/utils/letterLogic.ts +++ b/frontend/src/utils/letterLogic.ts @@ -1,4 +1,4 @@ -import { api } from "../api/apiClient"; +import { api, apiServerUrl, publicApi } from "../api/apiClient"; import type { CanvasJSON, FabricImageJSON, @@ -11,6 +11,35 @@ export interface CanvasImageRef { file: File; } +export interface DecryptedFabricImageJSON extends FabricImageJSON { + _customRawFile?: File; +} + +export interface DecryptionResult { + canvasDataWithDecryptedImages: CanvasJSON; + isPartialFailure: boolean; + errors: string[]; +} + +export interface EncryptionResult { + encryptedImageFiles: Map; + encryptedCanvasData: CanvasJSON; +} + +async function fetchEncryptedBlobFromRemote(remoteUrl: string): Promise { + // IF served statically from server, we need proper CORS setup + if (remoteUrl.includes(apiServerUrl)) { + const res = await api.get(remoteUrl, { responseType: "blob" }); + return res.data; + } + // Note: S3 Storage fetch (external url) has to bypass our existing CORS setup + const res = await publicApi.get(remoteUrl, { + responseType: "blob", + withCredentials: false, + }); + return res.data; +} + export async function decryptCanvasImages( canvasData: CanvasJSON, remoteImages: { file_name: string; file: string }[], @@ -18,51 +47,66 @@ export async function decryptCanvasImages( masterKey: CryptoKey, cryptoUtils: CryptoUtils, includeRawFile = false, -): Promise<{ isDecryptionPartialFailure: boolean; error: string }> { - if (!canvasData?.objects) - return { isDecryptionPartialFailure: false, error: "" }; - let isDecryptionPartialFailure = false; - let error = ""; +): Promise { + if (!canvasData?.objects) { + return { + canvasDataWithDecryptedImages: canvasData, + isPartialFailure: false, + errors: [], + }; + } const imageMap = new Map( remoteImages.map((img) => [img.file_name, img.file]), ); - const imageDecryptionPromises = canvasData.objects.map(async (obj, index) => { - if (obj.type !== "Image") return; - const imgObj = obj as FabricImageJSON; - const remoteUrl = imageMap.get(imgObj.src); - if (!remoteUrl) return; + const errors: string[] = []; + const processedObjects = await Promise.all( + canvasData.objects.map(async (obj) => { + if (obj.type !== "Image") return obj; - try { - // HACK: For S3 Storage fetch and avoiding CORS error - const res = await api.get(remoteUrl, { - responseType: "blob", - withCredentials: false, - }); - const originalSrc = imgObj.src; + const imgObj = obj as FabricImageJSON; + const remoteUrl = imageMap.get(imgObj.src); + if (!remoteUrl) return obj; - const blobUrl = await cryptoUtils.decryptImage( - res.data, - encrypted_dek, - masterKey, - ); + try { + const blob = await fetchEncryptedBlobFromRemote(remoteUrl); + const blobUrl = await cryptoUtils.decryptImage( + blob, + encrypted_dek, + masterKey, + ); - imgObj.src = blobUrl; + const decryptedObj: DecryptedFabricImageJSON = { + ...imgObj, + src: blobUrl, + }; - if (includeRawFile) { - imgObj._customRawFile = await blobUrlToFile(blobUrl, originalSrc); + if (includeRawFile) { + decryptedObj._customRawFile = await blobUrlToFile( + blobUrl, + imgObj.src, + ); + } + + return decryptedObj; + } catch (err) { + errors.push( + `Failed to decrypt ${imgObj.src}: ${err instanceof Error ? err.message : "Unknown error"}`, + ); + return null; } - } catch (_error) { - delete canvasData.objects[index]; - isDecryptionPartialFailure = true; - error = _error instanceof Error ? _error.message : "Unknown error"; - } - }); + }), + ); - await Promise.all(imageDecryptionPromises); - canvasData.objects = canvasData.objects.filter(Boolean); - return { isDecryptionPartialFailure, error }; + return { + canvasDataWithDecryptedImages: { + ...canvasData, + objects: processedObjects.filter((obj) => !!obj), + }, + isPartialFailure: errors.length > 0, + errors, + }; } export async function decryptCanvasImagesWithSharingKey( @@ -70,41 +114,53 @@ export async function decryptCanvasImagesWithSharingKey( remoteImages: { file_name: string; file: string }[], sharingKey: string, cryptoUtils: CryptoUtils, -): Promise<{ isDecryptionPartialFailure: boolean; error: string }> { - if (!canvasData?.objects) - return { isDecryptionPartialFailure: false, error: "" }; - let isDecryptionPartialFailure = false; - let error = ""; +): Promise { + if (!canvasData?.objects) { + return { + canvasDataWithDecryptedImages: canvasData, + isPartialFailure: false, + errors: [], + }; + } + const imageMap = new Map( remoteImages.map((img) => [img.file_name, img.file]), ); + const errors: string[] = []; - const decryptionPromises = canvasData.objects.map(async (obj, index) => { - if (obj.type !== "Image") return; + const processedObjects = await Promise.all( + canvasData.objects.map(async (obj) => { + if (obj.type !== "Image") return obj; - const imgObj = obj as FabricImageJSON; - const remoteUrl = imageMap.get(imgObj.src); - if (!remoteUrl) return; + const imgObj = obj as FabricImageJSON; + const remoteUrl = imageMap.get(imgObj.src); + if (!remoteUrl) return obj; - try { - const res = await api.get(remoteUrl, { - responseType: "blob", - withCredentials: false, - }); - imgObj.src = await cryptoUtils.decryptImageWithSharingKey( - res.data, - sharingKey, - ); - } catch (_error) { - delete canvasData.objects[index]; - isDecryptionPartialFailure = true; - error = _error instanceof Error ? _error.message : "Unknown error"; - } - }); + try { + const blob = await fetchEncryptedBlobFromRemote(remoteUrl); + const blobUrl = await cryptoUtils.decryptImageWithSharingKey( + blob, + sharingKey, + ); - await Promise.all(decryptionPromises); - canvasData.objects = canvasData.objects.filter(Boolean); - return { isDecryptionPartialFailure, error }; + return { ...imgObj, src: blobUrl }; + } catch (err) { + errors.push( + `Failed to decrypt ${imgObj.src}: ${err instanceof Error ? err.message : "Unknown error"}`, + ); + return null; + } + }), + ); + + return { + canvasDataWithDecryptedImages: { + ...canvasData, + objects: processedObjects.filter((obj) => !!obj), + }, + isPartialFailure: errors.length > 0, + errors, + }; } export async function encryptCanvasImages( @@ -112,23 +168,34 @@ export async function encryptCanvasImages( canvasImages: CanvasImageRef[], masterKey: CryptoKey, cryptoUtils: CryptoUtils, -) { - const encryptedFiles = new Map(); +): Promise { + const encryptedImageFiles = new Map(); const filenameMapping = new Map(); - for (const img of canvasImages) { - if (img.src.endsWith(".bin")) continue; - if (!img.file) continue; - const { filename, encryptedBlob } = await cryptoUtils.encryptImage( - img.file, - masterKey, - ); - filenameMapping.set(img.src, filename); - encryptedFiles.set(filename, encryptedBlob); - } + // filter out already encrypted images + const imagesToEncrypt = canvasImages.filter( + (img) => img.file && !img.src.endsWith(".bin"), + ); - if (canvasData?.objects) { - canvasData.objects = canvasData.objects.map((obj) => { + // encrypt images parallelly + await Promise.all( + imagesToEncrypt.map(async (img) => { + const { filename, encryptedBlob } = await cryptoUtils.encryptImage( + img.file, + masterKey, + ); + // map the og image url to the encrypted file name and filename to the encrypted source + filenameMapping.set(img.src, filename); + encryptedImageFiles.set(filename, encryptedBlob); + }), + ); + + if (!canvasData?.objects) + return { encryptedImageFiles, encryptedCanvasData: canvasData }; + + const newCanvasData = { + ...canvasData, + objects: canvasData.objects.map((obj) => { if (obj.type === "Image") { const imgObj = obj as FabricImageJSON; if (filenameMapping.has(imgObj.src)) { @@ -139,8 +206,8 @@ export async function encryptCanvasImages( } } return obj; - }); - } + }), + }; - return encryptedFiles; + return { encryptedImageFiles, encryptedCanvasData: newCanvasData }; }