Skip to content

Commit 976c279

Browse files
Merge pull request #273 from anshul23102/feat/soft-delete-note-recovery
feat: implement soft delete with 30-day recovery period for notes
2 parents 8e21977 + 2794e8a commit 976c279

9 files changed

Lines changed: 519 additions & 5 deletions

File tree

savebook/app/api/notes/[id]/route.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ export async function GET(request, { params }) {
3737
);
3838
}
3939

40-
const note = await Notes.findOne({ _id: id, user: decoded.userId });
40+
const note = await Notes.findOne({
41+
_id: id,
42+
user: decoded.userId,
43+
isDeleted: false, // Exclude soft-deleted notes
44+
});
4145

4246
if (!note) {
4347
return NextResponse.json(
@@ -108,12 +112,20 @@ export async function DELETE(request, { params }) {
108112
);
109113
}
110114

111-
// Delete the note
112-
await Notes.findByIdAndDelete(id);
115+
// Soft delete: mark as deleted instead of removing permanently
116+
// User has 30 days to recover from trash before permanent deletion via TTL
117+
await Notes.findByIdAndUpdate(
118+
id,
119+
{
120+
isDeleted: true,
121+
deletedAt: new Date(),
122+
},
123+
{ new: true }
124+
);
113125

114126
return NextResponse.json({
115127
success: true,
116-
message: "Note deleted successfully"
128+
message: "Note deleted. Recover from trash within 30 days."
117129
});
118130
} catch (error) {
119131
console.error(error);

savebook/app/api/notes/route.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export async function GET(request) {
3535

3636
const notes = await Notes.find({
3737
user: new mongoose.Types.ObjectId(decoded.userId),
38+
isDeleted: false, // Exclude soft-deleted notes from active notes
3839
}).lean();
3940

4041
// 👇 Attach isBookmarked to each note
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { NextResponse } from "next/server";
2+
import dbConnect from "@/lib/db/mongodb";
3+
import Notes from "@/lib/models/Notes";
4+
import mongoose from "mongoose";
5+
import { verifyJwtToken } from "@/lib/utils/jwtAuth";
6+
7+
export async function DELETE(request, { params }) {
8+
await dbConnect();
9+
10+
try {
11+
const { id: noteId } = await params;
12+
const token = request.cookies.get("authToken");
13+
14+
if (!token) {
15+
return NextResponse.json(
16+
{ error: "Unauthorized: No token provided" },
17+
{ status: 401 }
18+
);
19+
}
20+
21+
const decoded = await verifyJwtToken(token.value);
22+
23+
if (!decoded || !decoded.success) {
24+
return NextResponse.json(
25+
{ error: "Unauthorized: Invalid token" },
26+
{ status: 401 }
27+
);
28+
}
29+
30+
if (!mongoose.Types.ObjectId.isValid(noteId)) {
31+
return NextResponse.json(
32+
{ error: "Invalid note ID" },
33+
{ status: 400 }
34+
);
35+
}
36+
37+
const note = await Notes.findOne({
38+
_id: noteId,
39+
user: decoded.userId,
40+
isDeleted: true,
41+
});
42+
43+
if (!note) {
44+
return NextResponse.json(
45+
{ error: "Note not found in trash" },
46+
{ status: 404 }
47+
);
48+
}
49+
50+
await Notes.findByIdAndDelete(noteId);
51+
52+
return NextResponse.json({
53+
success: true,
54+
message: "Note permanently deleted",
55+
});
56+
} catch (error) {
57+
console.error(error);
58+
return NextResponse.json(
59+
{ error: "Server error" },
60+
{ status: 500 }
61+
);
62+
}
63+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { NextResponse } from "next/server";
2+
import dbConnect from "@/lib/db/mongodb";
3+
import Notes from "@/lib/models/Notes";
4+
import mongoose from "mongoose";
5+
import { verifyJwtToken } from "@/lib/utils/jwtAuth";
6+
7+
export async function PUT(request, { params }) {
8+
await dbConnect();
9+
10+
try {
11+
const { id: noteId } = await params;
12+
const token = request.cookies.get("authToken");
13+
14+
if (!token) {
15+
return NextResponse.json(
16+
{ error: "Unauthorized: No token provided" },
17+
{ status: 401 }
18+
);
19+
}
20+
21+
const decoded = await verifyJwtToken(token.value);
22+
23+
if (!decoded || !decoded.success) {
24+
return NextResponse.json(
25+
{ error: "Unauthorized: Invalid token" },
26+
{ status: 401 }
27+
);
28+
}
29+
30+
if (!mongoose.Types.ObjectId.isValid(noteId)) {
31+
return NextResponse.json(
32+
{ error: "Invalid note ID" },
33+
{ status: 400 }
34+
);
35+
}
36+
37+
const note = await Notes.findOne({
38+
_id: noteId,
39+
user: decoded.userId,
40+
isDeleted: true,
41+
});
42+
43+
if (!note) {
44+
return NextResponse.json(
45+
{ error: "Note not found in trash or recovery period expired" },
46+
{ status: 404 }
47+
);
48+
}
49+
50+
note.isDeleted = false;
51+
note.deletedAt = null;
52+
await note.save();
53+
54+
return NextResponse.json({
55+
success: true,
56+
data: note,
57+
message: "Note restored successfully",
58+
});
59+
} catch (error) {
60+
console.error(error);
61+
return NextResponse.json(
62+
{ error: "Server error" },
63+
{ status: 500 }
64+
);
65+
}
66+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { NextResponse } from "next/server";
2+
import dbConnect from "@/lib/db/mongodb";
3+
import Notes from "@/lib/models/Notes";
4+
import mongoose from "mongoose";
5+
import { verifyJwtToken } from "@/lib/utils/jwtAuth";
6+
7+
/**
8+
* GET /api/notes/trash
9+
* Retrieve all soft-deleted notes for the current user
10+
* Notes are recoverable for 30 days after deletion
11+
*/
12+
export async function GET(request) {
13+
await dbConnect();
14+
15+
try {
16+
const token = request.cookies.get("authToken");
17+
18+
if (!token) {
19+
return NextResponse.json(
20+
{ error: "Unauthorized: No token provided" },
21+
{ status: 401 }
22+
);
23+
}
24+
25+
const decoded = await verifyJwtToken(token.value);
26+
27+
if (!decoded || !decoded.success) {
28+
return NextResponse.json(
29+
{ error: "Unauthorized: Invalid token" },
30+
{ status: 401 }
31+
);
32+
}
33+
34+
// Get deleted notes within 30-day recovery window
35+
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
36+
37+
const trash = await Notes.find({
38+
user: new mongoose.Types.ObjectId(decoded.userId),
39+
isDeleted: true,
40+
deletedAt: { $gte: thirtyDaysAgo }, // Only show notes deleted within last 30 days
41+
})
42+
.sort({ deletedAt: -1 })
43+
.lean();
44+
45+
return NextResponse.json({
46+
success: true,
47+
data: trash,
48+
message: `${trash.length} deleted notes available for recovery (expires in 30 days)`,
49+
});
50+
} catch (error) {
51+
console.error(error);
52+
return NextResponse.json(
53+
{ error: "Server error" },
54+
{ status: 500 }
55+
);
56+
}
57+
}
58+

savebook/components/notes/Notes.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Addnote from './AddNote';
88
import NoteItem from './NoteItem';
99
import { useAuth } from '@/context/auth/authContext';
1010
import RichTextEditor from './RichTextEditor';
11+
import Trash from './Trash';
1112

1213
// Separate navigation handler component to use router with Suspense
1314
const NavigationHandler = ({ isAuthenticated, loading }) => {
@@ -560,6 +561,9 @@ export default function Notes() {
560561
</p>
561562
</div>
562563

564+
{activeTab === 'trash' ? (
565+
<Trash />
566+
) : (
563567
<div className="lg:flex lg:items-start lg:gap-8">
564568
<div className="flex-1">
565569
{/* Search and Filter Section */}
@@ -705,6 +709,16 @@ export default function Notes() {
705709
<span>Whiteboards</span>
706710
<span className="text-xs">{totalWhiteboards}</span>
707711
</button>
712+
<button
713+
onClick={() => setActiveTab('trash')}
714+
className={`flex items-center justify-between px-4 py-3 rounded-xl text-sm font-medium transition-all ${activeTab === 'trash'
715+
? 'bg-red-600 text-white'
716+
: 'bg-gray-800 text-gray-300 hover:bg-gray-700'
717+
}`}
718+
>
719+
<span>🗑️ Trash</span>
720+
<span className="text-xs">Recover</span>
721+
</button>
708722
</div>
709723

710724
<div className="border-t border-gray-700 pt-4 space-y-3">
@@ -721,6 +735,7 @@ export default function Notes() {
721735
</div>
722736
</aside>
723737
</div>
738+
)}
724739
</div>
725740
</div>
726741
</>

0 commit comments

Comments
 (0)