feat: add canvas export methods and implement encrypted letter submission to the letters endpoint

This commit is contained in:
Your Name
2026-04-12 02:40:42 +05:30
parent 18bc8ac238
commit b5c6e6c91d
3 changed files with 92 additions and 3 deletions
+21 -2
View File
@@ -4,7 +4,10 @@ import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
const PAD = 36; const PAD = 36;
export type CanvasTools = { 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<CanvasTools>((_props, ref) => { export const ComposeCanvas = forwardRef<CanvasTools>((_props, ref) => {
@@ -130,11 +133,12 @@ export const ComposeCanvas = forwardRef<CanvasTools>((_props, ref) => {
}, []); }, []);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
addImage: (url: string) => { addImage: (url: string, file: File) => {
if (!fabricRef.current) return; if (!fabricRef.current) return;
fabric.FabricImage.fromURL(url).then((img) => { fabric.FabricImage.fromURL(url).then((img) => {
img.scaleToWidth(300); img.scaleToWidth(300);
img.set({ img.set({
_customRawFile: file,
left: PAD, left: PAD,
top: PAD, top: PAD,
}); });
@@ -145,6 +149,21 @@ export const ComposeCanvas = forwardRef<CanvasTools>((_props, ref) => {
URL.revokeObjectURL(url); // cleanup browser upload 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 ( return (
+1
View File
@@ -6,6 +6,7 @@ export const endpoints = {
ME: "/api/auth/me/", ME: "/api/auth/me/",
REFRESH: "/api/auth/refresh/", REFRESH: "/api/auth/refresh/",
LOGOUT: "/api/auth/logout/", LOGOUT: "/api/auth/logout/",
LETTERS: "/api/letters/",
}; };
// simple utility to handle path params // simple utility to handle path params
+70 -1
View File
@@ -1,13 +1,19 @@
import { ImageIcon, LockIcon, TrayIcon } from "@phosphor-icons/react"; import { ImageIcon, LockIcon, TrayIcon } from "@phosphor-icons/react";
import type { FabricObject } from "fabric";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { api } from "../api/apiClient";
import { import {
type CanvasTools, type CanvasTools,
ComposeCanvas, ComposeCanvas,
} from "../components/ui/ComposeCanvas"; } from "../components/ui/ComposeCanvas";
import DateDisplay from "../components/ui/DateDisplay"; 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() { export default function Editor() {
const [recipient, setRecipient] = useState(""); const [recipient, setRecipient] = useState("");
const masterKey = useKeyStore.getState().masterKey;
const canvasRef = useRef<CanvasTools>(null); const canvasRef = useRef<CanvasTools>(null);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
@@ -15,10 +21,72 @@ export default function Editor() {
const file = e.target.files?.[0]; // pick one file at a time const file = e.target.files?.[0]; // pick one file at a time
if (file) { if (file) {
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
canvasRef.current?.addImage(url); canvasRef.current?.addImage(url, file);
} }
}; };
async function handleSeal(): Promise<void> {
const cryptoUtils = new CryptoUtils();
await cryptoUtils.initialize();
const images = canvasRef.current?.getImages() || [];
const imageEncMap = new Map<string, string>();
const encImageFilesMap = new Map<string, Blob>();
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 ( return (
<section className="flex-1 overflow-y-auto scrollbar-hide px-2 py-12 bg-base-300"> <section className="flex-1 overflow-y-auto scrollbar-hide px-2 py-12 bg-base-300">
<div className="max-w-[720px] mx-auto px-1 md:px-0"> <div className="max-w-[720px] mx-auto px-1 md:px-0">
@@ -78,6 +146,7 @@ export default function Editor() {
<button <button
type="button" type="button"
className="btn btn-primary btn-sm rounded-full px-6" className="btn btn-primary btn-sm rounded-full px-6"
onClick={handleSeal}
> >
<LockIcon size={14} weight="fill" className="mr-1" /> <LockIcon size={14} weight="fill" className="mr-1" />
Seal Seal