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()
}