From a3419c539077418ae5112290f7e3336d9e9efe17 Mon Sep 17 00:00:00 2001 From: Reem-15 Date: Mon, 20 Apr 2026 06:51:15 +0200 Subject: [PATCH 01/12] fix: defer MongoDB env validation to prevent build failure --- lib/mongodb.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/mongodb.ts b/lib/mongodb.ts index 01dddf9..a3f04e2 100644 --- a/lib/mongodb.ts +++ b/lib/mongodb.ts @@ -1,10 +1,6 @@ import mongoose from 'mongoose' -const MONGODB_URI = process.env.MONGODB_URI! - -if (!MONGODB_URI) { - throw new Error('Please define the MONGODB_URI environment variable') -} +// Remove the global throw from here! interface MongooseCache { conn: typeof mongoose | null @@ -21,6 +17,12 @@ global.mongooseCache = cached export async function connectDB(): Promise { if (cached.conn) return cached.conn + // MOVE THE CHECK HERE + const MONGODB_URI = process.env.MONGODB_URI + if (!MONGODB_URI) { + throw new Error('Please define the MONGODB_URI environment variable') + } + if (!cached.promise) { cached.promise = mongoose .connect(MONGODB_URI, { bufferCommands: false }) @@ -37,4 +39,4 @@ export async function connectDB(): Promise { cached.promise = null throw error } -} +} \ No newline at end of file From 4c0bf6b4f3746393c45591be14b56bd847f30166 Mon Sep 17 00:00:00 2001 From: Reem-15 Date: Mon, 20 Apr 2026 06:51:28 +0200 Subject: [PATCH 02/12] fix: enforce Zod validation and prevent field injection on student creation --- app/api/students/route.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/app/api/students/route.ts b/app/api/students/route.ts index 8f3dcc2..8c28459 100644 --- a/app/api/students/route.ts +++ b/app/api/students/route.ts @@ -29,7 +29,6 @@ export async function GET(req: NextRequest) { const search = (searchParams.get("search") ?? "").replace(/\s+/g, ' '); const classFilter = searchParams.get("class") ?? ""; - // Parse and validate pagination const pageStr = searchParams.get("page") ?? "1"; const limitStr = searchParams.get("limit") ?? "20"; @@ -38,11 +37,10 @@ export async function GET(req: NextRequest) { if (Number.isNaN(page) || page < 1) page = 1; if (Number.isNaN(limit) || limit < 1) limit = 20; - limit = Math.min(limit, 100); // Cap at 100 + limit = Math.min(limit, 100); const query: Record = { teacherId: userId }; if (search) { - // Escape regex special characters to prevent ReDoS const escapedSearch = escapeRegex(search); query.$or = [ { name: { $regex: escapedSearch, $options: "i" } }, @@ -50,7 +48,6 @@ export async function GET(req: NextRequest) { { class: { $regex: escapedSearch, $options: "i" } }, ]; } - // Add class filter if provided and not 'all' if (classFilter && classFilter !== "all") { query.class = classFilter; } @@ -84,17 +81,25 @@ export async function POST(req: NextRequest) { try { await connectDB() - + let body try { body = await req.json() } catch { return NextResponse.json({ error: 'Malformed JSON' }, { status: 400 }) } - - StudentSchema.safeParse(body) - const student = await Student.create({ ...(body as Record), teacherId: userId }) + // 1. Perform actual validation and catch errors + const validation = StudentSchema.safeParse(body) + if (!validation.success) { + return NextResponse.json({ + error: 'Validation failed', + details: validation.error.format() + }, { status: 400 }) + } + + // 2. Only use validated data (prevents field injection) + const student = await Student.create({ ...validation.data, teacherId: userId }) return NextResponse.json(student, { status: 201 }) } catch (error) { if (error instanceof Error) { @@ -105,4 +110,4 @@ export async function POST(req: NextRequest) { } return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } -} +} \ No newline at end of file From 462a9502e49c2ddea6cacbb416c496403a5752f4 Mon Sep 17 00:00:00 2001 From: Reem-15 Date: Mon, 20 Apr 2026 06:51:39 +0200 Subject: [PATCH 03/12] security: scope student mutations to authenticated teacher --- app/api/students/[id]/route.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/api/students/[id]/route.ts b/app/api/students/[id]/route.ts index 2eaaf93..fd690b7 100644 --- a/app/api/students/[id]/route.ts +++ b/app/api/students/[id]/route.ts @@ -33,12 +33,12 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string } } - await connectDB() - const student = await Student.findOneAndUpdate( - { _id: id }, - sanitizedBody, - { new: true } - ) + // FIXED +const student = await Student.findOneAndUpdate( + { _id: id, teacherId: userId }, // ← add this + sanitizedBody, + { new: true } +) if (!student) return NextResponse.json({ error: 'Not found' }, { status: 404 }) return NextResponse.json(student) } catch (error) { @@ -65,7 +65,8 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str } await connectDB() - const deleted = await Student.findOneAndDelete({ _id: id }) +// FIXED +const deleted = await Student.findOneAndDelete({ _id: id, teacherId: userId }) if (!deleted) { return NextResponse.json({ error: 'Student not found' }, { status: 404 }) From ce412663dfbefbc2970390f4409180f54671b961 Mon Sep 17 00:00:00 2001 From: Reem-15 Date: Mon, 20 Apr 2026 06:51:52 +0200 Subject: [PATCH 04/12] security: scope assignment mutations to authenticated teacher --- app/api/assignments/[id]/route.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/api/assignments/[id]/route.ts b/app/api/assignments/[id]/route.ts index 21fca1c..e5f1ff2 100644 --- a/app/api/assignments/[id]/route.ts +++ b/app/api/assignments/[id]/route.ts @@ -36,7 +36,8 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string } const assignment = await Assignment.findOneAndUpdate( - { _id: id }, +// FIXED +{ _id: id, teacherId: userId }, sanitizedBody, { new: true } ) @@ -63,7 +64,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 }) From ebd0b74355ae010948996eb25d0bf40685393f81 Mon Sep 17 00:00:00 2001 From: Reem-15 Date: Mon, 20 Apr 2026 06:52:27 +0200 Subject: [PATCH 05/12] harden: replace error stack trace with generic message in assignments GET --- app/api/assignments/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 }) } } From 504d9970de82f7fe7a1ed8531790e6219bbcf4df Mon Sep 17 00:00:00 2001 From: Reem-15 Date: Mon, 20 Apr 2026 06:52:43 +0200 Subject: [PATCH 06/12] security: scope announcement mutations to authenticated teacher --- app/api/announcements/[id]/route.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/announcements/[id]/route.ts b/app/api/announcements/[id]/route.ts index 6c32f47..dcf0324 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}, // ← add this to ensure users can only update their own announcements { $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 }) // ← add this to ensure users can only delete their own announcements if (!deleted) { return NextResponse.json({ error: 'Not found' }, { status: 404 }) From e467cbb84e24882e3b432d4c5885a847b1fa01b3 Mon Sep 17 00:00:00 2001 From: Reem-15 Date: Mon, 20 Apr 2026 06:53:01 +0200 Subject: [PATCH 07/12] security: scope grade mutations to authenticated teacher and fix recalculation --- app/api/grades/[id]/route.ts | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/app/api/grades/[id]/route.ts b/app/api/grades/[id]/route.ts index 0141f63..9de3a3b 100644 --- a/app/api/grades/[id]/route.ts +++ b/app/api/grades/[id]/route.ts @@ -13,7 +13,6 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string try { const { id } = await ctx.params - // Validate ObjectId if (!mongoose.Types.ObjectId.isValid(id)) { return NextResponse.json({ error: 'Not found' }, { status: 404 }) } @@ -25,7 +24,6 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string 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) { @@ -34,13 +32,27 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string } await connectDB() - const grade = await Grade.findOneAndUpdate( - { _id: id }, - sanitizedBody, - { new: true } - ) - if (!grade) return NextResponse.json({ error: 'Not found' }, { status: 404 }) - return NextResponse.json(grade) + + // 1. Find first to ensure ownership + const existingGrade = await Grade.findOne({ _id: id, teacherId: userId }) + if (!existingGrade) return NextResponse.json({ error: 'Not found' }, { status: 404 }) + + // 2. Apply whitelisted updates + Object.assign(existingGrade, sanitizedBody) + + // 3. Recalculate grade string if marks changed + if (existingGrade.marks != null && existingGrade.maxMarks != null) { + const percentage = (existingGrade.marks / existingGrade.maxMarks) * 100 + if (percentage >= 90) existingGrade.grade = 'A+' + else if (percentage >= 80) existingGrade.grade = 'A' + else if (percentage >= 70) existingGrade.grade = 'B' + else existingGrade.grade = 'F' + } + + // 4. Save the document + await existingGrade.save() + return NextResponse.json(existingGrade) + } catch (error) { if (error instanceof Error) { console.error('PUT /api/grades/[id] error:', error.message) @@ -49,6 +61,7 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string } } +// Only teachers can delete their own grades export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: string }> }) { const { userId } = await auth() if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) @@ -56,7 +69,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 }) @@ -69,4 +82,4 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str } return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } -} +} \ No newline at end of file From 2b6d239054a04e464f09e4565de64a41f9cd2ce0 Mon Sep 17 00:00:00 2001 From: Reem-15 Date: Mon, 20 Apr 2026 06:53:11 +0200 Subject: [PATCH 08/12] fix: add missing await to grade upsert, correct A+ threshold, remove stack traces --- 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..501a369 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' @@ -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 }) } } @@ -72,8 +72,8 @@ export async function POST(req: NextRequest) { const data = parsed.data const max = data.maxMarks! const term = data.term ?? 'Term 1' - - const grade = Grade.findOneAndUpdate( + //Fixed + 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 } From f893ae489cf2de50455295cd022a786c0c63520b Mon Sep 17 00:00:00 2001 From: Reem-15 Date: Mon, 20 Apr 2026 06:53:25 +0200 Subject: [PATCH 09/12] security: remove arbitrary userId query param from profile GET --- 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..f890800 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 f61793dc4d965c66fd046cb58a0df8a233f37257 Mon Sep 17 00:00:00 2001 From: Reem-15 Date: Mon, 20 Apr 2026 06:53:41 +0200 Subject: [PATCH 10/12] fix: stabilize navbar theme initialization to prevent hydration mismatch --- components/dashboard/Navbar.tsx | 48 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/components/dashboard/Navbar.tsx b/components/dashboard/Navbar.tsx index 49b4a0c..f95e974 100644 --- a/components/dashboard/Navbar.tsx +++ b/components/dashboard/Navbar.tsx @@ -1,31 +1,26 @@ -'use client' +"use client"; -import { useState, useEffect } from 'react' +import { useState, useEffect } from "react"; interface NavbarProps { - onMenuClick: () => void - title: string + onMenuClick: () => void; + title: string; } -export function Navbar({ onMenuClick, title }: NavbarProps) { - const [dark, setDark] = useState(false); +function getInitialDark(): boolean { + if (typeof window === "undefined") return false; + try { + const stored = localStorage.getItem("theme"); + if (stored) return stored === "dark"; + return window.matchMedia("(prefers-color-scheme: dark)").matches; + } catch { + return false; + } +} - // Initialize theme from localStorage and system preference on client-side only - 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 - } - }, []); +export function Navbar({ onMenuClick, title }: NavbarProps) { + const [dark, setDark] = useState(getInitialDark); - // Sync dark class to whenever dark state changes useEffect(() => { document.documentElement.classList.toggle("dark", dark); }, [dark]); @@ -33,12 +28,15 @@ export function Navbar({ onMenuClick, title }: NavbarProps) { const toggleDark = () => { const newDark = !dark; setDark(newDark); - document.documentElement.classList.toggle("dark", newDark); - localStorage.setItem("theme", newDark ? "dark" : "light"); + try { + localStorage.setItem("theme", newDark ? "dark" : "light"); + } catch { + // Silently fail if localStorage is not available + } }; return ( -
+
{/* Left: Hamburger + Title */}
); -} +} \ No newline at end of file From ee6f9c17411417eea2398d58eb151ac89cddeec5 Mon Sep 17 00:00:00 2001 From: Reem-15 Date: Mon, 20 Apr 2026 06:53:59 +0200 Subject: [PATCH 11/12] fix: include teacherId in attendance unique index to prevent cross-teacher conflicts --- models/Attendance.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/Attendance.ts b/models/Attendance.ts index 3f05fb1..d917e9f 100644 --- a/models/Attendance.ts +++ b/models/Attendance.ts @@ -23,7 +23,7 @@ const AttendanceSchema = new Schema( }, { timestamps: true } ) - -AttendanceSchema.index({ studentId: 1, date: 1 }, { unique: true }) +//FIXED +AttendanceSchema.index({ teacherId: 1, studentId: 1, date: 1 }, { unique: true }) export const Attendance = models.Attendance ?? model('Attendance', AttendanceSchema) From a00609684e03a777befce4ff1213fe287d9cb4c8 Mon Sep 17 00:00:00 2001 From: Reem-15 Date: Mon, 20 Apr 2026 06:54:06 +0200 Subject: [PATCH 12/12] refactor: migrate middleware to proxy for Next 16 support --- proxy.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 proxy.ts diff --git a/proxy.ts b/proxy.ts new file mode 100644 index 0000000..2d6056b --- /dev/null +++ b/proxy.ts @@ -0,0 +1,24 @@ +import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' + +// Ensure we are using the most modern matcher patterns +const isPublicRoute = createRouteMatcher([ + '/sign-in(.*)', + '/sign-up(.*)', + '/', +]) + +export default clerkMiddleware(async (auth, request) => { + // Added a small "hardening" check: ensure we don't protect static assets + // if the matcher somehow misses them + if (!isPublicRoute(request)) { + await auth.protect() + } +}) + +export const config = { + matcher: [ + // Optimized matcher for Next 16 proxy layer + '/((?!_next|static|favicon.ico).*)', + '/(api|trpc)(.*)', + ], +} \ No newline at end of file