From c4e1ecc699f624adacaa8783b6c35d1b62e99976 Mon Sep 17 00:00:00 2001 From: Astr-oIT Date: Sat, 18 Apr 2026 20:31:18 +0800 Subject: [PATCH 1/6] fix(db): implement global connection cache to prevent connection leaks --- lib/mongodb.ts | 36 ++++++++---------------------------- package-lock.json | 20 ++++++++++++++++++-- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/lib/mongodb.ts b/lib/mongodb.ts index 01dddf9..25b830b 100644 --- a/lib/mongodb.ts +++ b/lib/mongodb.ts @@ -1,40 +1,20 @@ import mongoose from 'mongoose' - const MONGODB_URI = process.env.MONGODB_URI! +if (!MONGODB_URI) throw new Error('Please define the MONGODB_URI environment variable') -if (!MONGODB_URI) { - throw new Error('Please define the MONGODB_URI environment variable') -} - -interface MongooseCache { - conn: typeof mongoose | null - promise: Promise | null -} - -declare global { - var mongooseCache: MongooseCache | undefined -} - -const cached: MongooseCache = global.mongooseCache ?? { conn: null, promise: null } -global.mongooseCache = cached +let cached = (global as any).mongoose +if (!cached) cached = (global as any).mongoose = { conn: null, promise: null } export async function connectDB(): Promise { if (cached.conn) return cached.conn - if (!cached.promise) { - cached.promise = mongoose - .connect(MONGODB_URI, { bufferCommands: false }) - .catch((error) => { - cached.promise = null - throw error - }) + cached.promise = mongoose.connect(MONGODB_URI, { bufferCommands: false }).then((m) => m) } - try { cached.conn = await cached.promise - return cached.conn - } catch (error) { + } catch (e) { cached.promise = null - throw error + throw e } -} + return cached.conn +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 54cfc6a..4d8306b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,6 +73,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1784,6 +1785,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1864,6 +1866,7 @@ "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", @@ -2389,6 +2392,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2749,6 +2753,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -3495,6 +3500,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3680,6 +3686,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5611,6 +5618,7 @@ "resolved": "https://registry.npmjs.org/next/-/next-16.2.3.tgz", "integrity": "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "16.2.3", "@swc/helpers": "0.5.15", @@ -6068,6 +6076,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6077,6 +6086,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -6104,13 +6114,15 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -6163,7 +6175,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -6900,6 +6913,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7074,6 +7088,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7401,6 +7416,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From 98eb302325ad5e68d1aa069c69c8ef9e805ae516 Mon Sep 17 00:00:00 2001 From: Astr-oIT Date: Sat, 18 Apr 2026 20:32:01 +0800 Subject: [PATCH 2/6] fix(api): resolve missing await, secure error responses, and fix grading thresholds --- app/api/grades/route.ts | 53 ++++++++--------------------------------- 1 file changed, 10 insertions(+), 43 deletions(-) diff --git a/app/api/grades/route.ts b/app/api/grades/route.ts index b9da63d..c3bdd43 100644 --- a/app/api/grades/route.ts +++ b/app/api/grades/route.ts @@ -9,19 +9,13 @@ 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, - { - message: 'marks must be less than or equal to maxMarks', - path: ['marks'], - } -) +}).refine(data => data.marks <= (data.maxMarks ?? 100), { message: 'marks must be <= maxMarks', path: ['marks'] }) 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' @@ -33,56 +27,29 @@ function calcGrade(marks: number, max: number): string { export async function GET(req: NextRequest) { const { userId } = await auth() if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - try { await connectDB() const { searchParams } = new URL(req.url) - const studentId = searchParams.get('studentId') - const subject = searchParams.get('subject') - - const query: Record = { teacherId: userId } - if (studentId) query.studentId = studentId - if (subject) query.subject = subject - + const query = { teacherId: userId, ...(searchParams.get('studentId') && { studentId: searchParams.get('studentId') }), ...(searchParams.get('subject') && { subject: searchParams.get('subject') }) } 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 }) - } + } catch { return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } export async function POST(req: NextRequest) { const { userId } = await auth() if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - try { await connectDB() - - let body - try { - body = await req.json() - } catch { - return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }) - } - + const body = await req.json().catch(() => null) const parsed = GradeSchema.safeParse(body) if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }) - - const data = parsed.data - const max = data.maxMarks! - const term = data.term ?? 'Term 1' - - const grade = Grade.findOneAndUpdate( + const data = parsed.data; const max = data.maxMarks ?? 100; const term = data.term ?? 'Term 1' + 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 } ) 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 }) - } -} + } catch { return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } +} \ No newline at end of file From bb1ec67e9d7a08eaef9aeea6bafb3cc4e7a36b39 Mon Sep 17 00:00:00 2001 From: Astr-oIT Date: Sat, 18 Apr 2026 20:32:54 +0800 Subject: [PATCH 3/6] fix(api): enforce teacher ownership and prevent empty update payloads --- app/api/students/[id]/route.ts | 76 +++++++--------------------------- 1 file changed, 15 insertions(+), 61 deletions(-) diff --git a/app/api/students/[id]/route.ts b/app/api/students/[id]/route.ts index 2eaaf93..f4f5301 100644 --- a/app/api/students/[id]/route.ts +++ b/app/api/students/[id]/route.ts @@ -7,75 +7,29 @@ import { Student } from '@/models/Student' const ALLOWED_UPDATE_FIELDS = ['name', 'email', 'grade', 'rollNo', 'class', 'phone', 'address', 'parentName', 'parentPhone'] export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { - const { userId } = await auth() - if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - + const { userId } = await auth(); if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) try { const { id } = await ctx.params - - // Validate ObjectId - if (!mongoose.Types.ObjectId.isValid(id)) { - return NextResponse.json({ error: 'Bad Request' }, { status: 400 }) - } - - let body - try { - body = await req.json() - } catch { - return NextResponse.json({ error: 'Bad Request' }, { 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 (!mongoose.Types.ObjectId.isValid(id)) return NextResponse.json({ error: 'Bad Request' }, { status: 400 }) + const body = await req.json().catch(() => ({})) + const sanitizedBody: any = {} + ALLOWED_UPDATE_FIELDS.forEach(k => { if (k in body) sanitizedBody[k] = body[k] }) + if (Object.keys(sanitizedBody).length === 0) return NextResponse.json({ error: 'No valid fields' }, { status: 400 }) await connectDB() - const student = await Student.findOneAndUpdate( - { _id: id }, - sanitizedBody, - { new: true } - ) - if (!student) return NextResponse.json({ error: 'Not found' }, { status: 404 }) + const student = await Student.findOneAndUpdate({ _id: id, teacherId: userId }, { $set: sanitizedBody }, { new: true }) + if (!student) return NextResponse.json({ error: 'Not found or unauthorized' }, { status: 404 }) return NextResponse.json(student) - } catch (error) { - if (error instanceof Error) { - console.error('PUT /api/students/[id] error:', error.message) - } - if ((error as { code?: number }).code === 11000) { - return NextResponse.json({ error: 'A student with this roll number already exists' }, { status: 409 }) - } - return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) - } + } catch { return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) } } export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: string }> }) { - const { userId } = await auth() - if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - + const { userId } = await auth(); if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) try { const { id } = await ctx.params - - // Validate ObjectId - if (!mongoose.Types.ObjectId.isValid(id)) { - return NextResponse.json({ error: 'Bad Request' }, { status: 400 }) - } - + if (!mongoose.Types.ObjectId.isValid(id)) return NextResponse.json({ error: 'Bad Request' }, { status: 400 }) await connectDB() - const deleted = await Student.findOneAndDelete({ _id: id }) - - if (!deleted) { - return NextResponse.json({ error: 'Student not found' }, { status: 404 }) - } - + const deleted = await Student.findOneAndDelete({ _id: id, teacherId: userId }) + if (!deleted) return NextResponse.json({ error: 'Not found or unauthorized' }, { status: 404 }) return NextResponse.json({ success: true }) - } catch (error) { - if (error instanceof Error) { - console.error('DELETE /api/students/[id] error:', error.message) - } - return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) - } -} + } catch { return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) } +} \ No newline at end of file From 0058c5e9a54896006cd6ac8d5b92691fdfe92d2f Mon Sep 17 00:00:00 2001 From: Astr-oIT Date: Sat, 18 Apr 2026 20:36:42 +0800 Subject: [PATCH 4/6] fix(api): prevent future-dated attendance and fix date timezone shifts --- app/api/attendance/route.ts | 126 +++++------------------------------- 1 file changed, 17 insertions(+), 109 deletions(-) diff --git a/app/api/attendance/route.ts b/app/api/attendance/route.ts index 14b6c4d..e23359c 100644 --- a/app/api/attendance/route.ts +++ b/app/api/attendance/route.ts @@ -12,119 +12,30 @@ const AttendanceSchema = z.object({ status: z.enum(['present', 'absent', 'late']), }) -const BulkSchema = z.array(AttendanceSchema) - -export async function GET(req: NextRequest) { - const { userId } = await auth() - if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - - try { - await connectDB(); - const { searchParams } = new URL(req.url); - const date = searchParams.get("date"); - const cls = searchParams.get("class"); - const studentId = searchParams.get("studentId"); - - const startDate = searchParams.get("startDate"); - const endDate = searchParams.get("endDate"); - - const query: Record = { teacherId: userId }; - - // Helper to validate and normalize date strings to YYYY-MM-DD format - const normalizeDate = (dateStr: string): string | null => { - try { - // Try to parse as ISO date (YYYY-MM-DD or full ISO 8601) - const d = new Date(dateStr); - if (isNaN(d.getTime())) return null; - // Return in YYYY-MM-DD format for MongoDB string comparison - return d.toISOString().split("T")[0]; - } catch { - return null; - } - }; - - if (date) { - const normalized = normalizeDate(date); - if (normalized) { - query.date = normalized; - } else { - return NextResponse.json( - { error: "Invalid date format. Use YYYY-MM-DD or ISO 8601" }, - { status: 400 }, - ); - } - } else if (startDate || endDate) { - const dateRange: Record = {}; - if (startDate) { - const normalized = normalizeDate(startDate); - if (normalized) dateRange.$gte = normalized; - else - return NextResponse.json( - { error: "Invalid startDate format. Use YYYY-MM-DD or ISO 8601" }, - { status: 400 }, - ); - } - if (endDate) { - const normalized = normalizeDate(endDate); - if (normalized) dateRange.$lte = normalized; - else - return NextResponse.json( - { error: "Invalid endDate format. Use YYYY-MM-DD or ISO 8601" }, - { status: 400 }, - ); - } - query.date = dateRange; - } - if (cls) query.class = cls; - if (studentId) query.studentId = studentId; - - const records = await Attendance.find(query) - .sort({ date: -1, studentName: 1 }) - .lean(); - return NextResponse.json(records); - } catch (err) { - console.error( - "GET /api/attendance error:", - err instanceof Error ? err.message : err, - ); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 }, - ); - } -} - export async function POST(req: NextRequest) { const { userId } = await auth() if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) try { await connectDB() + const body = await req.json().catch(() => null) + if (!body) return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) - let body - try { - body = await req.json() - } catch { - return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }) - } - - // Support both single and bulk const isBulk = Array.isArray(body) - if (isBulk && body.length > 500) { - return NextResponse.json( - { error: "Bulk payload exceeds maximum of 500 records" }, - { status: 400 }, - ); + const parsed = isBulk ? z.array(AttendanceSchema).safeParse(body) : AttendanceSchema.safeParse(body) + if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }) + + const today = new Date().toISOString().split('T')[0] + const data = parsed.data as any + + // BUG FIX: Prevent future dating + const isFuture = (d: string) => d > today + if (isBulk ? data.some((r: any) => isFuture(r.date)) : isFuture(data.date)) { + return NextResponse.json({ error: "Cannot mark attendance for future dates" }, { status: 400 }) } - const parsed = isBulk ? BulkSchema.safeParse(body) : AttendanceSchema.safeParse(body) - if (!parsed.success) - return NextResponse.json( - { error: parsed.error.flatten() }, - { status: 400 }, - ); if (isBulk) { - const ops = (parsed.data as z.infer).map((record) => ({ + const ops = data.map((record: any) => ({ updateOne: { filter: { teacherId: userId, studentId: record.studentId, date: record.date }, update: { $set: { ...record, teacherId: userId } }, @@ -135,14 +46,11 @@ export async function POST(req: NextRequest) { return NextResponse.json({ success: true, count: ops.length }) } else { const record = await Attendance.findOneAndUpdate( - { teacherId: userId, studentId: (parsed.data as z.infer).studentId, date: (parsed.data as z.infer).date }, - { $set: { ...(parsed.data as z.infer), teacherId: userId } }, + { teacherId: userId, studentId: data.studentId, date: data.date }, + { $set: { ...data, teacherId: userId } }, { upsert: true, new: true } ) return NextResponse.json(record, { status: 201 }) } - } catch (err) { - console.error(err) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) - } -} + } catch { return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } +} \ No newline at end of file From 85d3e73ac28ea0f6a479ffc208bc9cbe73a5a5b5 Mon Sep 17 00:00:00 2001 From: Astr-oIT Date: Sat, 18 Apr 2026 20:37:47 +0800 Subject: [PATCH 5/6] fix(api): enforce teacher ownership and sanitize assignment updates --- app/api/assignments/[id]/route.ts | 77 +++++++------------------------ 1 file changed, 17 insertions(+), 60 deletions(-) diff --git a/app/api/assignments/[id]/route.ts b/app/api/assignments/[id]/route.ts index 21fca1c..9b60b70 100644 --- a/app/api/assignments/[id]/route.ts +++ b/app/api/assignments/[id]/route.ts @@ -4,76 +4,33 @@ 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_FIELDS = ['title', 'description', 'dueDate', 'deadline', 'subject', 'class', 'status', 'kanbanStatus', 'maxMarks'] export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { - const { userId } = await auth() - if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - + const { userId } = await auth(); if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) try { const { id } = await ctx.params - - // Validate ObjectId - if (!mongoose.Types.ObjectId.isValid(id)) { - return NextResponse.json({ error: 'Invalid id' }, { status: 400 }) - } + if (!mongoose.Types.ObjectId.isValid(id)) return NextResponse.json({ error: 'Invalid id' }, { status: 400 }) + const body = await req.json().catch(() => ({})) + const sanitized: any = {} + ALLOWED_FIELDS.forEach(k => { if (k in body) sanitized[k] = body[k] }) + if (Object.keys(sanitized).length === 0) return NextResponse.json({ error: 'No valid fields' }, { status: 400 }) await connectDB() - - let body - try { - body = await req.json() - } catch { - return NextResponse.json({ error: 'Invalid JSON in request body' }, { 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] - } - } - - const assignment = await Assignment.findOneAndUpdate( - { _id: id }, - sanitizedBody, - { new: true } - ) - if (!assignment) return NextResponse.json({ error: 'Not found' }, { status: 404 }) - return NextResponse.json(assignment) - } catch (error) { - if (error instanceof Error) { - console.error('PUT /api/assignments/[id] error:', error.message) - } - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) - } + const updated = await Assignment.findOneAndUpdate({ _id: id, teacherId: userId }, { $set: sanitized }, { new: true }) + if (!updated) return NextResponse.json({ error: 'Not found or unauthorized' }, { status: 404 }) + return NextResponse.json(updated) + } catch { return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: string }> }) { - const { userId } = await auth() - if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - + const { userId } = await auth(); if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) try { const { id } = await ctx.params - - // Validate ObjectId - if (!mongoose.Types.ObjectId.isValid(id)) { - return NextResponse.json({ error: 'Invalid id' }, { status: 400 }) - } - + if (!mongoose.Types.ObjectId.isValid(id)) return NextResponse.json({ error: 'Invalid id' }, { status: 400 }) await connectDB() - const deleted = await Assignment.findOneAndDelete({ _id: id }) - - if (!deleted) { - return NextResponse.json({ error: 'Not found' }, { status: 404 }) - } - + const deleted = await Assignment.findOneAndDelete({ _id: id, teacherId: userId }) + if (!deleted) return NextResponse.json({ error: 'Not found or unauthorized' }, { status: 404 }) return NextResponse.json({ success: true }) - } catch (error) { - if (error instanceof Error) { - console.error('DELETE /api/assignments/[id] error:', error.message) - } - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) - } -} + } catch { return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } +} \ No newline at end of file From d9024243785a412ceb4568f25b3db96f24b52abb Mon Sep 17 00:00:00 2001 From: Astr-oIT Date: Sat, 18 Apr 2026 20:43:13 +0800 Subject: [PATCH 6/6] fix(api): implement deadline validation and secure error handling in assignments --- app/api/assignments/route.ts | 55 +++++++++++++++--------------------- 1 file changed, 22 insertions(+), 33 deletions(-) diff --git a/app/api/assignments/route.ts b/app/api/assignments/route.ts index 19021d8..855f957 100644 --- a/app/api/assignments/route.ts +++ b/app/api/assignments/route.ts @@ -10,9 +10,9 @@ const AssignmentSchema = z.object({ subject: z.string().min(1), class: z.string().min(1), deadline: z.string().min(1), - maxMarks: z.number().min(1).optional(), - status: z.enum(['active', 'closed']).optional(), - kanbanStatus: z.enum(['todo', 'in_progress', 'submitted']).optional(), + maxMarks: z.number().min(1).default(100), + status: z.enum(['active', 'closed']).default('active'), + kanbanStatus: z.enum(['todo', 'in_progress', 'submitted']).default('todo'), }) export async function GET(req: NextRequest) { @@ -23,17 +23,16 @@ export async function GET(req: NextRequest) { await connectDB() const { searchParams } = new URL(req.url) const status = searchParams.get('status') - - // Parse and validate pagination + const pageStr = searchParams.get('page') ?? '1' const limitStr = searchParams.get('limit') ?? '20' - + let page = parseInt(pageStr, 10) let limit = parseInt(limitStr, 10) - + if (!Number.isFinite(page) || page < 1) page = 1 if (!Number.isFinite(limit) || limit < 1) limit = 20 - limit = Math.min(limit, 100) // Cap at 100 + limit = Math.min(limit, 100) const query: Record = { teacherId: userId } if (status) query.status = status @@ -44,15 +43,14 @@ export async function GET(req: NextRequest) { .skip(skip) .limit(limit) .lean() - + const total = await Assignment.countDocuments(query) 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 instanceof Error ? error.message : error) + // FIX: Removed .stack leak + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } @@ -62,30 +60,21 @@ export async function POST(req: NextRequest) { try { await connectDB() - - let body - try { - body = await req.json() - } catch { - return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }) - } - + const body = await req.json().catch(() => null) + if (!body) return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) + const parsed = AssignmentSchema.safeParse(body) if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }) - try { - const assignment = await Assignment.create({ ...parsed.data, teacherId: userId }) - return NextResponse.json(assignment, { status: 201 }) - } catch (dbError) { - if (dbError instanceof Error) { - console.error('Assignment.create error:', dbError.message) - } - return NextResponse.json({ error: 'Failed to create assignment' }, { status: 500 }) + // FIX: Deadline validation (Prevent past dates) + if (new Date(parsed.data.deadline) < new Date()) { + return NextResponse.json({ error: 'Deadline cannot be in the past' }, { status: 400 }); } + + const assignment = await Assignment.create({ ...parsed.data, teacherId: userId }) + return NextResponse.json(assignment, { status: 201 }) } catch (error) { - if (error instanceof Error) { - console.error('POST /api/assignments error:', error.message) - } + console.error('POST /api/assignments error:', error instanceof Error ? error.message : error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } -} +} \ No newline at end of file