diff --git a/savebook/app/api/notes/[id]/route.js b/savebook/app/api/notes/[id]/route.js index 4748eb3..595308a 100644 --- a/savebook/app/api/notes/[id]/route.js +++ b/savebook/app/api/notes/[id]/route.js @@ -37,7 +37,11 @@ export async function GET(request, { params }) { ); } - const note = await Notes.findOne({ _id: id, user: decoded.userId }); + const note = await Notes.findOne({ + _id: id, + user: decoded.userId, + isDeleted: false, // Exclude soft-deleted notes + }); if (!note) { return NextResponse.json( @@ -108,12 +112,20 @@ export async function DELETE(request, { params }) { ); } - // Delete the note - await Notes.findByIdAndDelete(id); + // Soft delete: mark as deleted instead of removing permanently + // User has 30 days to recover from trash before permanent deletion via TTL + await Notes.findByIdAndUpdate( + id, + { + isDeleted: true, + deletedAt: new Date(), + }, + { new: true } + ); return NextResponse.json({ success: true, - message: "Note deleted successfully" + message: "Note deleted. Recover from trash within 30 days." }); } catch (error) { console.error(error); diff --git a/savebook/app/api/notes/route.js b/savebook/app/api/notes/route.js index aa29807..3e19f47 100644 --- a/savebook/app/api/notes/route.js +++ b/savebook/app/api/notes/route.js @@ -35,6 +35,7 @@ export async function GET(request) { const notes = await Notes.find({ user: new mongoose.Types.ObjectId(decoded.userId), + isDeleted: false, // Exclude soft-deleted notes from active notes }).lean(); // 👇 Attach isBookmarked to each note diff --git a/savebook/app/api/notes/trash/[id]/route.js b/savebook/app/api/notes/trash/[id]/route.js new file mode 100644 index 0000000..e4c1638 --- /dev/null +++ b/savebook/app/api/notes/trash/[id]/route.js @@ -0,0 +1,63 @@ +import { NextResponse } from "next/server"; +import dbConnect from "@/lib/db/mongodb"; +import Notes from "@/lib/models/Notes"; +import mongoose from "mongoose"; +import { verifyJwtToken } from "@/lib/utils/jwtAuth"; + +export async function DELETE(request, { params }) { + await dbConnect(); + + try { + const { id: noteId } = await params; + const token = request.cookies.get("authToken"); + + if (!token) { + return NextResponse.json( + { error: "Unauthorized: No token provided" }, + { status: 401 } + ); + } + + const decoded = await verifyJwtToken(token.value); + + if (!decoded || !decoded.success) { + return NextResponse.json( + { error: "Unauthorized: Invalid token" }, + { status: 401 } + ); + } + + if (!mongoose.Types.ObjectId.isValid(noteId)) { + return NextResponse.json( + { error: "Invalid note ID" }, + { status: 400 } + ); + } + + const note = await Notes.findOne({ + _id: noteId, + user: decoded.userId, + isDeleted: true, + }); + + if (!note) { + return NextResponse.json( + { error: "Note not found in trash" }, + { status: 404 } + ); + } + + await Notes.findByIdAndDelete(noteId); + + return NextResponse.json({ + success: true, + message: "Note permanently deleted", + }); + } catch (error) { + console.error(error); + return NextResponse.json( + { error: "Server error" }, + { status: 500 } + ); + } +} diff --git a/savebook/app/api/notes/trash/restore/[id]/route.js b/savebook/app/api/notes/trash/restore/[id]/route.js new file mode 100644 index 0000000..22635d8 --- /dev/null +++ b/savebook/app/api/notes/trash/restore/[id]/route.js @@ -0,0 +1,66 @@ +import { NextResponse } from "next/server"; +import dbConnect from "@/lib/db/mongodb"; +import Notes from "@/lib/models/Notes"; +import mongoose from "mongoose"; +import { verifyJwtToken } from "@/lib/utils/jwtAuth"; + +export async function PUT(request, { params }) { + await dbConnect(); + + try { + const { id: noteId } = await params; + const token = request.cookies.get("authToken"); + + if (!token) { + return NextResponse.json( + { error: "Unauthorized: No token provided" }, + { status: 401 } + ); + } + + const decoded = await verifyJwtToken(token.value); + + if (!decoded || !decoded.success) { + return NextResponse.json( + { error: "Unauthorized: Invalid token" }, + { status: 401 } + ); + } + + if (!mongoose.Types.ObjectId.isValid(noteId)) { + return NextResponse.json( + { error: "Invalid note ID" }, + { status: 400 } + ); + } + + const note = await Notes.findOne({ + _id: noteId, + user: decoded.userId, + isDeleted: true, + }); + + if (!note) { + return NextResponse.json( + { error: "Note not found in trash or recovery period expired" }, + { status: 404 } + ); + } + + note.isDeleted = false; + note.deletedAt = null; + await note.save(); + + return NextResponse.json({ + success: true, + data: note, + message: "Note restored successfully", + }); + } catch (error) { + console.error(error); + return NextResponse.json( + { error: "Server error" }, + { status: 500 } + ); + } +} diff --git a/savebook/app/api/notes/trash/route.js b/savebook/app/api/notes/trash/route.js new file mode 100644 index 0000000..d5f98d6 --- /dev/null +++ b/savebook/app/api/notes/trash/route.js @@ -0,0 +1,58 @@ +import { NextResponse } from "next/server"; +import dbConnect from "@/lib/db/mongodb"; +import Notes from "@/lib/models/Notes"; +import mongoose from "mongoose"; +import { verifyJwtToken } from "@/lib/utils/jwtAuth"; + +/** + * GET /api/notes/trash + * Retrieve all soft-deleted notes for the current user + * Notes are recoverable for 30 days after deletion + */ +export async function GET(request) { + await dbConnect(); + + try { + const token = request.cookies.get("authToken"); + + if (!token) { + return NextResponse.json( + { error: "Unauthorized: No token provided" }, + { status: 401 } + ); + } + + const decoded = await verifyJwtToken(token.value); + + if (!decoded || !decoded.success) { + return NextResponse.json( + { error: "Unauthorized: Invalid token" }, + { status: 401 } + ); + } + + // Get deleted notes within 30-day recovery window + const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); + + const trash = await Notes.find({ + user: new mongoose.Types.ObjectId(decoded.userId), + isDeleted: true, + deletedAt: { $gte: thirtyDaysAgo }, // Only show notes deleted within last 30 days + }) + .sort({ deletedAt: -1 }) + .lean(); + + return NextResponse.json({ + success: true, + data: trash, + message: `${trash.length} deleted notes available for recovery (expires in 30 days)`, + }); + } catch (error) { + console.error(error); + return NextResponse.json( + { error: "Server error" }, + { status: 500 } + ); + } +} + diff --git a/savebook/components/notes/Notes.js b/savebook/components/notes/Notes.js index 5593efe..1b3bf11 100644 --- a/savebook/components/notes/Notes.js +++ b/savebook/components/notes/Notes.js @@ -8,6 +8,7 @@ import Addnote from './AddNote'; import NoteItem from './NoteItem'; import { useAuth } from '@/context/auth/authContext'; import RichTextEditor from './RichTextEditor'; +import Trash from './Trash'; // Separate navigation handler component to use router with Suspense const NavigationHandler = ({ isAuthenticated, loading }) => { @@ -560,6 +561,9 @@ export default function Notes() {
+ {activeTab === 'trash' ? ( ++ Recover deleted notes within 30 days. After 30 days, notes are permanently deleted. +
+Total in Trash
++ {trashedNotes.length} +
+Matching Search
++ {filteredNotes.length} +
+Recovery Window
+30 days
++ {trashedNotes.length === 0 ? "No deleted notes" : "No notes match your search"} +
++ {trashedNotes.length === 0 ? "Your trash is empty. Deleted notes will appear here." : "Try adjusting your search filters."} +
++ {note.description} +
++ Deleted {new Date(note.deletedAt).toLocaleDateString()} at {new Date(note.deletedAt).toLocaleTimeString()} +
+