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/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/attendance/route.ts b/app/api/attendance/route.ts index 14b6c4d..e714bde 100644 --- a/app/api/attendance/route.ts +++ b/app/api/attendance/route.ts @@ -1,5 +1,6 @@ import { auth } from '@clerk/nextjs/server' import { NextRequest, NextResponse } from 'next/server' +import mongoose from 'mongoose' import { connectDB } from '@/lib/mongodb' import { Attendance } from '@/models/Attendance' import { z } from 'zod' @@ -76,7 +77,12 @@ export async function GET(req: NextRequest) { query.date = dateRange; } if (cls) query.class = cls; - if (studentId) query.studentId = studentId; + if (studentId) { + if (!mongoose.Types.ObjectId.isValid(studentId)) { + return NextResponse.json({ error: "Invalid studentId" }, { status: 400 }); + } + query.studentId = studentId; + } const records = await Attendance.find(query) .sort({ date: -1, studentName: 1 }) @@ -124,7 +130,15 @@ export async function POST(req: NextRequest) { ); if (isBulk) { - const ops = (parsed.data as z.infer).map((record) => ({ + const data = parsed.data as z.infer; + // Validate all studentIds + for (const record of data) { + if (!mongoose.Types.ObjectId.isValid(record.studentId)) { + return NextResponse.json({ error: `Invalid studentId: ${record.studentId}` }, { status: 400 }); + } + } + + const ops = data.map((record) => ({ updateOne: { filter: { teacherId: userId, studentId: record.studentId, date: record.date }, update: { $set: { ...record, teacherId: userId } }, @@ -134,9 +148,14 @@ export async function POST(req: NextRequest) { await Attendance.bulkWrite(ops) return NextResponse.json({ success: true, count: ops.length }) } else { + const data = parsed.data as z.infer; + if (!mongoose.Types.ObjectId.isValid(data.studentId)) { + return NextResponse.json({ error: "Invalid studentId" }, { status: 400 }); + } + 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 }) diff --git a/app/api/grades/[id]/route.ts b/app/api/grades/[id]/route.ts index 0141f63..64c8da8 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 } ) @@ -55,8 +55,14 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str try { const { id } = await ctx.params + + // Validate ObjectId + 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 }) diff --git a/app/api/grades/route.ts b/app/api/grades/route.ts index b9da63d..f60a233 100644 --- a/app/api/grades/route.ts +++ b/app/api/grades/route.ts @@ -1,5 +1,6 @@ import { auth } from '@clerk/nextjs/server' import { NextRequest, NextResponse } from 'next/server' +import mongoose from 'mongoose' import { connectDB } from '@/lib/mongodb' import { Grade } from '@/models/Grade' import { z } from 'zod' @@ -20,8 +21,9 @@ const GradeSchema = z.object({ ) function calcGrade(marks: number, max: number): string { + if (max <= 0) return 'F' 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' @@ -41,14 +43,19 @@ export async function GET(req: NextRequest) { const subject = searchParams.get('subject') const query: Record = { teacherId: userId } - if (studentId) query.studentId = studentId + if (studentId) { + if (!mongoose.Types.ObjectId.isValid(studentId)) { + return NextResponse.json({ error: "Invalid studentId" }, { status: 400 }); + } + query.studentId = studentId + } if (subject) query.subject = 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 }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } @@ -70,10 +77,10 @@ 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 } @@ -83,6 +90,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 }) } } 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 { diff --git a/app/api/students/[id]/route.ts b/app/api/students/[id]/route.ts index 2eaaf93..cb7bd14 100644 --- a/app/api/students/[id]/route.ts +++ b/app/api/students/[id]/route.ts @@ -3,6 +3,7 @@ import { NextRequest, NextResponse } from 'next/server' import mongoose from 'mongoose' import { connectDB } from '@/lib/mongodb' import { Student } from '@/models/Student' +import { z } from 'zod' const ALLOWED_UPDATE_FIELDS = ['name', 'email', 'grade', 'rollNo', 'class', 'phone', 'address', 'parentName', 'parentPhone'] @@ -25,19 +26,25 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string 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] - } - } + const StudentUpdateSchema = z.object({ + name: z.string().min(1).optional(), + rollNo: z.string().min(1).optional(), + class: z.string().min(1).optional(), + email: z.string().email().optional().or(z.literal('')), + phone: z.string().optional(), + address: z.string().optional(), + parentName: z.string().optional(), + parentPhone: z.string().optional(), + }) + + const parsed = StudentUpdateSchema.safeParse(body) + if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }) await connectDB() const student = await Student.findOneAndUpdate( - { _id: id }, - sanitizedBody, - { new: true } + { _id: id, teacherId: userId }, + { $set: parsed.data }, + { new: true, runValidators: true } ) if (!student) return NextResponse.json({ error: 'Not found' }, { status: 404 }) return NextResponse.json(student) @@ -65,7 +72,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 }) diff --git a/app/api/students/route.ts b/app/api/students/route.ts index 8f3dcc2..93d77d4 100644 --- a/app/api/students/route.ts +++ b/app/api/students/route.ts @@ -92,9 +92,10 @@ 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) { diff --git a/app/dashboard/OverviewClient.tsx b/app/dashboard/OverviewClient.tsx index 169795e..5c96d47 100644 --- a/app/dashboard/OverviewClient.tsx +++ b/app/dashboard/OverviewClient.tsx @@ -200,6 +200,11 @@ export function OverviewClient() { fetch("/api/announcements?limit=5"), ]); + // Check that all responses are OK before processing + for (const res of [studentsRes, assignmentsRes, attendanceRes, gradesRes, announcementsRes]) { + if (!res.ok) throw new Error(`API error: ${res.url} returned ${res.status}`); + } + const [students, assignmentsData, attendance, grades, announcements] = await Promise.all([ studentsRes.json(), @@ -254,7 +259,7 @@ export function OverviewClient() { "B+": 8, B: 7, C: 6, - D: 4, + D: 5, F: 0, }; const termMap: Record = {}; @@ -316,7 +321,7 @@ export function OverviewClient() { .slice(0, 5); setStats({ - totalStudents: students.students?.length ?? 0, + totalStudents: students.total ?? students.students?.length ?? 0, totalAssignments: Array.isArray(assignments) ? assignments.length : (assignments.length ?? 0), diff --git a/app/dashboard/announcements/AnnouncementsClient.tsx b/app/dashboard/announcements/AnnouncementsClient.tsx index 0de73bb..6c85b43 100644 --- a/app/dashboard/announcements/AnnouncementsClient.tsx +++ b/app/dashboard/announcements/AnnouncementsClient.tsx @@ -46,7 +46,9 @@ const CATEGORY_BADGE: Record; if (update && typeof update === "object") { - const marks = update.marks; - const maxMarks = update.maxMarks; + // Support both direct updates and $set operator + const source = (update.$set && typeof update.$set === "object" ? update.$set : update) as Record; + const marks = source.marks; + const maxMarks = source.maxMarks; if ( marks !== undefined && typeof marks === "number" && maxMarks !== undefined && typeof maxMarks === "number" && @@ -56,8 +58,10 @@ GradeSchema.pre("findOneAndUpdate", function () { GradeSchema.pre("updateOne", function () { const update = this.getUpdate() as Record; if (update && typeof update === "object") { - const marks = update.marks; - const maxMarks = update.maxMarks; + // Support both direct updates and $set operator + const source = (update.$set && typeof update.$set === "object" ? update.$set : update) as Record; + const marks = source.marks; + const maxMarks = source.maxMarks; if ( marks !== undefined && typeof marks === "number" && maxMarks !== undefined && typeof maxMarks === "number" &&