diff --git a/app/(auth)/sign-in/[[...sign-in]]/page.tsx b/app/(auth)/sign-in/[[...sign-in]]/page.tsx index f491a20..1a579d1 100644 --- a/app/(auth)/sign-in/[[...sign-in]]/page.tsx +++ b/app/(auth)/sign-in/[[...sign-in]]/page.tsx @@ -8,7 +8,7 @@ export const metadata: Metadata = { export default function SignInPage() { return ( -
+

EduDesk

diff --git a/app/(auth)/sign-up/[[...sign-up]]/page.tsx b/app/(auth)/sign-up/[[...sign-up]]/page.tsx index de96598..f5bbe7b 100644 --- a/app/(auth)/sign-up/[[...sign-up]]/page.tsx +++ b/app/(auth)/sign-up/[[...sign-up]]/page.tsx @@ -2,7 +2,7 @@ import { SignUp } from '@clerk/nextjs' export default function SignUpPage() { return ( -
+

EduDesk

diff --git a/app/api/announcements/[id]/route.ts b/app/api/announcements/[id]/route.ts index 6c32f47..d0b7022 100644 --- a/app/api/announcements/[id]/route.ts +++ b/app/api/announcements/[id]/route.ts @@ -1,79 +1,115 @@ -import { auth } from '@clerk/nextjs/server' -import { NextRequest, NextResponse } from 'next/server' -import mongoose from 'mongoose' -import { connectDB } from '@/lib/mongodb' -import { Announcement } from '@/models/Announcement' +import { auth } from "@clerk/nextjs/server"; +import { NextRequest, NextResponse } from "next/server"; +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", + "body", + "audience", + "category", + "pinned", + "expiresAt", +]; -export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { - const { userId } = await auth() - if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) +export async function PUT( + req: NextRequest, + ctx: { params: Promise<{ id: string }> }, +) { + const { userId } = await auth(); + if (!userId) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); try { - const { id } = await ctx.params + const { id } = await ctx.params; // Validate ObjectId if (!mongoose.Types.ObjectId.isValid(id)) { - return NextResponse.json({ error: 'Invalid id' }, { status: 400 }) + return NextResponse.json({ error: "Invalid id" }, { status: 400 }); } - await connectDB() - - let body + await connectDB(); + + let body; try { - body = await req.json() + body = await req.json(); } catch { - return NextResponse.json({ error: 'Invalid JSON request body' }, { status: 400 }) + return NextResponse.json( + { error: "Invalid JSON request body" }, + { status: 400 }, + ); + } + if (body === null || typeof body !== "object" || Array.isArray(body)) { + // Valid JSON can still be a primitive/null/array, but this route requires an object payload. + return NextResponse.json( + { error: "Invalid JSON request body" }, + { status: 400 }, + ); } // Sanitize: only allow whitelisted fields - const sanitizedBody: Record = {} + const sanitizedBody: Record = {}; for (const key of ALLOWED_FIELDS) { if (key in body) { - sanitizedBody[key] = body[key] + sanitizedBody[key] = body[key]; } } const announcement = await Announcement.findOneAndUpdate( - { _id: id }, + { _id: id, teacherId: userId }, { $set: sanitizedBody }, - { new: true, runValidators: true, context: 'query' } - ) - if (!announcement) return NextResponse.json({ error: 'Not found' }, { status: 404 }) - return NextResponse.json(announcement) + { new: true, runValidators: true, context: "query" }, + ); + if (!announcement) + return NextResponse.json({ error: "Not found" }, { status: 404 }); + return NextResponse.json(announcement); } catch (error) { if (error instanceof Error) { - console.error('PUT /api/announcements/[id] error:', error.message) + console.error("PUT /api/announcements/[id] error:", error.message); } - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + 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 }) +export async function DELETE( + _req: NextRequest, + ctx: { params: Promise<{ id: string }> }, +) { + const { userId } = await auth(); + if (!userId) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); try { - const { id } = await ctx.params + const { id } = await ctx.params; // Validate ObjectId if (!mongoose.Types.ObjectId.isValid(id)) { - return NextResponse.json({ error: 'Invalid id' }, { status: 400 }) + return NextResponse.json({ error: "Invalid id" }, { status: 400 }); } - await connectDB() - const deleted = await Announcement.findOneAndDelete({ _id: id }) - + await connectDB(); + const deleted = await Announcement.findOneAndDelete({ + _id: id, + teacherId: userId, + }); + if (!deleted) { - return NextResponse.json({ error: 'Not found' }, { status: 404 }) + return NextResponse.json({ error: "Not found" }, { status: 404 }); } - - return NextResponse.json({ success: true }) + + return NextResponse.json({ success: true }); } catch (error) { if (error instanceof Error) { - console.error('DELETE /api/announcements/[id] error:', error.message) + console.error("DELETE /api/announcements/[id] error:", error.message); } - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } diff --git a/app/api/assignments/[id]/route.ts b/app/api/assignments/[id]/route.ts index 21fca1c..b6428fb 100644 --- a/app/api/assignments/[id]/route.ts +++ b/app/api/assignments/[id]/route.ts @@ -1,79 +1,116 @@ -import { auth } from '@clerk/nextjs/server' -import { NextRequest, NextResponse } from 'next/server' -import mongoose from 'mongoose' -import { connectDB } from '@/lib/mongodb' -import { Assignment } from '@/models/Assignment' +import { auth } from "@clerk/nextjs/server"; +import { NextRequest, NextResponse } from "next/server"; +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", + "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 }) +export async function PUT( + req: NextRequest, + ctx: { params: Promise<{ id: string }> }, +) { + const { userId } = await auth(); + if (!userId) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); try { - const { id } = await ctx.params + const { id } = await ctx.params; // Validate ObjectId if (!mongoose.Types.ObjectId.isValid(id)) { - return NextResponse.json({ error: 'Invalid id' }, { status: 400 }) + return NextResponse.json({ error: "Invalid id" }, { status: 400 }); } - await connectDB() - - let body + await connectDB(); + + let body; try { - body = await req.json() + body = await req.json(); } catch { - return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }) + return NextResponse.json( + { error: "Invalid JSON in request body" }, + { status: 400 }, + ); + } + if (body === null || typeof body !== "object" || Array.isArray(body)) { + return NextResponse.json( + { error: "Invalid JSON in request body" }, + { status: 400 }, + ); } // Sanitize: only allow whitelisted fields - const sanitizedBody: Record = {} + const sanitizedBody: Record = {}; for (const key of ALLOWED_UPDATE_FIELDS) { if (key in body) { - sanitizedBody[key] = body[key] + sanitizedBody[key] = body[key]; } } const assignment = await Assignment.findOneAndUpdate( - { _id: id }, + { _id: id, teacherId: userId }, sanitizedBody, - { new: true } - ) - if (!assignment) return NextResponse.json({ error: 'Not found' }, { status: 404 }) - return NextResponse.json(assignment) + { new: true, runValidators: true, context: "query" }, + ); + 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) + console.error("PUT /api/assignments/[id] error:", error.message); } - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + 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 }) +export async function DELETE( + _req: NextRequest, + ctx: { params: Promise<{ id: string }> }, +) { + const { userId } = await auth(); + if (!userId) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); try { - const { id } = await ctx.params + const { id } = await ctx.params; // Validate ObjectId if (!mongoose.Types.ObjectId.isValid(id)) { - return NextResponse.json({ error: 'Invalid id' }, { status: 400 }) + return NextResponse.json({ error: "Invalid id" }, { status: 400 }); } - await connectDB() - const deleted = await Assignment.findOneAndDelete({ _id: id }) - + await connectDB(); + const deleted = await Assignment.findOneAndDelete({ + _id: id, + teacherId: userId, + }); + if (!deleted) { - return NextResponse.json({ error: 'Not found' }, { status: 404 }) + return NextResponse.json({ error: "Not found" }, { status: 404 }); } - - return NextResponse.json({ success: true }) + + return NextResponse.json({ success: true }); } catch (error) { if (error instanceof Error) { - console.error('DELETE /api/assignments/[id] error:', error.message) + console.error("DELETE /api/assignments/[id] error:", error.message); } - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } 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..b6d52cb 100644 --- a/app/api/attendance/route.ts +++ b/app/api/attendance/route.ts @@ -2,10 +2,16 @@ import { auth } from '@clerk/nextjs/server' import { NextRequest, NextResponse } from 'next/server' import { connectDB } from '@/lib/mongodb' import { Attendance } from '@/models/Attendance' +import { Student } from '@/models/Student' import { z } from 'zod' +// Use strict 24-hex ObjectId checks to reject 12-byte non-hex strings. +const isCanonicalObjectId = (id: string) => /^[0-9a-fA-F]{24}$/.test(id) + const AttendanceSchema = z.object({ - studentId: z.string().min(1), + studentId: z.string().refine((id) => isCanonicalObjectId(id), { + message: 'Invalid studentId', + }), studentName: z.string().min(1), class: z.string().min(1), date: z.string().min(1), @@ -76,6 +82,9 @@ export async function GET(req: NextRequest) { query.date = dateRange; } if (cls) query.class = cls; + if (studentId && !isCanonicalObjectId(studentId)) { + return NextResponse.json({ error: 'Invalid studentId' }, { status: 400 }); + } if (studentId) query.studentId = studentId; const records = await Attendance.find(query) @@ -124,6 +133,21 @@ export async function POST(req: NextRequest) { ); if (isBulk) { + const studentIds = [ + ...new Set( + (parsed.data as z.infer).map( + (record) => record.studentId, + ), + ), + ]; + const ownedCount = await Student.countDocuments({ + _id: { $in: studentIds }, + teacherId: userId, + }); + if (ownedCount !== studentIds.length) { + return NextResponse.json({ error: 'Invalid studentId' }, { status: 400 }); + } + const ops = (parsed.data as z.infer).map((record) => ({ updateOne: { filter: { teacherId: userId, studentId: record.studentId, date: record.date }, @@ -134,9 +158,25 @@ export async function POST(req: NextRequest) { await Attendance.bulkWrite(ops) return NextResponse.json({ success: true, count: ops.length }) } else { + const parsedRecord = parsed.data as z.infer; + const student = await Student.exists({ + _id: parsedRecord.studentId, + teacherId: userId, + }); + if (!student) { + return NextResponse.json( + { error: 'Student not found or access denied' }, + { status: 403 }, + ); + } + 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: parsedRecord.studentId, + date: parsedRecord.date, + }, + { $set: { ...parsedRecord, 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..b4429ba 100644 --- a/app/api/grades/[id]/route.ts +++ b/app/api/grades/[id]/route.ts @@ -1,72 +1,104 @@ -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 { 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"; -const ALLOWED_UPDATE_FIELDS = ['marks', 'maxMarks', 'grade'] +const ALLOWED_UPDATE_FIELDS = ["marks", "maxMarks", "grade"]; -export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { - const { userId } = await auth() - if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) +export async function PUT( + req: NextRequest, + ctx: { params: Promise<{ id: string }> }, +) { + const { userId } = await auth(); + if (!userId) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); try { - const { id } = await ctx.params + const { id } = await ctx.params; // Validate ObjectId if (!mongoose.Types.ObjectId.isValid(id)) { - return NextResponse.json({ error: 'Not found' }, { status: 404 }) + return NextResponse.json({ error: "Not found" }, { status: 404 }); } - let body + let body; try { - body = await req.json() + body = await req.json(); } catch { - return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }) + return NextResponse.json( + { error: "Invalid JSON in request body" }, + { status: 400 }, + ); + } + if (body === null || typeof body !== "object" || Array.isArray(body)) { + return NextResponse.json( + { error: "Invalid JSON in request body" }, + { status: 400 }, + ); } // Sanitize: only allow whitelisted fields - const sanitizedBody: Record = {} + const sanitizedBody: Record = {}; for (const key of ALLOWED_UPDATE_FIELDS) { if (key in body) { - sanitizedBody[key] = body[key] + sanitizedBody[key] = body[key]; } } - await connectDB() + await connectDB(); const grade = await Grade.findOneAndUpdate( - { _id: id }, + { _id: id, teacherId: userId }, sanitizedBody, - { new: true } - ) - if (!grade) return NextResponse.json({ error: 'Not found' }, { status: 404 }) - return NextResponse.json(grade) + { new: true, runValidators: true, context: "query" }, + ); + if (!grade) + return NextResponse.json({ error: "Not found" }, { status: 404 }); + return NextResponse.json(grade); } catch (error) { if (error instanceof Error) { - console.error('PUT /api/grades/[id] error:', error.message) + console.error("PUT /api/grades/[id] error:", error.message); } - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + 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 }) +export async function DELETE( + _req: NextRequest, + ctx: { params: Promise<{ id: string }> }, +) { + const { userId } = await auth(); + if (!userId) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); try { - const { id } = await ctx.params - await connectDB() - const deleted = await Grade.findOneAndDelete({ _id: id }) - + const { id } = await ctx.params; + + if (!mongoose.Types.ObjectId.isValid(id)) { + return NextResponse.json({ error: "Invalid id" }, { status: 400 }); + } + + await connectDB(); + const deleted = await Grade.findOneAndDelete({ + _id: id, + teacherId: userId, + }); + if (!deleted) { - return NextResponse.json({ error: 'Grade not found' }, { status: 404 }) + return NextResponse.json({ error: "Grade not found" }, { status: 404 }); } - - return NextResponse.json({ success: true }) + + return NextResponse.json({ success: true }); } catch (error) { if (error instanceof Error) { - console.error('DELETE /api/grades/[id] error:', error.message) + console.error("DELETE /api/grades/[id] error:", error.message); } - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } diff --git a/app/api/grades/route.ts b/app/api/grades/route.ts index b9da63d..ba8ba32 100644 --- a/app/api/grades/route.ts +++ b/app/api/grades/route.ts @@ -1,88 +1,143 @@ -import { auth } from '@clerk/nextjs/server' -import { NextRequest, NextResponse } from 'next/server' -import { connectDB } from '@/lib/mongodb' -import { Grade } from '@/models/Grade' -import { z } from 'zod' +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"; -const GradeSchema = z.object({ - studentId: z.string().min(1), - studentName: z.string().min(1), - subject: z.string().min(1), - marks: z.number().min(0), - maxMarks: z.number().min(1).optional(), - term: z.string().optional(), -}).refine( - (data) => !data.maxMarks || data.marks <= data.maxMarks, - { - message: 'marks must be less than or equal to maxMarks', - path: ['marks'], - } -) +const GradeSchema = z + .object({ + studentId: z.string().refine((id) => mongoose.Types.ObjectId.isValid(id), { + message: "Invalid studentId", + }), + studentName: z.string().min(1), + subject: z.string().min(1), + marks: z.number().min(0), + maxMarks: z.number().min(1).optional(), + term: z.string().optional(), + }) + .refine((data) => !data.maxMarks || data.marks <= data.maxMarks, { + message: "marks must be less than or equal to maxMarks", + path: ["marks"], + }); 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' + 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 }) + 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') + 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: Record = { teacherId: userId }; + if (studentId && !mongoose.Types.ObjectId.isValid(studentId)) { + return NextResponse.json({ error: "Invalid studentId" }, { status: 400 }); + } + if (studentId) query.studentId = studentId; + if (subject) query.subject = subject; - const grades = await Grade.find(query).sort({ createdAt: -1 }).lean() - return NextResponse.json(grades) + 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 instanceof Error ? error.message : error, + ); + 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 }) + const { userId } = await auth(); + if (!userId) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); try { - await connectDB() - - let body + await connectDB(); + + let body; try { - body = await req.json() + body = await req.json(); } catch { - return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }) + return NextResponse.json( + { error: "Invalid JSON in request body" }, + { status: 400 }, + ); + } + + const parsed = GradeSchema.safeParse(body); + if (!parsed.success) + return NextResponse.json( + { error: parsed.error.flatten() }, + { status: 400 }, + ); + + const data = parsed.data; + const term = data.term ?? "Term 1"; + const gradeFilter = { + teacherId: userId, + studentId: data.studentId, + subject: data.subject, + term, + }; + let max: number; + if (data.maxMarks === undefined) { + const existing = await Grade.findOne(gradeFilter).select("maxMarks").lean(); + max = existing?.maxMarks ?? 100; + } else { + max = data.maxMarks; + } + if (!Number.isFinite(data.marks)) { + return NextResponse.json( + { error: "marks must be a valid number" }, + { status: 400 }, + ); + } + if (data.marks > max) { + return NextResponse.json( + { error: "marks must be less than or equal to maxMarks" }, + { status: 400 }, + ); } - - 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( - { 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 }) + const grade = await Grade.findOneAndUpdate( + gradeFilter, + { + $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) { if (error instanceof Error) { - console.error('POST /api/grades error:', error.message) + 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..ce45e53 100644 --- a/app/api/profile/route.ts +++ b/app/api/profile/route.ts @@ -1,114 +1,152 @@ -import { auth, currentUser } from '@clerk/nextjs/server' -import { NextRequest, NextResponse } from 'next/server' -import { connectDB } from '@/lib/mongodb' -import { Teacher } from '@/models/Teacher' +import { auth, currentUser } from "@clerk/nextjs/server"; +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 - } - if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) +export async function GET() { + const { userId } = await auth(); + if (!userId) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); try { - await connectDB() - let teacher = await Teacher.findOne({ clerkId: userId }).lean() - + await connectDB(); + let teacher = await Teacher.findOne({ clerkId: userId }).lean(); if (!teacher) { - const clerkUser = await currentUser() - const created = await Teacher.create({ - clerkId: userId, - name: clerkUser?.fullName ?? '', - email: clerkUser?.emailAddresses[0]?.emailAddress ?? '', - department: '', - subjects: [], - }) - teacher = created.toObject() + const clerkUser = await currentUser(); + teacher = await Teacher.findOneAndUpdate( + { clerkId: userId }, + { + $setOnInsert: { + clerkId: userId, + name: clerkUser?.fullName ?? "", + email: clerkUser?.emailAddresses?.[0]?.emailAddress ?? "", + department: "", + subjects: [], + }, + }, + { upsert: true, new: true, setDefaultsOnInsert: true, lean: true }, + ); } - return NextResponse.json(teacher) + return NextResponse.json(teacher); } catch (error) { - console.error('GET /api/profile error:', error instanceof Error ? error.message : error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + console.error( + "GET /api/profile error:", + error instanceof Error ? error.message : error, + ); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } export async function PUT(req: NextRequest) { - 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 { - await connectDB() - - let body + await connectDB(); + + let body; try { - body = await req.json() + body = await req.json(); } catch { - return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }) + return NextResponse.json( + { error: "Invalid JSON in request body" }, + { status: 400 }, + ); } - - const { name, department, subjects, phone, bio, academicHistory } = body + + const { name, department, subjects, phone, bio, academicHistory } = body; // Validate input - if (typeof name !== 'string' || !name.trim()) { - return NextResponse.json({ error: 'name must be a non-empty string' }, { status: 400 }) + if (typeof name !== "string" || !name.trim()) { + return NextResponse.json( + { error: "name must be a non-empty string" }, + { status: 400 }, + ); } - if (department !== undefined && typeof department !== 'string') { - return NextResponse.json({ error: 'department must be a string' }, { status: 400 }) + if (department !== undefined && typeof department !== "string") { + return NextResponse.json( + { error: "department must be a string" }, + { status: 400 }, + ); } - if (!Array.isArray(subjects) || !subjects.every((s) => typeof s === 'string')) { - return NextResponse.json({ error: 'subjects must be an array of strings' }, { status: 400 }) + if ( + !Array.isArray(subjects) || + !subjects.every((s) => typeof s === "string") + ) { + return NextResponse.json( + { error: "subjects must be an array of strings" }, + { status: 400 }, + ); } - if (phone !== undefined && typeof phone !== 'string') { - return NextResponse.json({ error: 'phone must be a string' }, { status: 400 }) + if (phone !== undefined && typeof phone !== "string") { + return NextResponse.json( + { error: "phone must be a string" }, + { status: 400 }, + ); } - if (bio !== undefined && typeof bio !== 'string') { - return NextResponse.json({ error: 'bio must be a string' }, { status: 400 }) + if (bio !== undefined && typeof bio !== "string") { + return NextResponse.json( + { error: "bio must be a string" }, + { status: 400 }, + ); } if (academicHistory !== undefined) { if ( !Array.isArray(academicHistory) || academicHistory.length > 20 || - !academicHistory.every( - (entry: unknown) => - entry !== null && - typeof entry === 'object' && - typeof (entry as Record).year === 'string' && - typeof (entry as Record).title === 'string', - ) + !academicHistory.every((entry: unknown) => { + if (entry === null || typeof entry !== "object") return false; + const record = entry as Record; + if (typeof record.year !== "string" || record.year.length > 10) + return false; + if (typeof record.title !== "string" || record.title.length > 200) + return false; + if ( + record.description !== undefined && + (typeof record.description !== "string" || + record.description.length > 2000) + ) { + return false; + } + return true; + }) ) { return NextResponse.json( - { error: 'academicHistory must be an array of objects with string year and title (max 20 items)' }, + { + error: + "academicHistory must be an array of objects with year/title strings (year<=10, title<=200, optional description<=2000, max 20 items)", + }, { status: 400 }, - ) + ); } } - const updatePayload: Record = { name, subjects } - if (department !== undefined) updatePayload.department = department - if (phone !== undefined) updatePayload.phone = phone - if (bio !== undefined) updatePayload.bio = bio - if (academicHistory !== undefined) updatePayload.academicHistory = academicHistory + const updatePayload: Record = { name, subjects }; + if (department !== undefined) updatePayload.department = department; + if (phone !== undefined) updatePayload.phone = phone; + if (bio !== undefined) updatePayload.bio = bio; + if (academicHistory !== undefined) + updatePayload.academicHistory = academicHistory; const teacher = await Teacher.findOneAndUpdate( { clerkId: userId }, { $set: updatePayload }, - { new: true } - ) - - if (!teacher) { - return NextResponse.json({ error: 'Teacher not found' }, { status: 404 }) - } + { new: true, upsert: true, setDefaultsOnInsert: true }, + ); - return NextResponse.json(teacher) + return NextResponse.json(teacher); } catch (error) { if (error instanceof Error) { - console.error('PUT /api/profile error:', error.message) + console.error("PUT /api/profile error:", error.message); } - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } diff --git a/app/api/students/[id]/route.ts b/app/api/students/[id]/route.ts index 2eaaf93..b056a0b 100644 --- a/app/api/students/[id]/route.ts +++ b/app/api/students/[id]/route.ts @@ -1,81 +1,120 @@ -import { auth } from '@clerk/nextjs/server' -import { NextRequest, NextResponse } from 'next/server' -import mongoose from 'mongoose' -import { connectDB } from '@/lib/mongodb' -import { Student } from '@/models/Student' +import { auth } from "@clerk/nextjs/server"; +import { NextRequest, NextResponse } from "next/server"; +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() - if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) +export async function PUT( + req: NextRequest, + ctx: { params: Promise<{ id: string }> }, +) { + const { userId } = await auth(); + if (!userId) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); try { - const { id } = await ctx.params + const { id } = await ctx.params; // Validate ObjectId if (!mongoose.Types.ObjectId.isValid(id)) { - return NextResponse.json({ error: 'Bad Request' }, { status: 400 }) + return NextResponse.json({ error: "Bad Request" }, { status: 400 }); } - let body + let body; try { - body = await req.json() + body = await req.json(); } catch { - return NextResponse.json({ error: 'Bad Request' }, { status: 400 }) + return NextResponse.json( + { error: "Invalid JSON in request body" }, + { status: 400 }, + ); + } + if (body === null || typeof body !== "object" || Array.isArray(body)) { + return NextResponse.json( + { error: "Invalid JSON in request body" }, + { status: 400 }, + ); } // Sanitize: only allow whitelisted fields - const sanitizedBody: Record = {} + const sanitizedBody: Record = {}; for (const key of ALLOWED_UPDATE_FIELDS) { if (key in body) { - sanitizedBody[key] = body[key] + sanitizedBody[key] = body[key]; } } - await connectDB() + await connectDB(); const student = await Student.findOneAndUpdate( - { _id: id }, + { _id: id, teacherId: userId }, sanitizedBody, - { new: true } - ) - if (!student) return NextResponse.json({ error: 'Not found' }, { status: 404 }) - return NextResponse.json(student) + { new: true, runValidators: true, context: "query" }, + ); + if (!student) + return NextResponse.json({ error: "Not found" }, { status: 404 }); + return NextResponse.json(student); } catch (error) { if (error instanceof Error) { - console.error('PUT /api/students/[id] error:', error.message) + 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: "A student with this roll number already exists" }, + { status: 409 }, + ); } - return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) + 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 }) +export async function DELETE( + _req: NextRequest, + ctx: { params: Promise<{ id: string }> }, +) { + const { userId } = await auth(); + if (!userId) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); try { - const { id } = await ctx.params + const { id } = await ctx.params; // Validate ObjectId if (!mongoose.Types.ObjectId.isValid(id)) { - return NextResponse.json({ error: 'Bad Request' }, { status: 400 }) + return NextResponse.json({ error: "Bad Request" }, { status: 400 }); } - await connectDB() - const deleted = await Student.findOneAndDelete({ _id: id }) - + await connectDB(); + const deleted = await Student.findOneAndDelete({ + _id: id, + teacherId: userId, + }); + if (!deleted) { - return NextResponse.json({ error: 'Student not found' }, { status: 404 }) + return NextResponse.json({ error: "Student not found" }, { status: 404 }); } - - return NextResponse.json({ success: true }) + + return NextResponse.json({ success: true }); } catch (error) { if (error instanceof Error) { - console.error('DELETE /api/students/[id] error:', error.message) + console.error("DELETE /api/students/[id] error:", error.message); } - return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 }, + ); } } diff --git a/app/api/students/route.ts b/app/api/students/route.ts index 8f3dcc2..f3d2bb1 100644 --- a/app/api/students/route.ts +++ b/app/api/students/route.ts @@ -1,32 +1,33 @@ -import { auth } from '@clerk/nextjs/server' -import { NextRequest, NextResponse } from 'next/server' -import { connectDB } from '@/lib/mongodb' -import { Student } from '@/models/Student' -import { z } from 'zod' +import { auth } from "@clerk/nextjs/server"; +import { NextRequest, NextResponse } from "next/server"; +import { connectDB } from "@/lib/mongodb"; +import { Student } from "@/models/Student"; +import { z } from "zod"; const StudentSchema = z.object({ name: z.string().min(1), rollNo: z.string().min(1), class: z.string().min(1), - email: z.string().email().optional().or(z.literal('')), + email: z.string().email().optional().or(z.literal("")), phone: z.string().optional(), address: z.string().optional(), parentName: z.string().optional(), parentPhone: z.string().optional(), -}) +}); function escapeRegex(str: string): string { - return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } export async function GET(req: NextRequest) { - 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 { await connectDB(); const { searchParams } = new URL(req.url); - const search = (searchParams.get("search") ?? "").replace(/\s+/g, ' '); + const search = (searchParams.get("search") ?? "").replace(/\s+/g, " "); const classFilter = searchParams.get("class") ?? ""; // Parse and validate pagination @@ -72,37 +73,52 @@ export async function GET(req: NextRequest) { }); } catch (error) { if (error instanceof Error) { - console.error('GET /api/students error:', error.message) + console.error("GET /api/students error:", error.message); } - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + 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 }) + const { userId } = await auth(); + if (!userId) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); try { - await connectDB() - - let body + await connectDB(); + + let body; try { - body = await req.json() + body = await req.json(); } catch { - return NextResponse.json({ error: 'Malformed JSON' }, { status: 400 }) + return NextResponse.json({ error: "Malformed JSON" }, { status: 400 }); } - - StudentSchema.safeParse(body) - const student = await Student.create({ ...(body as Record), teacherId: userId }) - return NextResponse.json(student, { status: 201 }) + const parsed = StudentSchema.safeParse(body); + if (!parsed.success) + return NextResponse.json( + { error: parsed.error.flatten() }, + { status: 400 }, + ); + + const student = await Student.create({ ...parsed.data, teacherId: userId }); + return NextResponse.json(student, { status: 201 }); } catch (error) { if (error instanceof Error) { - console.error('POST /api/students error:', error.message) + console.error("POST /api/students 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: "A student with this roll number already exists" }, + { status: 409 }, + ); } - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } diff --git a/app/dashboard/OverviewClient.tsx b/app/dashboard/OverviewClient.tsx index 169795e..b7e93b1 100644 --- a/app/dashboard/OverviewClient.tsx +++ b/app/dashboard/OverviewClient.tsx @@ -189,21 +189,42 @@ 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] = + if ( + !studentsRes.ok || + !assignmentsRes.ok || + !activeAssignmentsRes.ok || + !attendanceRes.ok || + !gradesRes.ok || + !announcementsRes.ok + ) { + throw new Error("Failed to load dashboard data"); + } + + const [ + students, + assignmentsData, + activeAssignmentsData, + attendance, + grades, + announcements, + ] = await Promise.all([ studentsRes.json(), assignmentsRes.json(), + activeAssignmentsRes.json(), attendanceRes.json(), gradesRes.json(), announcementsRes.json(), @@ -254,7 +275,7 @@ export function OverviewClient() { "B+": 8, B: 7, C: 6, - D: 4, + D: 5, F: 0, }; const termMap: Record = {}; @@ -316,13 +337,11 @@ 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: students.total ?? students.students?.length ?? 0, + totalAssignments: + assignmentsData.total ?? + (Array.isArray(assignments) ? assignments.length : 0), + pendingAssignments: activeAssignmentsData.total ?? 0, attendancePct, attendanceBreakdown: { present: totalPresent, diff --git a/app/dashboard/grades/GradesClient.tsx b/app/dashboard/grades/GradesClient.tsx index fdaa00b..adfe7fd 100644 --- a/app/dashboard/grades/GradesClient.tsx +++ b/app/dashboard/grades/GradesClient.tsx @@ -60,7 +60,13 @@ function pct(marks: number, max: number) { } const GRADE_POINT: Record = { - 'A+': 10, A: 9, 'B+': 8, B: 7, C: 6, D: 5, F: 0, + 'A+': 10, + A: 9, + 'B+': 8, + B: 7, + C: 6, + D: 5, + F: 0, } function cgpaFromGrades(gradeList: Grade[]) { diff --git a/app/dashboard/students/StudentsClient.tsx b/app/dashboard/students/StudentsClient.tsx index 82a1d96..28b90f4 100644 --- a/app/dashboard/students/StudentsClient.tsx +++ b/app/dashboard/students/StudentsClient.tsx @@ -211,7 +211,15 @@ function StudentDrawer({ }, [grades]); const cgpa = useMemo(() => { - const GRADE_POINT: Record = { 'A+': 10, A: 9, 'B+': 8, B: 7, C: 6, D: 5, F: 0 } + const GRADE_POINT: Record = { + 'A+': 10, + A: 9, + 'B+': 8, + B: 7, + C: 6, + D: 5, + F: 0, + } if (!grades.length) return null const sum = grades.reduce((s, g) => s + (GRADE_POINT[g.grade] ?? 0), 0) return (sum / grades.length).toFixed(2) diff --git a/lib/mongodb.ts b/lib/mongodb.ts index 01dddf9..1593889 100644 --- a/lib/mongodb.ts +++ b/lib/mongodb.ts @@ -1,40 +1,47 @@ -import mongoose from 'mongoose' +import mongoose from "mongoose"; -const MONGODB_URI = process.env.MONGODB_URI! +const MONGODB_URI = process.env.MONGODB_URI!; if (!MONGODB_URI) { - throw new Error('Please define the MONGODB_URI environment variable') + throw new Error("Please define the MONGODB_URI environment variable"); } interface MongooseCache { - conn: typeof mongoose | null - promise: Promise | null + conn: typeof mongoose | null; + promise: Promise | null; } declare global { - var mongooseCache: MongooseCache | undefined + var mongooseCache: MongooseCache | undefined; } -const cached: MongooseCache = global.mongooseCache ?? { conn: null, promise: null } -global.mongooseCache = cached +const cached: MongooseCache = global.mongooseCache ?? { + conn: null, + promise: null, +}; +global.mongooseCache = cached; export async function connectDB(): Promise { - if (cached.conn) return cached.conn + if (cached.conn) return cached.conn; if (!cached.promise) { cached.promise = mongoose - .connect(MONGODB_URI, { bufferCommands: false }) - .catch((error) => { - cached.promise = null - throw error + .connect(MONGODB_URI, { + bufferCommands: false, + tls: true, + serverSelectionTimeoutMS: 10000, }) + .catch((error) => { + cached.promise = null; + throw error; + }); } try { - cached.conn = await cached.promise - return cached.conn + cached.conn = await cached.promise; + return cached.conn; } catch (error) { - cached.promise = null - throw error + cached.promise = null; + throw error; } } diff --git a/models/Teacher.ts b/models/Teacher.ts index 837bbc8..69898ec 100644 --- a/models/Teacher.ts +++ b/models/Teacher.ts @@ -32,9 +32,9 @@ const TeacherSchema = new Schema( academicHistory: { type: [ { - year: { type: String, required: true }, - title: { type: String, required: true }, - description: { type: String }, + year: { type: String, required: true, maxlength: 10 }, + title: { type: String, required: true, maxlength: 200 }, + description: { type: String, maxlength: 2000 }, }, ], default: [], diff --git a/scripts/seed.mjs b/scripts/seed.mjs index 24d6dc3..4f7a59c 100644 --- a/scripts/seed.mjs +++ b/scripts/seed.mjs @@ -225,9 +225,45 @@ const ANNOUNCEMENTS_DATA = [ // ── Main ─────────────────────────────────────────────────────────────────────── async function main() { - console.log('🔗 Connecting to MongoDB…') - await mongoose.connect(MONGODB_URI, { bufferCommands: false }) - console.log('✅ Connected.\n') + const isAtlas = MONGODB_URI.includes('mongodb+srv://') + const hostMatch = MONGODB_URI.match(/@([^\/?]+)/) + const host = hostMatch ? hostMatch[1] : 'local or unknown host' + + console.log(`🔗 Connecting to MongoDB...`) + if (process.env.DEBUG_DB === 'true') { + console.log(`[DEBUG] Target Host: ${host}`) + console.log(`[DEBUG] Connecting with TLS and 10s timeout...`) + } + + try { + await mongoose.connect(MONGODB_URI, { + bufferCommands: false, + tls: true, + serverSelectionTimeoutMS: 10000, + }) + console.log('✅ Connected to MongoDB.\n') + } catch (err) { + console.error('\n❌ MongoDB Connection Failed!') + console.error(` Error: ${err.message}`) + + if (err.message.includes('tls') || err.message.includes('SSL') || err.message.includes('tlsv1 alert')) { + console.error('\n🔍 DIAGNOSTIC (TLS/SSL Issue):') + console.error(' - Check if your connection permits outbound TLS over port 27017.') + console.error(' - Could be an anti-virus or proxy intercepting the SSL handshake.') + } else if (err.message.includes('timeout') || err.name === 'MongooseServerSelectionError') { + console.error('\n🔍 DIAGNOSTIC (Timeout Issue):') + if (isAtlas) { + console.error(' - You are using MongoDB Atlas.') + console.error(' - LIKELY CAUSE: Your IP address is not whitelisted.') + console.error(' - FIX: Go to Atlas > Network Access > Add IP Address > "0.0.0.0/0" for testing.') + } else { + console.error(' - Check if the database is running and accessible at the network address.') + } + } else if (err.message.includes('bad auth')) { + console.error('\n🔍 DIAGNOSTIC (Auth Issue): Check if your password contains unencoded special characters.') + } + process.exit(1) + } try { // ── Resolve teacher ────────────────────────────────────────────────────────── @@ -350,16 +386,41 @@ async function main() { ) console.log(` ✔ ${announcements.length} announcements created.`) + // ── Verification ───────────────────────────────────────────────────────────── + console.log('\n🔎 Verifying inserted data…') + const verifyCounts = { + students: await Student.countDocuments({ teacherId }), + attendance: await Attendance.countDocuments({ teacherId }), + assignments: await Assignment.countDocuments({ teacherId }), + grades: await Grade.countDocuments({ teacherId }), + announcements: await Announcement.countDocuments({ teacherId }), + } + + if (process.env.DEBUG_DB === 'true') { + console.log('[DEBUG] DB Counts:', verifyCounts) + } + + if (!verifyCounts.students || !verifyCounts.attendance || !verifyCounts.assignments || !verifyCounts.grades || !verifyCounts.announcements) { + throw new Error(`Verification failed! Zeros detected in one or more collections.\nCounts: ${JSON.stringify(verifyCounts)}`) + } + console.log(' ✔ Data verified successfully.') + // ── Summary ─────────────────────────────────────────────────────────────────── console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') - console.log('🌱 Seed complete!') + console.log('🌱 Data seeded successfully!') console.log(` Teacher profile: updated`) - console.log(` Students: ${students.length}`) - console.log(` Attendance: ${attendance.length} (14 days)`) - console.log(` Assignments: ${assignments.length} (todo / in_progress / submitted)`) - console.log(` Grades: ${grades.length} (Term 1 & Term 2)`) - console.log(` Announcements: ${announcements.length} (academic / events / admin / general)`) + console.log(` Students: ${verifyCounts.students}`) + console.log(` Attendance: ${verifyCounts.attendance} (14 days)`) + console.log(` Assignments: ${verifyCounts.assignments} (todo / in_progress / submitted)`) + console.log(` Grades: ${verifyCounts.grades} (Term 1 & Term 2)`) + console.log(` Announcements: ${verifyCounts.announcements} (academic / events / admin / general)`) console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + + return + } catch (err) { + console.error('\n❌ Seed operations failed:') + console.error(` ${err.message ?? err}`) + throw err } finally { await mongoose.disconnect() }