From e3d350c909aad4a2db6f47ee955abb6e0d931f53 Mon Sep 17 00:00:00 2001 From: Bhoomika S Date: Wed, 21 Jan 2026 19:40:37 -0800 Subject: [PATCH 1/3] feat: add clint-side end-to-end encryption for notes --- savebook/components/notes/AddNote.js | 744 +++++--------------------- savebook/components/notes/NoteItem.js | 480 ++++++----------- savebook/context/NoteState.js | 237 ++++---- 3 files changed, 410 insertions(+), 1051 deletions(-) diff --git a/savebook/components/notes/AddNote.js b/savebook/components/notes/AddNote.js index bde454c..029fb0b 100644 --- a/savebook/components/notes/AddNote.js +++ b/savebook/components/notes/AddNote.js @@ -1,616 +1,146 @@ -"use client" -import noteContext from '@/context/noteContext'; -import React, { useContext, useState } from 'react' -import toast from 'react-hot-toast'; -import Modal from '../common/Modal'; -import AudioRecorder from '@/components/AudioRecorder'; -import AudioPlayer from '@/components/AudioPlayer'; +"use client"; -// Define Note Templates +import React, { useContext, useState } from "react"; +import noteContext from "@/context/noteContext"; +import toast from "react-hot-toast"; +import { encrypt } from "@/lib/utils/crypto"; + +/* Note templates */ const NOTE_TEMPLATES = { - meeting: `Date: [Insert Date]\n\nAttendees: [List attendees]\n\nAgenda:\n- \n- \n- \n\nNotes:\n\nAction Items:\n- \n- `, - journal: `What happened today:\n[Write your experiences here]\n\nGoals for tomorrow:\n[List your goals]\n\nGratitude:\n[Write things you're grateful for]`, - checklist: `Project: [Project Name]\n\nTasks:\n- [ ] Define project goal\n- [ ] List all tasks\n- [ ] Assign owners\n- [ ] Set deadlines\n- [ ] Plan resources\n- [ ] Review progress\n- [ ] Complete project` + meeting: `Date:\n\nAttendees:\n\nAgenda:\n- \n- \n\nNotes:\n\nAction Items:\n- `, + journal: `What happened today:\n\nGoals for tomorrow:\n\nGratitude:\n`, + checklist: `Tasks:\n- [ ] Task 1\n- [ ] Task 2\n- [ ] Task 3`, }; -export default function Addnote() { - const context = useContext(noteContext); - const { addNote, notes } = context; - - const [note, setNote] = useState({ title: "", description: "", tag: "" }); - const [isSubmitting, setIsSubmitting] = useState(false); - - // Modal State - const [showModal, setShowModal] = useState(false); - const [pendingTemplate, setPendingTemplate] = useState(null); - const defaultTags = [ - "General", - "Basic", - "Finance", - "Grocery", - "Office", - "Personal", - "Work", - "Ideas" - ]; - const [images, setImages] = useState([]); - const [preview, setPreview] = useState([]); - - // Audio state management - const [audioBlob, setAudioBlob] = useState(null); - const [recordedAudioUrl, setRecordedAudioUrl] = useState(null); - const [audioData, setAudioData] = useState(null); - const [isUploadingAudio, setIsUploadingAudio] = useState(false); - - const handleImageChange = (e) => { - const files = Array.from(e.target.files); - - setImages(files); - setPreview(files.map(file => URL.createObjectURL(file))); - }; - - - const uploadImages = async () => { - const formData = new FormData(); - images.forEach((file) => formData.append("image", file)); - - const res = await fetch("/api/upload/user-media", { - method: "POST", - credentials: "include", - body: formData, - }); - - if (!res.ok) { - throw new Error("Image upload failed"); - } - - const data = await res.json(); - return Array.isArray(data.imageUrls) ? data.imageUrls : []; - }; - - // Handle audio recording from AudioRecorder component - const handleAudioRecorded = (blob) => { - setAudioBlob(blob); - // Create temporary URL for preview - const url = URL.createObjectURL(blob); - setRecordedAudioUrl(url); - }; - - // Upload audio to API - const uploadAudio = async (blob) => { - const formData = new FormData(); - formData.append('audio', blob, 'recording.webm'); - - const res = await fetch('/api/upload/audio', { - method: 'POST', - credentials: 'include', - body: formData, - }); - - if (!res.ok) { - // Log full error response for debugging - let errorMessage = `HTTP ${res.status}`; - const contentType = res.headers.get('content-type'); - - try { - // Try to parse as JSON first - if (contentType?.includes('application/json')) { - const errorData = await res.json(); - errorMessage = errorData.error || errorData.message || errorMessage; - } else { - // Fallback to text - const errorText = await res.text(); - errorMessage = errorText.slice(0, 200) || errorMessage; - } - } catch (parseError) { - // If parsing fails, use status code - console.error('Failed to parse error response:', parseError); - } - - console.error('Audio upload error:', { status: res.status, error: errorMessage }); - throw new Error(`Audio upload failed: ${errorMessage}`); - } - - const data = await res.json(); - return { - url: data.audioUrl, - duration: data.duration || 0, - }; - }; - - // Clear audio recording - const clearAudioRecording = () => { - if (recordedAudioUrl) { - URL.revokeObjectURL(recordedAudioUrl); - } - setAudioBlob(null); - setRecordedAudioUrl(null); - setAudioData(null); - }; - - - - - - - - const handleSaveNote = async (e) => { - e.preventDefault(); - if (isSubmitting) return; - - setIsSubmitting(true); - try { - // Upload images - const imageUrls = images.length ? await uploadImages() : []; - - // Upload audio if recording exists - REQUIRED before note creation - let finalAudioData = null; - if (audioBlob) { - setIsUploadingAudio(true); - try { - finalAudioData = await uploadAudio(audioBlob); - } catch (audioError) { - console.error('Audio upload error:', audioError); - toast.error(audioError.message || 'Audio upload failed. Please try again.'); - // Abort note creation - do NOT save note without audio - return; - } - } - - // Save note with audio data (if available) - await addNote( - note.title, - note.description, - note.tag, - imageUrls, - finalAudioData // Pass audio data (or null) - ); - - toast.success("Note has been saved"); - setNote({ title: "", description: "", tag: "" }); - setImages([]); - setPreview([]); - clearAudioRecording(); // Only clear after successful save - } catch (error) { - console.error('Error saving note:', error); - toast.error(error.message || "Failed to save note. Please try again."); - } finally { - setIsSubmitting(false); - setIsUploadingAudio(false); - } - }; - - - - - - const onchange = (e) => { - setNote({ ...note, [e.target.name]: e.target.value }); - } - - // Apply Template Handler - const applyTemplate = (templateKey) => { - const template = NOTE_TEMPLATES[templateKey]; - if (!template) return; - - // If description is empty, directly apply the template - if (!note.description.trim()) { - setNote({ ...note, description: template }); - toast.success(`${templateKey.charAt(0).toUpperCase() + templateKey.slice(1)} template applied!`); - } else { - // If description has content, trigger modal - setPendingTemplate({ key: templateKey, content: template }); - setShowModal(true); - } - } - - const confirmTemplateChange = () => { - if (pendingTemplate) { - setNote({ ...note, description: pendingTemplate.content }); - toast.success(`${pendingTemplate.key.charAt(0).toUpperCase() + pendingTemplate.key.slice(1)} template applied!`); - handleCloseModal(); - } +export default function AddNote() { + const { addNote, notes } = useContext(noteContext); + + const [note, setNote] = useState({ title: "", description: "", tag: "" }); + const [isSubmitting, setIsSubmitting] = useState(false); + + const defaultTags = [ + "General", + "Basic", + "Finance", + "Grocery", + "Office", + "Personal", + "Work", + "Ideas", + ]; + + const handleChange = (e) => { + setNote({ ...note, [e.target.name]: e.target.value }); + }; + + const handleSaveNote = async (e) => { + e.preventDefault(); + if (isSubmitting) return; + + setIsSubmitting(true); + + try { + const secret = localStorage.getItem("encryptionKey"); + if (!secret) { + toast.error("Encryption key missing. Please log in again."); + return; + } + + const encryptedTitle = encrypt(note.title, secret); + const encryptedDescription = encrypt(note.description, secret); + + await addNote(encryptedTitle, encryptedDescription, note.tag); + + toast.success("Note saved"); + setNote({ title: "", description: "", tag: "" }); + } catch (err) { + toast.error("Failed to save note"); + } finally { + setIsSubmitting(false); } + }; - const handleCloseModal = () => { - setShowModal(false); - setPendingTemplate(null); + const applyTemplate = (key) => { + if (NOTE_TEMPLATES[key]) { + setNote({ ...note, description: NOTE_TEMPLATES[key] }); + toast.success("Template applied"); } - // Collect unique tags from existing notes - const userTags = Array.from( - new Set( - (Array.isArray(notes) ? notes : []) - .map(note => note.tag) - .filter(tag => tag && tag.trim() !== "") - ) - ); - - const allTags = Array.from(new Set([...defaultTags, ...userTags])); - const isFormValid = note.title.length >= 5 && note.description.length >= 5 && note.tag.length >= 2; - - return ( -
-
- {/* Header */} -
-

- Notebook on the Cloud -

-

- Your notes, securely stored and accessible anywhere -

-
- - {/* Add Note Form */} -
-

- Add New Note -

- -
- {/* Title Field */} -
- - -

- Minimum 5 characters required -

-
- - {/* Description Field with Templates */} -
-
- - - Quick Templates: - -
- - {/* Templates Row - Quick Start Buttons */} -
- - - - - -
- -