mirror of
https://github.com/ramvignesh-b/pi-ku.git
synced 2026-05-04 08:56:52 +00:00
feat: add canvas export methods and implement encrypted letter submission to the letters endpoint
This commit is contained in:
@@ -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 (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user