From af03d7af7ba524e89da70f00efe8d9532e3c60f3 Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:31:57 +0800 Subject: [PATCH 01/25] Validate student create payload --- app/api/students/route.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/api/students/route.ts b/app/api/students/route.ts index 8f3dcc2..bed5f30 100644 --- a/app/api/students/route.ts +++ b/app/api/students/route.ts @@ -92,9 +92,12 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: 'Malformed JSON' }, { status: 400 }) } - StudentSchema.safeParse(body) + const parsed = StudentSchema.safeParse(body) + if (!parsed.success) { + return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }) + } - const student = await Student.create({ ...(body as Record), teacherId: userId }) + const student = await Student.create({ ...parsed.data, teacherId: userId }) return NextResponse.json(student, { status: 201 }) } catch (error) { if (error instanceof Error) { From f8d2eb9d7de2e16a6fd9492c1183a9d47fbf54d9 Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:32:18 +0800 Subject: [PATCH 02/25] Require auth for profile fetch --- app/api/profile/route.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/api/profile/route.ts b/app/api/profile/route.ts index a3d98bf..f0d39ca 100644 --- a/app/api/profile/route.ts +++ b/app/api/profile/route.ts @@ -4,15 +4,14 @@ import { connectDB } from '@/lib/mongodb' import { Teacher } from '@/models/Teacher' export async function GET(req: NextRequest) { + const { userId } = await auth() + if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + const { searchParams } = new URL(req.url) const queryUserId = searchParams.get('userId') - - let userId: string | null = queryUserId - if (!userId) { - const session = await auth() - userId = session.userId + if (queryUserId && queryUserId !== userId) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } - if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) try { await connectDB() From edcb3d7d078d5035f9f7e326e4a2259ed7042e6a Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:32:27 +0800 Subject: [PATCH 03/25] Fix grade creation defaults --- app/api/grades/route.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/api/grades/route.ts b/app/api/grades/route.ts index b9da63d..07fa25f 100644 --- a/app/api/grades/route.ts +++ b/app/api/grades/route.ts @@ -70,13 +70,13 @@ export async function POST(req: NextRequest) { if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }) const data = parsed.data - const max = data.maxMarks! + const maxMarks = data.maxMarks ?? 100 const term = data.term ?? 'Term 1' - const grade = Grade.findOneAndUpdate( + const grade = await Grade.findOneAndUpdate( { teacherId: userId, studentId: data.studentId, subject: data.subject, term }, - { $set: { ...data, term, teacherId: userId, grade: calcGrade(data.marks, max) } }, - { upsert: true, new: true } + { $set: { ...data, term, maxMarks, teacherId: userId, grade: calcGrade(data.marks, maxMarks) } }, + { upsert: true, new: true, runValidators: true, context: 'query' } ) return NextResponse.json(grade, { status: 201 }) } catch (error) { From 69c0ea052ee6348e6a148d5fa7ca8aa3d74ac49e Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:32:35 +0800 Subject: [PATCH 04/25] Secure grade updates --- app/api/grades/[id]/route.ts | 38 +++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/app/api/grades/[id]/route.ts b/app/api/grades/[id]/route.ts index 0141f63..c30ce8e 100644 --- a/app/api/grades/[id]/route.ts +++ b/app/api/grades/[id]/route.ts @@ -6,6 +6,17 @@ import { Grade } from '@/models/Grade' const ALLOWED_UPDATE_FIELDS = ['marks', 'maxMarks', 'grade'] +function calcGrade(marks: number, max: number): string { + const pct = (marks / max) * 100 + if (pct > 90) return 'A+' + if (pct >= 80) return 'A' + if (pct >= 70) return 'B+' + if (pct >= 60) return 'B' + if (pct >= 50) return 'C' + if (pct >= 40) return 'D' + return 'F' +} + export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { const { userId } = await auth() if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) @@ -15,7 +26,7 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string // Validate ObjectId if (!mongoose.Types.ObjectId.isValid(id)) { - return NextResponse.json({ error: 'Not found' }, { status: 404 }) + return NextResponse.json({ error: 'Invalid id' }, { status: 400 }) } let body @@ -34,10 +45,22 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string } await connectDB() + const existing = await Grade.findOne({ _id: id, teacherId: userId }).lean() + if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 }) + + const updatePayload: Record = { ...sanitizedBody } + if ('marks' in updatePayload || 'maxMarks' in updatePayload) { + const nextMarks = + typeof updatePayload.marks === 'number' ? updatePayload.marks : existing.marks + const nextMaxMarks = + typeof updatePayload.maxMarks === 'number' ? updatePayload.maxMarks : existing.maxMarks + updatePayload.grade = calcGrade(nextMarks, nextMaxMarks) + } + const grade = await Grade.findOneAndUpdate( - { _id: id }, - sanitizedBody, - { new: true } + { _id: id, teacherId: userId }, + { $set: updatePayload }, + { new: true, runValidators: true, context: 'query' } ) if (!grade) return NextResponse.json({ error: 'Not found' }, { status: 404 }) return NextResponse.json(grade) @@ -55,8 +78,13 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str try { const { id } = await ctx.params + + if (!mongoose.Types.ObjectId.isValid(id)) { + return NextResponse.json({ error: 'Invalid id' }, { status: 400 }) + } + await connectDB() - const deleted = await Grade.findOneAndDelete({ _id: id }) + const deleted = await Grade.findOneAndDelete({ _id: id, teacherId: userId }) if (!deleted) { return NextResponse.json({ error: 'Grade not found' }, { status: 404 }) From dfdb956cc9bbf4668c9c1b174a92f516cd5c0a7a Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:32:42 +0800 Subject: [PATCH 05/25] Enforce ownership on record updates --- app/api/announcements/[id]/route.ts | 4 ++-- app/api/assignments/[id]/route.ts | 8 ++++---- app/api/students/[id]/route.ts | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/api/announcements/[id]/route.ts b/app/api/announcements/[id]/route.ts index 6c32f47..22d4d0a 100644 --- a/app/api/announcements/[id]/route.ts +++ b/app/api/announcements/[id]/route.ts @@ -36,7 +36,7 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string } const announcement = await Announcement.findOneAndUpdate( - { _id: id }, + { _id: id, teacherId: userId }, { $set: sanitizedBody }, { new: true, runValidators: true, context: 'query' } ) @@ -63,7 +63,7 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str } await connectDB() - const deleted = await Announcement.findOneAndDelete({ _id: id }) + const deleted = await Announcement.findOneAndDelete({ _id: id, teacherId: userId }) if (!deleted) { return NextResponse.json({ error: 'Not found' }, { status: 404 }) diff --git a/app/api/assignments/[id]/route.ts b/app/api/assignments/[id]/route.ts index 21fca1c..f5ebfed 100644 --- a/app/api/assignments/[id]/route.ts +++ b/app/api/assignments/[id]/route.ts @@ -36,9 +36,9 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string } const assignment = await Assignment.findOneAndUpdate( - { _id: id }, - sanitizedBody, - { new: true } + { _id: id, teacherId: userId }, + { $set: sanitizedBody }, + { new: true, runValidators: true, context: 'query' } ) if (!assignment) return NextResponse.json({ error: 'Not found' }, { status: 404 }) return NextResponse.json(assignment) @@ -63,7 +63,7 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str } await connectDB() - const deleted = await Assignment.findOneAndDelete({ _id: id }) + const deleted = await Assignment.findOneAndDelete({ _id: id, teacherId: userId }) if (!deleted) { return NextResponse.json({ error: 'Not found' }, { status: 404 }) diff --git a/app/api/students/[id]/route.ts b/app/api/students/[id]/route.ts index 2eaaf93..cf6b0d4 100644 --- a/app/api/students/[id]/route.ts +++ b/app/api/students/[id]/route.ts @@ -35,9 +35,9 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string await connectDB() const student = await Student.findOneAndUpdate( - { _id: id }, - sanitizedBody, - { new: true } + { _id: id, teacherId: userId }, + { $set: sanitizedBody }, + { new: true, runValidators: true, context: 'query' } ) if (!student) return NextResponse.json({ error: 'Not found' }, { status: 404 }) return NextResponse.json(student) @@ -65,7 +65,7 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str } await connectDB() - const deleted = await Student.findOneAndDelete({ _id: id }) + const deleted = await Student.findOneAndDelete({ _id: id, teacherId: userId }) if (!deleted) { return NextResponse.json({ error: 'Student not found' }, { status: 404 }) From 1e5856359e047b2ecabc5d97bd8a1be176d48f01 Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:33:18 +0800 Subject: [PATCH 06/25] Validate grade updates --- app/api/grades/[id]/route.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/app/api/grades/[id]/route.ts b/app/api/grades/[id]/route.ts index c30ce8e..5f02636 100644 --- a/app/api/grades/[id]/route.ts +++ b/app/api/grades/[id]/route.ts @@ -49,11 +49,19 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 }) const updatePayload: Record = { ...sanitizedBody } - if ('marks' in updatePayload || 'maxMarks' in updatePayload) { - const nextMarks = - typeof updatePayload.marks === 'number' ? updatePayload.marks : existing.marks - const nextMaxMarks = - typeof updatePayload.maxMarks === 'number' ? updatePayload.maxMarks : existing.maxMarks + const hasMarks = 'marks' in updatePayload + const hasMaxMarks = 'maxMarks' in updatePayload + if (hasMarks || hasMaxMarks) { + const nextMarks = hasMarks ? Number(updatePayload.marks) : existing.marks + const nextMaxMarks = hasMaxMarks ? Number(updatePayload.maxMarks) : existing.maxMarks + if ((hasMarks && Number.isNaN(nextMarks)) || (hasMaxMarks && Number.isNaN(nextMaxMarks))) { + return NextResponse.json({ error: 'marks and maxMarks must be numbers' }, { status: 400 }) + } + if (nextMarks > nextMaxMarks) { + return NextResponse.json({ error: 'marks must be less than or equal to maxMarks' }, { status: 400 }) + } + if (hasMarks) updatePayload.marks = nextMarks + if (hasMaxMarks) updatePayload.maxMarks = nextMaxMarks updatePayload.grade = calcGrade(nextMarks, nextMaxMarks) } From 7b711cdb3718a8c49eee155f2355e5170112cebd Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:35:51 +0800 Subject: [PATCH 07/25] Harden grade validation --- app/api/grades/route.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/api/grades/route.ts b/app/api/grades/route.ts index 07fa25f..eb7ea81 100644 --- a/app/api/grades/route.ts +++ b/app/api/grades/route.ts @@ -9,10 +9,10 @@ const GradeSchema = z.object({ studentName: z.string().min(1), subject: z.string().min(1), marks: z.number().min(0), - maxMarks: z.number().min(1).optional(), + maxMarks: z.number().min(1).default(100), term: z.string().optional(), }).refine( - (data) => !data.maxMarks || data.marks <= data.maxMarks, + (data) => data.marks <= data.maxMarks, { message: 'marks must be less than or equal to maxMarks', path: ['marks'], @@ -70,7 +70,7 @@ export async function POST(req: NextRequest) { if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }) const data = parsed.data - const maxMarks = data.maxMarks ?? 100 + const maxMarks = data.maxMarks const term = data.term ?? 'Term 1' const grade = await Grade.findOneAndUpdate( From 56377f0657a3b59eb172c6ca0b0bf008f1809af3 Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:38:09 +0800 Subject: [PATCH 08/25] Prevent manual grade overrides --- app/api/grades/[id]/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/grades/[id]/route.ts b/app/api/grades/[id]/route.ts index 5f02636..349319a 100644 --- a/app/api/grades/[id]/route.ts +++ b/app/api/grades/[id]/route.ts @@ -4,7 +4,7 @@ import mongoose from 'mongoose' import { connectDB } from '@/lib/mongodb' import { Grade } from '@/models/Grade' -const ALLOWED_UPDATE_FIELDS = ['marks', 'maxMarks', 'grade'] +const ALLOWED_UPDATE_FIELDS = ['marks', 'maxMarks'] function calcGrade(marks: number, max: number): string { const pct = (marks / max) * 100 From 6b0ca08d86fe487fd17cb840d4414c1557e57e61 Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:38:18 +0800 Subject: [PATCH 09/25] Hide internal errors in API responses --- app/api/assignments/route.ts | 2 +- app/api/grades/route.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/api/assignments/route.ts b/app/api/assignments/route.ts index 19021d8..1d88b7e 100644 --- a/app/api/assignments/route.ts +++ b/app/api/assignments/route.ts @@ -52,7 +52,7 @@ export async function GET(req: NextRequest) { if (error instanceof Error) { console.error('GET /api/assignments error:', error.message) } - return NextResponse.json({ error: error instanceof Error ? error.stack : 'Internal server error' }, { status: 500 }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } diff --git a/app/api/grades/route.ts b/app/api/grades/route.ts index eb7ea81..2a27bb7 100644 --- a/app/api/grades/route.ts +++ b/app/api/grades/route.ts @@ -48,7 +48,7 @@ export async function GET(req: NextRequest) { return NextResponse.json(grades) } catch (error) { console.error('GET /api/grades error:', error instanceof Error ? error.message : error) - return NextResponse.json({ error: error instanceof Error ? error.stack : 'Internal server error' }, { status: 500 }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } @@ -83,6 +83,6 @@ export async function POST(req: NextRequest) { if (error instanceof Error) { console.error('POST /api/grades error:', error.message) } - return NextResponse.json({ error: error instanceof Error ? error.stack : 'Internal server error' }, { status: 500 }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } From 2ce79ff50e5dfa27c87deb6d22161c6d230d8ab3 Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:39:04 +0800 Subject: [PATCH 10/25] Align CGPA grade points --- app/dashboard/grades/GradesClient.tsx | 2 +- app/dashboard/students/StudentsClient.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/dashboard/grades/GradesClient.tsx b/app/dashboard/grades/GradesClient.tsx index fdaa00b..beb66c1 100644 --- a/app/dashboard/grades/GradesClient.tsx +++ b/app/dashboard/grades/GradesClient.tsx @@ -60,7 +60,7 @@ function pct(marks: number, max: number) { } const GRADE_POINT: Record = { - 'A+': 10, A: 9, 'B+': 8, B: 7, C: 6, D: 5, F: 0, + 'A+': 10, A: 9, 'B+': 8, B: 7, C: 6, D: 4, F: 0, } function cgpaFromGrades(gradeList: Grade[]) { diff --git a/app/dashboard/students/StudentsClient.tsx b/app/dashboard/students/StudentsClient.tsx index 82a1d96..b2d2a82 100644 --- a/app/dashboard/students/StudentsClient.tsx +++ b/app/dashboard/students/StudentsClient.tsx @@ -211,7 +211,7 @@ function StudentDrawer({ }, [grades]); const cgpa = useMemo(() => { - const GRADE_POINT: Record = { 'A+': 10, A: 9, 'B+': 8, B: 7, C: 6, D: 5, F: 0 } + const GRADE_POINT: Record = { 'A+': 10, A: 9, 'B+': 8, B: 7, C: 6, D: 4, F: 0 } if (!grades.length) return null const sum = grades.reduce((s, g) => s + (GRADE_POINT[g.grade] ?? 0), 0) return (sum / grades.length).toFixed(2) From bfa2dd9030760c8fa58d154fb44ae61db03f917e Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:39:21 +0800 Subject: [PATCH 11/25] Handle assignment move failures --- app/dashboard/assignments/AssignmentsClient.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/dashboard/assignments/AssignmentsClient.tsx b/app/dashboard/assignments/AssignmentsClient.tsx index e0e7f13..d375d8c 100644 --- a/app/dashboard/assignments/AssignmentsClient.tsx +++ b/app/dashboard/assignments/AssignmentsClient.tsx @@ -387,7 +387,7 @@ export function AssignmentsClient() { ), ); try { - await fetch(`/api/assignments/${id}`, { + const res = await fetch(`/api/assignments/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -395,6 +395,7 @@ export function AssignmentsClient() { status: col === "submitted" ? "closed" : "active", }), }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); } catch (error) { fetchAssignments(); toast( From 99cecafdd71ddeed46b76d3c7256a501568dcdde Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:39:48 +0800 Subject: [PATCH 12/25] Fix dashboard assignment counts --- app/dashboard/OverviewClient.tsx | 36 +++++++++++++++++++------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/app/dashboard/OverviewClient.tsx b/app/dashboard/OverviewClient.tsx index 169795e..ca905ca 100644 --- a/app/dashboard/OverviewClient.tsx +++ b/app/dashboard/OverviewClient.tsx @@ -189,27 +189,39 @@ export function OverviewClient() { const [ studentsRes, assignmentsRes, + assignmentsActiveRes, attendanceRes, gradesRes, announcementsRes, ] = await Promise.all([ fetch("/api/students?limit=5"), fetch("/api/assignments"), + fetch("/api/assignments?status=active&limit=1"), fetch("/api/attendance"), fetch("/api/grades"), fetch("/api/announcements?limit=5"), ]); - const [students, assignmentsData, attendance, grades, announcements] = - await Promise.all([ - studentsRes.json(), - assignmentsRes.json(), - attendanceRes.json(), - gradesRes.json(), - announcementsRes.json(), - ]); + const [ + students, + assignmentsData, + assignmentsActiveData, + attendance, + grades, + announcements, + ] = await Promise.all([ + studentsRes.json(), + assignmentsRes.json(), + assignmentsActiveRes.json(), + attendanceRes.json(), + gradesRes.json(), + announcementsRes.json(), + ]); const assignments = assignmentsData.assignments ?? assignmentsData; + const assignmentsTotal = assignmentsData.total ?? assignments.length; + const pendingAssignments = assignmentsActiveData.total ?? + (assignmentsActiveData.assignments ?? assignmentsActiveData ?? []).length; // ── Attendance ── const dateMap: Record< @@ -317,12 +329,8 @@ export function OverviewClient() { setStats({ totalStudents: students.students?.length ?? 0, - totalAssignments: Array.isArray(assignments) - ? assignments.length - : (assignments.length ?? 0), - pendingAssignments: assignments.filter( - (a: { status: string }) => a.status === "active", - ).length, + totalAssignments: assignmentsTotal, + pendingAssignments, attendancePct, attendanceBreakdown: { present: totalPresent, From 5f38d94866dae02b20becc4551ef83918633da33 Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:40:00 +0800 Subject: [PATCH 13/25] Fix dashboard student count --- app/dashboard/OverviewClient.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/dashboard/OverviewClient.tsx b/app/dashboard/OverviewClient.tsx index ca905ca..d7b40af 100644 --- a/app/dashboard/OverviewClient.tsx +++ b/app/dashboard/OverviewClient.tsx @@ -328,7 +328,7 @@ export function OverviewClient() { .slice(0, 5); setStats({ - totalStudents: students.students?.length ?? 0, + totalStudents: students.total ?? students.students?.length ?? 0, totalAssignments: assignmentsTotal, pendingAssignments, attendancePct, From b15a5c9aed0ccf46c92b32b1b6886a9daf5bd236 Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:40:16 +0800 Subject: [PATCH 14/25] Validate empty attendance bulk payload --- app/api/attendance/route.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/api/attendance/route.ts b/app/api/attendance/route.ts index 14b6c4d..f2c9c3a 100644 --- a/app/api/attendance/route.ts +++ b/app/api/attendance/route.ts @@ -110,6 +110,12 @@ export async function POST(req: NextRequest) { // Support both single and bulk const isBulk = Array.isArray(body) + if (isBulk && body.length === 0) { + return NextResponse.json( + { error: "Bulk payload must include at least one record" }, + { status: 400 }, + ); + } if (isBulk && body.length > 500) { return NextResponse.json( { error: "Bulk payload exceeds maximum of 500 records" }, From d086b0990e4ad38e80110cece9dbc68c0e6e825f Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:40:30 +0800 Subject: [PATCH 15/25] Fix assignment edit date input --- app/dashboard/assignments/AssignmentsClient.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/dashboard/assignments/AssignmentsClient.tsx b/app/dashboard/assignments/AssignmentsClient.tsx index d375d8c..482a8a3 100644 --- a/app/dashboard/assignments/AssignmentsClient.tsx +++ b/app/dashboard/assignments/AssignmentsClient.tsx @@ -344,7 +344,7 @@ export function AssignmentsClient() { description: a.description, subject: a.subject, class: a.class, - deadline: a.deadline, + deadline: a.deadline ? a.deadline.slice(0, 10) : "", maxMarks: a.maxMarks, }); setModalOpen(true); From f2a9d802edfec9c6b6feb715e9852a2a948edea7 Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:42:42 +0800 Subject: [PATCH 16/25] Handle empty maxMarks in grades form --- app/dashboard/grades/GradesClient.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/dashboard/grades/GradesClient.tsx b/app/dashboard/grades/GradesClient.tsx index beb66c1..919ba90 100644 --- a/app/dashboard/grades/GradesClient.tsx +++ b/app/dashboard/grades/GradesClient.tsx @@ -294,10 +294,13 @@ export function GradesClient() { toast("Selected student not found", "error"); return; } + const normalizedMaxMarks = Number.isFinite(data.maxMarks) + ? Number(data.maxMarks) + : undefined; const payload = { ...data, marks: Number(data.marks), - maxMarks: Number(data.maxMarks), + ...(normalizedMaxMarks !== undefined ? { maxMarks: normalizedMaxMarks } : {}), studentName: selectedStudent.name, }; const url = editing ? `/api/grades/${editing._id}` : "/api/grades"; From 3db6529c0c5b02f333c157fb505062ec90747683 Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:42:50 +0800 Subject: [PATCH 17/25] Handle empty maxMarks in assignments form --- app/dashboard/assignments/AssignmentsClient.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/dashboard/assignments/AssignmentsClient.tsx b/app/dashboard/assignments/AssignmentsClient.tsx index 482a8a3..b9b1c0d 100644 --- a/app/dashboard/assignments/AssignmentsClient.tsx +++ b/app/dashboard/assignments/AssignmentsClient.tsx @@ -356,10 +356,18 @@ export function AssignmentsClient() { : "/api/assignments"; const method = editing ? "PUT" : "POST"; try { + const normalizedMaxMarks = Number.isFinite(data.maxMarks) + ? Number(data.maxMarks) + : undefined; const res = await fetch(url, { method, headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ ...data, maxMarks: Number(data.maxMarks) }), + body: JSON.stringify({ + ...data, + ...(normalizedMaxMarks !== undefined + ? { maxMarks: normalizedMaxMarks } + : {}), + }), }); if (res.ok) { toast( From 918aa223a5ef1e979a2f8e713a162cc9b8e8faa9 Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:43:48 +0800 Subject: [PATCH 18/25] Default empty grade term --- app/api/grades/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/grades/route.ts b/app/api/grades/route.ts index 2a27bb7..021a876 100644 --- a/app/api/grades/route.ts +++ b/app/api/grades/route.ts @@ -71,7 +71,7 @@ export async function POST(req: NextRequest) { const data = parsed.data const maxMarks = data.maxMarks - const term = data.term ?? 'Term 1' + const term = data.term?.trim() || 'Term 1' const grade = await Grade.findOneAndUpdate( { teacherId: userId, studentId: data.studentId, subject: data.subject, term }, From f33cc45c334cf039523632d4f508deca782e9528 Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:44:50 +0800 Subject: [PATCH 19/25] Fix auth, validation, and dashboard stats bugs ## Summary - Enforce ownership and auth checks for updates/deletes and profile access; validate student creation and grade updates. - Harden grade creation/update defaults (term/maxMarks), prevent manual grade overrides, and align CGPA point mapping. - Improve UX and data integrity: dashboard totals, assignment move rollback, edit date normalization, maxMarks form handling, and attendance bulk validation. - Remove stack-trace leaks from API error responses. ## Testing - Not run (not requested) --- next.config.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/next.config.ts b/next.config.ts index 6230bd9..b08b686 100644 --- a/next.config.ts +++ b/next.config.ts @@ -4,27 +4,30 @@ const nextConfig: NextConfig = { images: { remotePatterns: [ { - protocol: 'https', - hostname: 'img.clerk.com', + protocol: "https", + hostname: "img.clerk.com", }, { - protocol: 'https', - hostname: 'images.clerk.dev', + protocol: "https", + hostname: "images.clerk.dev", }, ], }, async headers() { return [ { - source: '/(.*)', + source: "/(.*)", headers: [ - { key: 'X-Content-Type-Options', value: 'nosniff' }, - { key: 'X-Frame-Options', value: 'DENY' }, - { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, - { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' }, + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "X-Frame-Options", value: "DENY" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + { + key: "Permissions-Policy", + value: "camera=(), microphone=(), geolocation=()", + }, ], }, - ] + ]; }, }; From 5386ecbcbad94f9bd0514006e9385ce38d279a00 Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:51:03 +0800 Subject: [PATCH 20/25] Defer MongoDB env validation --- lib/mongodb.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/mongodb.ts b/lib/mongodb.ts index 01dddf9..f8e26cf 100644 --- a/lib/mongodb.ts +++ b/lib/mongodb.ts @@ -1,9 +1,11 @@ import mongoose from 'mongoose' -const MONGODB_URI = process.env.MONGODB_URI! - -if (!MONGODB_URI) { - throw new Error('Please define the MONGODB_URI environment variable') +function getMongoUri(): string { + const uri = process.env.MONGODB_URI + if (!uri) { + throw new Error('Please define the MONGODB_URI environment variable') + } + return uri } interface MongooseCache { @@ -22,8 +24,9 @@ export async function connectDB(): Promise { if (cached.conn) return cached.conn if (!cached.promise) { + const uri = getMongoUri() cached.promise = mongoose - .connect(MONGODB_URI, { bufferCommands: false }) + .connect(uri, { bufferCommands: false }) .catch((error) => { cached.promise = null throw error From 60f4cadf5e2e70ed64f5a5bbbe3b5f5a31ed5c7b Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:51:14 +0800 Subject: [PATCH 21/25] Init navbar theme from document --- components/dashboard/Navbar.tsx | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/components/dashboard/Navbar.tsx b/components/dashboard/Navbar.tsx index 49b4a0c..14a75a7 100644 --- a/components/dashboard/Navbar.tsx +++ b/components/dashboard/Navbar.tsx @@ -10,19 +10,10 @@ interface NavbarProps { export function Navbar({ onMenuClick, title }: NavbarProps) { const [dark, setDark] = useState(false); - // Initialize theme from localStorage and system preference on client-side only + // Initialize theme from the document state (set in RootLayout script) useEffect(() => { - try { - const stored = localStorage.getItem("theme"); - const prefersDark = window.matchMedia( - "(prefers-color-scheme: dark)", - ).matches; - const isDark = stored ? stored === "dark" : prefersDark; - setDark(isDark); - document.documentElement.classList.toggle("dark", isDark); - } catch (e) { - // Silently fail if localStorage is not available - } + const isDark = document.documentElement.classList.contains("dark"); + setDark(isDark); }, []); // Sync dark class to whenever dark state changes From 82ac82ee2e9b61708f763fa77ab58d6ec0168cea Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:51:23 +0800 Subject: [PATCH 22/25] Migrate middleware to proxy --- middleware.ts => proxy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename middleware.ts => proxy.ts (99%) diff --git a/middleware.ts b/proxy.ts similarity index 99% rename from middleware.ts rename to proxy.ts index f352d7d..d8e7f84 100644 --- a/middleware.ts +++ b/proxy.ts @@ -17,4 +17,4 @@ export const config = { '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', '/(api|trpc)(.*)', ], -} +} \ No newline at end of file From cfaa5e641060d20380e7b09b01f75284adc40dfe Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 14:52:14 +0800 Subject: [PATCH 23/25] Avoid setState in navbar effect --- components/dashboard/Navbar.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/components/dashboard/Navbar.tsx b/components/dashboard/Navbar.tsx index 14a75a7..b5664cf 100644 --- a/components/dashboard/Navbar.tsx +++ b/components/dashboard/Navbar.tsx @@ -8,13 +8,10 @@ interface NavbarProps { } export function Navbar({ onMenuClick, title }: NavbarProps) { - const [dark, setDark] = useState(false); - - // Initialize theme from the document state (set in RootLayout script) - useEffect(() => { - const isDark = document.documentElement.classList.contains("dark"); - setDark(isDark); - }, []); + const [dark, setDark] = useState(() => { + if (typeof document === "undefined") return false; + return document.documentElement.classList.contains("dark"); + }); // Sync dark class to whenever dark state changes useEffect(() => { From 8aa5dab8097d08ba4553c378b0f82fb76f3c128b Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 15:04:46 +0800 Subject: [PATCH 24/25] Align route whitelists with schemas --- app/api/announcements/[id]/route.ts | 2 +- app/api/assignments/[id]/route.ts | 2 +- app/api/students/[id]/route.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/api/announcements/[id]/route.ts b/app/api/announcements/[id]/route.ts index 22d4d0a..4e0929b 100644 --- a/app/api/announcements/[id]/route.ts +++ b/app/api/announcements/[id]/route.ts @@ -4,7 +4,7 @@ import mongoose from 'mongoose' import { connectDB } from '@/lib/mongodb' import { Announcement } from '@/models/Announcement' -const ALLOWED_FIELDS = ['title', 'content', 'body', 'audience', 'category', 'pinned', 'expiresAt'] +const ALLOWED_FIELDS = ['title', 'content', 'audience', 'category', 'pinned'] export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { const { userId } = await auth() diff --git a/app/api/assignments/[id]/route.ts b/app/api/assignments/[id]/route.ts index f5ebfed..7e894a8 100644 --- a/app/api/assignments/[id]/route.ts +++ b/app/api/assignments/[id]/route.ts @@ -4,7 +4,7 @@ import mongoose from 'mongoose' import { connectDB } from '@/lib/mongodb' import { Assignment } from '@/models/Assignment' -const ALLOWED_UPDATE_FIELDS = ['title', 'description', 'dueDate', 'deadline', 'subject', 'class', 'status', 'kanbanStatus', 'maxMarks'] +const ALLOWED_UPDATE_FIELDS = ['title', 'description', 'deadline', 'subject', 'class', 'status', 'kanbanStatus', 'maxMarks'] export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { const { userId } = await auth() diff --git a/app/api/students/[id]/route.ts b/app/api/students/[id]/route.ts index cf6b0d4..f21cab6 100644 --- a/app/api/students/[id]/route.ts +++ b/app/api/students/[id]/route.ts @@ -4,7 +4,7 @@ import mongoose from 'mongoose' import { connectDB } from '@/lib/mongodb' import { Student } from '@/models/Student' -const ALLOWED_UPDATE_FIELDS = ['name', 'email', 'grade', 'rollNo', 'class', 'phone', 'address', 'parentName', 'parentPhone'] +const ALLOWED_UPDATE_FIELDS = ['name', 'email', 'rollNo', 'class', 'phone', 'address', 'parentName', 'parentPhone'] export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { const { userId } = await auth() From 0201d546d8bc44dd5709890ddcb0ddba1bfbf0db Mon Sep 17 00:00:00 2001 From: notedwin-dev Date: Sat, 18 Apr 2026 15:05:22 +0800 Subject: [PATCH 25/25] Fix A+ threshold at 90 percent --- app/api/grades/[id]/route.ts | 2 +- app/api/grades/route.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/grades/[id]/route.ts b/app/api/grades/[id]/route.ts index 349319a..b07b7b0 100644 --- a/app/api/grades/[id]/route.ts +++ b/app/api/grades/[id]/route.ts @@ -8,7 +8,7 @@ const ALLOWED_UPDATE_FIELDS = ['marks', 'maxMarks'] function calcGrade(marks: number, max: number): string { const pct = (marks / max) * 100 - if (pct > 90) return 'A+' + if (pct >= 90) return 'A+' if (pct >= 80) return 'A' if (pct >= 70) return 'B+' if (pct >= 60) return 'B' diff --git a/app/api/grades/route.ts b/app/api/grades/route.ts index 021a876..1b0dc80 100644 --- a/app/api/grades/route.ts +++ b/app/api/grades/route.ts @@ -21,7 +21,7 @@ const GradeSchema = z.object({ function calcGrade(marks: number, max: number): string { const pct = (marks / max) * 100 - if (pct > 90) return 'A+' + if (pct >= 90) return 'A+' if (pct >= 80) return 'A' if (pct >= 70) return 'B+' if (pct >= 60) return 'B'