From ae8664f3ecc00931272a091abe25c197fc0a81b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Anh=20T=C3=BA?= Date: Sat, 18 Apr 2026 13:23:21 +0700 Subject: [PATCH 01/11] fix: Add teacherId validation to PUT and DELETE routes for announcements, assignments, grades, and students --- app/api/announcements/[id]/route.ts | 4 ++-- app/api/assignments/[id]/route.ts | 4 ++-- app/api/grades/[id]/route.ts | 4 ++-- app/api/students/[id]/route.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 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..b041302 100644 --- a/app/api/assignments/[id]/route.ts +++ b/app/api/assignments/[id]/route.ts @@ -36,7 +36,7 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string } const assignment = await Assignment.findOneAndUpdate( - { _id: id }, + { _id: id, teacherId: userId }, sanitizedBody, { new: true } ) @@ -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/grades/[id]/route.ts b/app/api/grades/[id]/route.ts index 0141f63..a4e66ff 100644 --- a/app/api/grades/[id]/route.ts +++ b/app/api/grades/[id]/route.ts @@ -35,7 +35,7 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string await connectDB() const grade = await Grade.findOneAndUpdate( - { _id: id }, + { _id: id, teacherId: userId }, sanitizedBody, { new: true } ) @@ -56,7 +56,7 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str try { const { id } = await ctx.params 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 }) diff --git a/app/api/students/[id]/route.ts b/app/api/students/[id]/route.ts index 2eaaf93..990037b 100644 --- a/app/api/students/[id]/route.ts +++ b/app/api/students/[id]/route.ts @@ -35,7 +35,7 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string await connectDB() const student = await Student.findOneAndUpdate( - { _id: id }, + { _id: id, teacherId: userId }, sanitizedBody, { new: true } ) @@ -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 eb9dfa34ced996405dc3be5713b9fede03ad3c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Anh=20T=C3=BA?= Date: Sat, 18 Apr 2026 13:25:38 +0700 Subject: [PATCH 02/11] fix: Simplify GET function by removing unnecessary userId query parameter handling --- app/api/profile/route.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/app/api/profile/route.ts b/app/api/profile/route.ts index a3d98bf..dbdebdc 100644 --- a/app/api/profile/route.ts +++ b/app/api/profile/route.ts @@ -3,15 +3,8 @@ import { NextRequest, NextResponse } from 'next/server' import { connectDB } from '@/lib/mongodb' import { Teacher } from '@/models/Teacher' -export async function GET(req: NextRequest) { - 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 - } +export async function GET() { + const { userId } = await auth() if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) try { From 4a53d0c846f7bb42e4b8d7afd144fc28628703f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Anh=20T=C3=BA?= Date: Sat, 18 Apr 2026 13:27:31 +0700 Subject: [PATCH 03/11] fix: Update POST route to handle maxMarks default value and improve query validation --- 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..70b305f 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 max = 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, maxMarks: max, term, teacherId: userId, grade: calcGrade(data.marks, max) } }, + { upsert: true, new: true, runValidators: true, context: 'query' } ) return NextResponse.json(grade, { status: 201 }) } catch (error) { From 44136fae17d0071e7c18ae2b661236a668238ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Anh=20T=C3=BA?= Date: Sat, 18 Apr 2026 13:30:22 +0700 Subject: [PATCH 04/11] fix: Enhance PUT route for grades with comprehensive validation and error handling --- app/api/grades/[id]/route.ts | 65 ++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/app/api/grades/[id]/route.ts b/app/api/grades/[id]/route.ts index a4e66ff..0af35d6 100644 --- a/app/api/grades/[id]/route.ts +++ b/app/api/grades/[id]/route.ts @@ -4,7 +4,18 @@ 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 = ['studentId', 'studentName', 'subject', 'term', 'marks', 'maxMarks'] + +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() @@ -34,10 +45,55 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string } await connectDB() + + const existingGrade = await Grade.findOne({ _id: id, teacherId: userId }) + if (!existingGrade) return NextResponse.json({ error: 'Not found' }, { status: 404 }) + + if ('studentId' in sanitizedBody) { + const studentId = sanitizedBody.studentId + if (typeof studentId !== 'string' || !mongoose.Types.ObjectId.isValid(studentId)) { + return NextResponse.json({ error: 'studentId must be a valid id' }, { status: 400 }) + } + } + + if ('studentName' in sanitizedBody && typeof sanitizedBody.studentName !== 'string') { + return NextResponse.json({ error: 'studentName must be a string' }, { status: 400 }) + } + if ('subject' in sanitizedBody && typeof sanitizedBody.subject !== 'string') { + return NextResponse.json({ error: 'subject must be a string' }, { status: 400 }) + } + if ('term' in sanitizedBody && typeof sanitizedBody.term !== 'string') { + return NextResponse.json({ error: 'term must be a string' }, { status: 400 }) + } + + if ('marks' in sanitizedBody) { + const marks = sanitizedBody.marks + if (typeof marks !== 'number' || Number.isNaN(marks) || marks < 0) { + return NextResponse.json({ error: 'marks must be a number >= 0' }, { status: 400 }) + } + } + if ('maxMarks' in sanitizedBody) { + const maxMarks = sanitizedBody.maxMarks + if (typeof maxMarks !== 'number' || Number.isNaN(maxMarks) || maxMarks < 1) { + return NextResponse.json({ error: 'maxMarks must be a number >= 1' }, { status: 400 }) + } + } + + const nextMarks = typeof sanitizedBody.marks === 'number' ? sanitizedBody.marks : existingGrade.marks + const nextMaxMarks = typeof sanitizedBody.maxMarks === 'number' ? sanitizedBody.maxMarks : (existingGrade.maxMarks ?? 100) + + if (nextMarks > nextMaxMarks) { + return NextResponse.json({ error: 'marks must be less than or equal to maxMarks' }, { status: 400 }) + } + const grade = await Grade.findOneAndUpdate( { _id: id, teacherId: userId }, - sanitizedBody, - { new: true } + { + ...sanitizedBody, + maxMarks: nextMaxMarks, + grade: calcGrade(nextMarks, nextMaxMarks), + }, + { new: true, runValidators: true, context: 'query' } ) if (!grade) return NextResponse.json({ error: 'Not found' }, { status: 404 }) return NextResponse.json(grade) @@ -45,6 +101,9 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string if (error instanceof Error) { console.error('PUT /api/grades/[id] error:', error.message) } + if ((error as { code?: number }).code === 11000) { + return NextResponse.json({ error: 'A grade for this student, subject, and term already exists' }, { status: 409 }) + } return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } From f2834cfd340bc0dfd500489b29909882693e66e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Anh=20T=C3=BA?= Date: Sat, 18 Apr 2026 13:31:59 +0700 Subject: [PATCH 05/11] fix: Refactor validation logic for marks and maxMarks in update hooks --- models/Grade.ts | 68 +++++++++++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/models/Grade.ts b/models/Grade.ts index 311b757..d3b3eaf 100644 --- a/models/Grade.ts +++ b/models/Grade.ts @@ -38,35 +38,53 @@ GradeSchema.pre("save", function () { } }); -GradeSchema.pre("findOneAndUpdate", function () { - const update = this.getUpdate() as Record; - if (update && typeof update === "object") { - const marks = update.marks; - const maxMarks = update.maxMarks; - if ( - marks !== undefined && typeof marks === "number" && - maxMarks !== undefined && typeof maxMarks === "number" && - marks > maxMarks - ) { - throw new Error("marks must be less than or equal to maxMarks"); - } +function pickNumberFromUpdate(update: Record, field: "marks" | "maxMarks"): number | undefined { + const direct = update[field]; + if (typeof direct === "number" && Number.isFinite(direct)) return direct; + + const $set = update.$set; + if ($set && typeof $set === "object") { + const setValue = ($set as Record)[field]; + if (typeof setValue === "number" && Number.isFinite(setValue)) return setValue; } -}); -GradeSchema.pre("updateOne", function () { - const update = this.getUpdate() as Record; - if (update && typeof update === "object") { - const marks = update.marks; - const maxMarks = update.maxMarks; - if ( - marks !== undefined && typeof marks === "number" && - maxMarks !== undefined && typeof maxMarks === "number" && - marks > maxMarks - ) { - throw new Error("marks must be less than or equal to maxMarks"); + return undefined; +} + +async function validateMarksMaxMarksOnUpdate(this: mongoose.Query) { + const update = this.getUpdate() as Record | null; + if (!update || typeof update !== "object") return; + + let nextMarks = pickNumberFromUpdate(update, "marks"); + let nextMaxMarks = pickNumberFromUpdate(update, "maxMarks"); + + if (nextMarks === undefined && nextMaxMarks === undefined) return; + + if (nextMarks === undefined || nextMaxMarks === undefined) { + const currentRaw = await this.model.findOne(this.getQuery()).select("marks maxMarks").lean(); + if (currentRaw && typeof currentRaw === "object") { + const current = currentRaw as { marks?: unknown; maxMarks?: unknown }; + if (nextMarks === undefined && typeof current.marks === "number") nextMarks = current.marks; + if (nextMaxMarks === undefined && typeof current.maxMarks === "number") nextMaxMarks = current.maxMarks; + } + + // For upsert flows where maxMarks is omitted, schema default is 100. + if (nextMarks !== undefined && nextMaxMarks === undefined) { + nextMaxMarks = 100; } } -}); + + if ( + nextMarks !== undefined && + nextMaxMarks !== undefined && + nextMarks > nextMaxMarks + ) { + throw new Error("marks must be less than or equal to maxMarks"); + } +} + +GradeSchema.pre("findOneAndUpdate", validateMarksMaxMarksOnUpdate); +GradeSchema.pre("updateOne", validateMarksMaxMarksOnUpdate); GradeSchema.index({ teacherId: 1, studentId: 1, subject: 1, term: 1 }, { unique: true }) From ff631fd7878bd3cf7ebff483c3b92bbe94d4896a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Anh=20T=C3=BA?= Date: Sat, 18 Apr 2026 13:33:08 +0700 Subject: [PATCH 06/11] fix: Improve POST route validation for student creation --- 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 4108ab64196f328f81d629df2c467d2cf44acd20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Anh=20T=C3=BA?= Date: Sat, 18 Apr 2026 13:34:25 +0700 Subject: [PATCH 07/11] fix: Simplify error logging in GET and POST routes for assignments and grades --- app/api/assignments/route.ts | 6 ++---- app/api/grades/route.ts | 10 ++++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/api/assignments/route.ts b/app/api/assignments/route.ts index 19021d8..e9f1006 100644 --- a/app/api/assignments/route.ts +++ b/app/api/assignments/route.ts @@ -49,10 +49,8 @@ export async function GET(req: NextRequest) { return NextResponse.json({ assignments, total, page, pages: Math.ceil(total / limit) }) } catch (error) { - 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 }) + console.error('GET /api/assignments error:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } diff --git a/app/api/grades/route.ts b/app/api/grades/route.ts index 70b305f..4a660c5 100644 --- a/app/api/grades/route.ts +++ b/app/api/grades/route.ts @@ -47,8 +47,8 @@ export async function GET(req: NextRequest) { const grades = await Grade.find(query).sort({ createdAt: -1 }).lean() 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 }) + console.error('GET /api/grades error:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } @@ -80,9 +80,7 @@ export async function POST(req: NextRequest) { ) return NextResponse.json(grade, { status: 201 }) } catch (error) { - 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 }) + console.error('POST /api/grades error:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } From 2a3d23b0bcca3a8573f0c1acb4793ad9839eec81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Anh=20T=C3=BA?= Date: Sat, 18 Apr 2026 13:36:44 +0700 Subject: [PATCH 08/11] fix: Enhance OverviewClient to fetch and handle active assignments data --- app/dashboard/OverviewClient.tsx | 64 +++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/app/dashboard/OverviewClient.tsx b/app/dashboard/OverviewClient.tsx index 169795e..d48723b 100644 --- a/app/dashboard/OverviewClient.tsx +++ b/app/dashboard/OverviewClient.tsx @@ -189,27 +189,68 @@ export function OverviewClient() { const [ studentsRes, assignmentsRes, + activeAssignmentsRes, 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] = + const [ + studentsData, + assignmentsData, + activeAssignmentsData, + attendance, + grades, + announcements, + ] = await Promise.all([ studentsRes.json(), assignmentsRes.json(), + activeAssignmentsRes.json(), attendanceRes.json(), gradesRes.json(), announcementsRes.json(), ]); - const assignments = assignmentsData.assignments ?? assignmentsData; + const studentsList = Array.isArray(studentsData?.students) + ? studentsData.students + : Array.isArray(studentsData) + ? studentsData + : []; + + const assignmentsList = Array.isArray(assignmentsData?.assignments) + ? assignmentsData.assignments + : Array.isArray(assignmentsData) + ? assignmentsData + : []; + + const totalStudents = + typeof studentsData?.total === "number" + ? studentsData.total + : studentsList.length; + + const totalAssignments = + typeof assignmentsData?.total === "number" + ? assignmentsData.total + : assignmentsList.length; + + const pendingAssignments = + typeof activeAssignmentsData?.total === "number" + ? activeAssignmentsData.total + : Array.isArray(activeAssignmentsData?.assignments) + ? activeAssignmentsData.assignments.length + : Array.isArray(activeAssignmentsData) + ? activeAssignmentsData.length + : assignmentsList.filter( + (a: { status: string }) => a.status === "active", + ).length; // ── Attendance ── const dateMap: Record< @@ -296,7 +337,7 @@ export function OverviewClient() { // ── Upcoming deadlines ── const now = Date.now(); const upcomingDeadlines = ( - assignments as { + assignmentsList as { _id: string; title: string; subject: string; @@ -316,13 +357,9 @@ export function OverviewClient() { .slice(0, 5); 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, + totalStudents, + totalAssignments, + pendingAssignments, attendancePct, attendanceBreakdown: { present: totalPresent, @@ -334,7 +371,7 @@ export function OverviewClient() { gradeDistribution, upcomingDeadlines, recentAnnouncements: announcements.slice(0, 5), - recentStudents: students.students?.slice(0, 5) ?? [], + recentStudents: studentsList.slice(0, 5), }); setLastRefreshed(new Date()); } catch (err) { @@ -701,13 +738,12 @@ export function OverviewClient() { {stats.upcomingDeadlines.map((a) => (
  • {a.daysLeft < 0 ? "!" : a.daysLeft}
    From 3670fc091a3133668d12336446037417c63a589a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Anh=20T=C3=BA?= Date: Sat, 18 Apr 2026 13:41:36 +0700 Subject: [PATCH 09/11] fix: Refactor grading logic by introducing grading utility functions and removing inline calculations --- app/api/grades/[id]/route.ts | 14 ++------ app/api/grades/route.ts | 14 ++------ app/dashboard/OverviewClient.tsx | 20 ++++------- app/dashboard/grades/GradesClient.tsx | 44 +++++++++-------------- app/dashboard/students/StudentsClient.tsx | 35 ++++++++---------- lib/grading.ts | 31 ++++++++++++++++ 6 files changed, 72 insertions(+), 86 deletions(-) create mode 100644 lib/grading.ts diff --git a/app/api/grades/[id]/route.ts b/app/api/grades/[id]/route.ts index 0af35d6..6818a12 100644 --- a/app/api/grades/[id]/route.ts +++ b/app/api/grades/[id]/route.ts @@ -3,20 +3,10 @@ import { NextRequest, NextResponse } from 'next/server' import mongoose from 'mongoose' import { connectDB } from '@/lib/mongodb' import { Grade } from '@/models/Grade' +import { calculateLetterGrade } from '@/lib/grading' const ALLOWED_UPDATE_FIELDS = ['studentId', 'studentName', 'subject', 'term', 'marks', 'maxMarks'] -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 }) @@ -91,7 +81,7 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string { ...sanitizedBody, maxMarks: nextMaxMarks, - grade: calcGrade(nextMarks, nextMaxMarks), + grade: calculateLetterGrade(nextMarks, nextMaxMarks), }, { new: true, runValidators: true, context: 'query' } ) diff --git a/app/api/grades/route.ts b/app/api/grades/route.ts index 4a660c5..49cdad5 100644 --- a/app/api/grades/route.ts +++ b/app/api/grades/route.ts @@ -2,6 +2,7 @@ import { auth } from '@clerk/nextjs/server' import { NextRequest, NextResponse } from 'next/server' import { connectDB } from '@/lib/mongodb' import { Grade } from '@/models/Grade' +import { calculateLetterGrade } from '@/lib/grading' import { z } from 'zod' const GradeSchema = z.object({ @@ -19,17 +20,6 @@ const GradeSchema = z.object({ } ) -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 GET(req: NextRequest) { const { userId } = await auth() if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) @@ -75,7 +65,7 @@ export async function POST(req: NextRequest) { const grade = await Grade.findOneAndUpdate( { teacherId: userId, studentId: data.studentId, subject: data.subject, term }, - { $set: { ...data, maxMarks: max, term, teacherId: userId, grade: calcGrade(data.marks, max) } }, + { $set: { ...data, maxMarks: max, term, teacherId: userId, grade: calculateLetterGrade(data.marks, max) } }, { upsert: true, new: true, runValidators: true, context: 'query' } ) return NextResponse.json(grade, { status: 201 }) diff --git a/app/dashboard/OverviewClient.tsx b/app/dashboard/OverviewClient.tsx index d48723b..e31839f 100644 --- a/app/dashboard/OverviewClient.tsx +++ b/app/dashboard/OverviewClient.tsx @@ -8,6 +8,7 @@ import { } from 'recharts' import { CardSkeleton } from '@/components/ui/Skeleton' import { Badge } from '@/components/ui/Badge' +import { getGradePoint } from '@/lib/grading' interface Stats { totalStudents: number @@ -289,18 +290,9 @@ export function OverviewClient() { })); // ── CGPA trend ── - const GRADE_POINT: Record = { - "A+": 10, - A: 9, - "B+": 8, - B: 7, - C: 6, - D: 4, - F: 0, - }; const termMap: Record = {}; for (const g of grades) { - (termMap[g.term] ??= []).push(GRADE_POINT[g.grade] ?? 0); + (termMap[g.term] ??= []).push(getGradePoint(g.grade)); } const TERM_ORDER = [ "Term 1", @@ -739,10 +731,10 @@ export function OverviewClient() {
  • {a.daysLeft < 0 ? "!" : a.daysLeft} diff --git a/app/dashboard/grades/GradesClient.tsx b/app/dashboard/grades/GradesClient.tsx index fdaa00b..a307f1d 100644 --- a/app/dashboard/grades/GradesClient.tsx +++ b/app/dashboard/grades/GradesClient.tsx @@ -13,6 +13,7 @@ import { Badge } from '@/components/ui/Badge' import { TableSkeleton } from '@/components/ui/Skeleton' import { useToast } from '@/components/ui/Toast' import { ConfirmModal } from "@/components/ui/ConfirmModal"; +import { calculateCgpaFromGrades, getGradePoint } from '@/lib/grading' interface Grade { _id: string @@ -59,16 +60,6 @@ function pct(marks: number, max: number) { return Math.round((marks / max) * 100); } -const GRADE_POINT: Record = { - 'A+': 10, A: 9, 'B+': 8, B: 7, C: 6, D: 5, F: 0, -} - -function cgpaFromGrades(gradeList: Grade[]) { - if (!gradeList.length) return null - const total = gradeList.reduce((s, g) => s + (GRADE_POINT[g.grade] ?? 0), 0) - return (total / gradeList.length).toFixed(2) -} - const LINE_COLORS = ['#6366f1', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6'] function exportCsv(grades: Grade[], filename = "grades.csv") { @@ -211,9 +202,9 @@ export function GradesClient() { const avg = sg.length > 0 ? Math.round( - sg.reduce((s, g) => s + pct(g.marks, g.maxMarks), 0) / - sg.length, - ) + sg.reduce((s, g) => s + pct(g.marks, g.maxMarks), 0) / + sg.length, + ) : 0; return { subject: subject.length > 10 ? subject.slice(0, 10) + "…" : subject, @@ -243,7 +234,7 @@ export function GradesClient() { const sg = grades.filter((g) => g.studentId === cgpaStudentId); return { grades: sg, - cgpa: cgpaFromGrades(sg), + cgpa: calculateCgpaFromGrades(sg), name: students.find((s) => s._id === cgpaStudentId)?.name ?? "", }; }, [cgpaStudentId, grades, students]); @@ -339,9 +330,9 @@ export function GradesClient() { const activeAvg = activeGrades.length > 0 ? Math.round( - activeGrades.reduce((s, g) => s + pct(g.marks, g.maxMarks), 0) / - activeGrades.length, - ) + activeGrades.reduce((s, g) => s + pct(g.marks, g.maxMarks), 0) / + activeGrades.length, + ) : 0; const activeFilters = @@ -364,11 +355,10 @@ export function GradesClient() { @@ -673,7 +663,7 @@ export function GradesClient() { - {GRADE_POINT[g.grade] ?? 0} + {getGradePoint(g.grade)} ))} @@ -792,19 +782,17 @@ export function GradesClient() { diff --git a/lib/grading.ts b/lib/grading.ts new file mode 100644 index 0000000..efce5fe --- /dev/null +++ b/lib/grading.ts @@ -0,0 +1,31 @@ +export const GRADE_POINTS: Record = { + 'A+': 10, + A: 9, + 'B+': 8, + B: 7, + C: 6, + D: 5, + F: 0, +} + +export function getGradePoint(grade: string | null | undefined): number { + if (!grade) return 0 + return GRADE_POINTS[grade] ?? 0 +} + +export function calculateCgpaFromGrades(grades: T[]): string | null { + if (!grades.length) return null + const total = grades.reduce((sum, item) => sum + getGradePoint(item.grade), 0) + return (total / grades.length).toFixed(2) +} + +export function calculateLetterGrade(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' +} From 93e7a7c33a4c876c0e8aa9f3adcf7d1a35577155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Anh=20T=C3=BA?= Date: Sat, 18 Apr 2026 13:44:44 +0700 Subject: [PATCH 10/11] fix: Update allowed fields and enhance validation in PUT routes for announcements, assignments, and students --- app/api/announcements/[id]/route.ts | 20 +++++++++++++++++--- app/api/assignments/[id]/route.ts | 20 +++++++++++++++++--- app/api/students/[id]/route.ts | 20 +++++++++++++++++--- 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/app/api/announcements/[id]/route.ts b/app/api/announcements/[id]/route.ts index 22d4d0a..a44bcb6 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() @@ -27,14 +27,28 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string return NextResponse.json({ error: 'Invalid JSON request body' }, { status: 400 }) } + if (!body || typeof body !== 'object' || Array.isArray(body)) { + return NextResponse.json({ error: 'Invalid JSON request body' }, { status: 400 }) + } + + const payload = body as Record + const unknownFields = Object.keys(payload).filter((key) => !ALLOWED_FIELDS.includes(key)) + if (unknownFields.length > 0) { + return NextResponse.json({ error: `Unknown field(s): ${unknownFields.join(', ')}` }, { status: 400 }) + } + // Sanitize: only allow whitelisted fields const sanitizedBody: Record = {} for (const key of ALLOWED_FIELDS) { - if (key in body) { - sanitizedBody[key] = body[key] + if (Object.prototype.hasOwnProperty.call(payload, key)) { + sanitizedBody[key] = payload[key] } } + if (Object.keys(sanitizedBody).length === 0) { + return NextResponse.json({ error: 'No updatable fields provided' }, { status: 400 }) + } + const announcement = await Announcement.findOneAndUpdate( { _id: id, teacherId: userId }, { $set: sanitizedBody }, diff --git a/app/api/assignments/[id]/route.ts b/app/api/assignments/[id]/route.ts index b041302..0fae820 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() @@ -27,14 +27,28 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }) } + if (!body || typeof body !== 'object' || Array.isArray(body)) { + return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }) + } + + const payload = body as Record + const unknownFields = Object.keys(payload).filter((key) => !ALLOWED_UPDATE_FIELDS.includes(key)) + if (unknownFields.length > 0) { + return NextResponse.json({ error: `Unknown field(s): ${unknownFields.join(', ')}` }, { status: 400 }) + } + // Sanitize: only allow whitelisted fields const sanitizedBody: Record = {} for (const key of ALLOWED_UPDATE_FIELDS) { - if (key in body) { - sanitizedBody[key] = body[key] + if (Object.prototype.hasOwnProperty.call(payload, key)) { + sanitizedBody[key] = payload[key] } } + if (Object.keys(sanitizedBody).length === 0) { + return NextResponse.json({ error: 'No updatable fields provided' }, { status: 400 }) + } + const assignment = await Assignment.findOneAndUpdate( { _id: id, teacherId: userId }, sanitizedBody, diff --git a/app/api/students/[id]/route.ts b/app/api/students/[id]/route.ts index 990037b..92bb22d 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() @@ -25,14 +25,28 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string return NextResponse.json({ error: 'Bad Request' }, { status: 400 }) } + if (!body || typeof body !== 'object' || Array.isArray(body)) { + return NextResponse.json({ error: 'Bad Request' }, { status: 400 }) + } + + const payload = body as Record + const unknownFields = Object.keys(payload).filter((key) => !ALLOWED_UPDATE_FIELDS.includes(key)) + if (unknownFields.length > 0) { + return NextResponse.json({ error: `Unknown field(s): ${unknownFields.join(', ')}` }, { status: 400 }) + } + // Sanitize: only allow whitelisted fields const sanitizedBody: Record = {} for (const key of ALLOWED_UPDATE_FIELDS) { - if (key in body) { - sanitizedBody[key] = body[key] + if (Object.prototype.hasOwnProperty.call(payload, key)) { + sanitizedBody[key] = payload[key] } } + if (Object.keys(sanitizedBody).length === 0) { + return NextResponse.json({ error: 'No updatable fields provided' }, { status: 400 }) + } + await connectDB() const student = await Student.findOneAndUpdate( { _id: id, teacherId: userId }, From 139f0a6a4b7e2a52b445090956ef30c8ece255a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Anh=20T=C3=BA?= Date: Sat, 18 Apr 2026 13:47:18 +0700 Subject: [PATCH 11/11] fix: Enhance PUT routes for assignments, grades, and students with improved validation and database connection handling --- app/api/assignments/[id]/route.ts | 6 +++--- app/api/grades/[id]/route.ts | 26 ++++++++++++++++++++------ app/api/students/[id]/route.ts | 2 +- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/app/api/assignments/[id]/route.ts b/app/api/assignments/[id]/route.ts index 0fae820..f6b5ff8 100644 --- a/app/api/assignments/[id]/route.ts +++ b/app/api/assignments/[id]/route.ts @@ -18,8 +18,6 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string return NextResponse.json({ error: 'Invalid id' }, { status: 400 }) } - await connectDB() - let body try { body = await req.json() @@ -49,10 +47,12 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string return NextResponse.json({ error: 'No updatable fields provided' }, { status: 400 }) } + await connectDB() + const assignment = await Assignment.findOneAndUpdate( { _id: id, teacherId: userId }, sanitizedBody, - { new: true } + { new: true, runValidators: true, context: 'query' } ) if (!assignment) return NextResponse.json({ error: 'Not found' }, { status: 404 }) return NextResponse.json(assignment) diff --git a/app/api/grades/[id]/route.ts b/app/api/grades/[id]/route.ts index 6818a12..0929397 100644 --- a/app/api/grades/[id]/route.ts +++ b/app/api/grades/[id]/route.ts @@ -26,18 +26,27 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }) } + if (!body || typeof body !== 'object' || Array.isArray(body)) { + return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }) + } + + const payload = body as Record + const unknownFields = Object.keys(payload).filter((key) => !ALLOWED_UPDATE_FIELDS.includes(key)) + if (unknownFields.length > 0) { + return NextResponse.json({ error: `Unknown field(s): ${unknownFields.join(', ')}` }, { status: 400 }) + } + // Sanitize: only allow whitelisted fields const sanitizedBody: Record = {} for (const key of ALLOWED_UPDATE_FIELDS) { - if (key in body) { - sanitizedBody[key] = body[key] + if (Object.prototype.hasOwnProperty.call(payload, key)) { + sanitizedBody[key] = payload[key] } } - await connectDB() - - const existingGrade = await Grade.findOne({ _id: id, teacherId: userId }) - if (!existingGrade) return NextResponse.json({ error: 'Not found' }, { status: 404 }) + if (Object.keys(sanitizedBody).length === 0) { + return NextResponse.json({ error: 'No updatable fields provided' }, { status: 400 }) + } if ('studentId' in sanitizedBody) { const studentId = sanitizedBody.studentId @@ -69,6 +78,11 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string } } + await connectDB() + + const existingGrade = await Grade.findOne({ _id: id, teacherId: userId }) + if (!existingGrade) return NextResponse.json({ error: 'Not found' }, { status: 404 }) + const nextMarks = typeof sanitizedBody.marks === 'number' ? sanitizedBody.marks : existingGrade.marks const nextMaxMarks = typeof sanitizedBody.maxMarks === 'number' ? sanitizedBody.maxMarks : (existingGrade.maxMarks ?? 100) diff --git a/app/api/students/[id]/route.ts b/app/api/students/[id]/route.ts index 92bb22d..478c65e 100644 --- a/app/api/students/[id]/route.ts +++ b/app/api/students/[id]/route.ts @@ -51,7 +51,7 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string const student = await Student.findOneAndUpdate( { _id: id, teacherId: userId }, sanitizedBody, - { new: true } + { new: true, runValidators: true, context: 'query' } ) if (!student) return NextResponse.json({ error: 'Not found' }, { status: 404 }) return NextResponse.json(student)