From b5c6e6c91d19500da4a6ebca2db6e75cff0e6614 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 12 Apr 2026 02:40:42 +0530 Subject: [PATCH] feat: add canvas export methods and implement encrypted letter submission to the letters endpoint --- frontend/src/components/ui/ComposeCanvas.tsx | 23 ++++++- frontend/src/config/endpoints.ts | 1 + frontend/src/pages/Editor.tsx | 71 +++++++++++++++++++- 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/ui/ComposeCanvas.tsx b/frontend/src/components/ui/ComposeCanvas.tsx index e716751..dc38c01 100644 --- a/frontend/src/components/ui/ComposeCanvas.tsx +++ b/frontend/src/components/ui/ComposeCanvas.tsx @@ -4,7 +4,10 @@ import { forwardRef, useEffect, useImperativeHandle, useRef } from "react"; const PAD = 36; export type CanvasTools = { - addImage: (url: string) => void; + addImage: (url: string, file: File) => void; + getData: () => { objects: any }; + getJsonData: () => string; + getImages: () => { src: string; file: File }[]; }; export const ComposeCanvas = forwardRef((_props, ref) => { @@ -130,11 +133,12 @@ export const ComposeCanvas = forwardRef((_props, ref) => { }, []); useImperativeHandle(ref, () => ({ - addImage: (url: string) => { + addImage: (url: string, file: File) => { if (!fabricRef.current) return; fabric.FabricImage.fromURL(url).then((img) => { img.scaleToWidth(300); img.set({ + _customRawFile: file, left: PAD, top: PAD, }); @@ -145,6 +149,21 @@ export const ComposeCanvas = forwardRef((_props, ref) => { URL.revokeObjectURL(url); // cleanup browser upload }); }, + getData: () => { + if (!fabricRef.current) return ""; + return fabricRef.current.toJSON(); + }, + getJsonData: () => { + if (!fabricRef.current) return ""; + return JSON.stringify(fabricRef.current.toJSON()); // convert to json string + }, + getImages: () => { + if (!fabricRef.current) return []; + return fabricRef.current.getObjects("Image").map((img: any) => ({ + src: img._element.currentSrc, + file: img._customRawFile, + })); + }, })); return ( diff --git a/frontend/src/config/endpoints.ts b/frontend/src/config/endpoints.ts index 07915aa..2712430 100644 --- a/frontend/src/config/endpoints.ts +++ b/frontend/src/config/endpoints.ts @@ -6,6 +6,7 @@ export const endpoints = { ME: "/api/auth/me/", REFRESH: "/api/auth/refresh/", LOGOUT: "/api/auth/logout/", + LETTERS: "/api/letters/", }; // simple utility to handle path params diff --git a/frontend/src/pages/Editor.tsx b/frontend/src/pages/Editor.tsx index 651a5d1..5ed6fa2 100644 --- a/frontend/src/pages/Editor.tsx +++ b/frontend/src/pages/Editor.tsx @@ -1,13 +1,19 @@ import { ImageIcon, LockIcon, TrayIcon } from "@phosphor-icons/react"; +import type { FabricObject } from "fabric"; import { useRef, useState } from "react"; +import { api } from "../api/apiClient"; import { type CanvasTools, ComposeCanvas, } from "../components/ui/ComposeCanvas"; import DateDisplay from "../components/ui/DateDisplay"; +import { endpoints } from "../config/endpoints"; +import { useKeyStore } from "../store/useKeyStore"; +import { CryptoUtils } from "../utils/crypto"; export default function Editor() { const [recipient, setRecipient] = useState(""); + const masterKey = useKeyStore.getState().masterKey; const canvasRef = useRef(null); const fileInputRef = useRef(null); @@ -15,10 +21,72 @@ export default function Editor() { const file = e.target.files?.[0]; // pick one file at a time if (file) { const url = URL.createObjectURL(file); - canvasRef.current?.addImage(url); + canvasRef.current?.addImage(url, file); } }; + async function handleSeal(): Promise { + const cryptoUtils = new CryptoUtils(); + await cryptoUtils.initialize(); + + const images = canvasRef.current?.getImages() || []; + const imageEncMap = new Map(); + const encImageFilesMap = new Map(); + + if (!masterKey) { + throw new Error("Master key is not initialized"); + } + + for (const image of images) { + const encrypted_image = await cryptoUtils.encryptImage( + image.file, + masterKey, + ); + imageEncMap.set(image.src, encrypted_image.filename); + encImageFilesMap.set( + encrypted_image.filename, + encrypted_image.encryptedBlob, + ); + } + + // replace image src with encrypted image filename + const canvasData = canvasRef.current?.getData() ?? { objects: [] }; + canvasData.objects = canvasData.objects?.map( + (obj: FabricObject & { src: string }) => + obj.type === "Image" ? { ...obj, src: imageEncMap.get(obj.src) } : obj, + ); + + const encrypted_letter = await cryptoUtils.encryptLetter( + JSON.stringify(canvasData), + masterKey, + ); + const encrypted_metadata = ""; + + // upload to server + + // sample payload + /* + payload = { + "type": "SENT", + "status": "SEALED", + "encrypted_content": "enc_content==", + "encrypted_metadata": "enc_metadata==", + "encrypted_dek": "enc_dek==", + "image_files": [image1, image2], + } + */ + const formData = new FormData(); + formData.append("type", "SENT"); + formData.append("status", "SEALED"); + formData.append("encrypted_content", encrypted_letter.encrypted_content); + formData.append("encrypted_dek", encrypted_letter.encrypted_dek); + formData.append("encrypted_metadata", encrypted_metadata); + encImageFilesMap.forEach((image, filename) => { + formData.append("image_files", image, filename); + }); + await api.post(endpoints.LETTERS, formData); + } + return (
@@ -78,6 +146,7 @@ export default function Editor() {