diff --git a/app/api/assignments/route.ts b/app/api/assignments/route.ts index 19021d8..fed2776 100644 --- a/app/api/assignments/route.ts +++ b/app/api/assignments/route.ts @@ -4,13 +4,16 @@ import { connectDB } from '@/lib/mongodb' import { Assignment } from '@/models/Assignment' import { z } from 'zod' +const SUBJECTS = ['Mathematics', 'Data Structures', 'Operating Systems', 'DBMS', 'Computer Networks'] as const; +const CLASSES = ['CS-A', 'CS-B'] as const; + const AssignmentSchema = z.object({ - title: z.string().min(1), - description: z.string().optional(), - subject: z.string().min(1), - class: z.string().min(1), - deadline: z.string().min(1), - maxMarks: z.number().min(1).optional(), + title: z.string().trim().min(3), + description: z.string().max(1000).optional(), + subject: z.enum(SUBJECTS), + class: z.enum(CLASSES), + deadline: z.string().refine((value) => new Date(value) > new Date(), 'Deadline must be in the future'), + maxMarks: z.number().min(1).max(1000).optional(), status: z.enum(['active', 'closed']).optional(), kanbanStatus: z.enum(['todo', 'in_progress', 'submitted']).optional(), }) diff --git a/app/api/grades/route.ts b/app/api/grades/route.ts index b9da63d..124e205 100644 --- a/app/api/grades/route.ts +++ b/app/api/grades/route.ts @@ -3,9 +3,15 @@ import { NextRequest, NextResponse } from 'next/server' import { connectDB } from '@/lib/mongodb' import { Grade } from '@/models/Grade' import { z } from 'zod' +import mongoose from 'mongoose' +import { Teacher } from '@/models/Teacher' +async function findTeacherByClerkId(userId: string){ + return Teacher.findOne({ clerkId: userId }).select('_id').lean() +}; + const GradeSchema = z.object({ - studentId: z.string().min(1), + studentId: z.string().regex(/^[0-9a-fA-F]{24}$/, 'Invalid studentId'), studentName: z.string().min(1), subject: z.string().min(1), marks: z.number().min(0), @@ -21,7 +27,7 @@ const GradeSchema = z.object({ function calcGrade(marks: number, max: number): string { const pct = (marks / max) * 100 - if (pct > 90) return 'A+' + if (pct >= 90) return 'A+' if (pct >= 80) return 'A' if (pct >= 70) return 'B+' if (pct >= 60) return 'B' @@ -35,20 +41,29 @@ export async function GET(req: NextRequest) { if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) try { - await connectDB() + await connectDB(); + + const teacher = await findTeacherByClerkId(userId); + if (!teacher) return NextResponse.json({ error: 'Teacher not found' }, { status: 404 }); + 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 + const query: Record = { teacherId: teacher._id }; + + if (studentId) { + if (!mongoose.Types.ObjectId.isValid(studentId)) return NextResponse.json({ error: 'Invalid studentId' }, { status: 400 }); + query.studentId = new mongoose.Types.ObjectId(studentId) + }; + if (subject) query.subject = subject const grades = await Grade.find(query).sort({ createdAt: -1 }).lean() return NextResponse.json(grades) } catch (error) { console.error('GET /api/grades error:', error instanceof Error ? error.message : error) - return NextResponse.json({ error: error instanceof Error ? error.stack : 'Internal server error' }, { status: 500 }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } @@ -58,6 +73,9 @@ export async function POST(req: NextRequest) { try { await connectDB() + + const teacher = await findTeacherByClerkId(userId); + if (!teacher) return NextResponse.json({ error: 'Teacher not found' }, { status: 404 }); let body try { @@ -69,13 +87,17 @@ export async function POST(req: NextRequest) { 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 data = { + ...parsed.data, + studentName: parsed.data.studentName.trim(), + subject: parsed.data.subject.trim(), + }; + const max = data.maxMarks ?? 100; 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) } }, + const grade = await Grade.findOneAndUpdate( + { teacherId: teacher._id, studentId: new mongoose.Types.ObjectId(data.studentId), subject: data.subject, term }, + { $set: { ...data, term, teacherId: teacher._id, grade: calcGrade(data.marks, max) } }, { upsert: true, new: true } ) return NextResponse.json(grade, { status: 201 }) @@ -83,6 +105,6 @@ export async function POST(req: NextRequest) { if (error instanceof Error) { console.error('POST /api/grades error:', error.message) } - return NextResponse.json({ error: error instanceof Error ? error.stack : 'Internal server error' }, { status: 500 }) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } diff --git a/app/api/profile/route.ts b/app/api/profile/route.ts index a3d98bf..bbd7c51 100644 --- a/app/api/profile/route.ts +++ b/app/api/profile/route.ts @@ -20,6 +20,9 @@ export async function GET(req: NextRequest) { if (!teacher) { const clerkUser = await currentUser() + if (!clerkUser) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } const created = await Teacher.create({ clerkId: userId, name: clerkUser?.fullName ?? '', @@ -60,9 +63,15 @@ export async function PUT(req: NextRequest) { 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')) { + + if ( + !Array.isArray(subjects) || + subjects.length === 0 || + !subjects.every((s: unknown) => typeof s === 'string' && s.trim().length > 0) + ) { 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 }) } @@ -77,7 +86,7 @@ export async function PUT(req: NextRequest) { (entry: unknown) => entry !== null && typeof entry === 'object' && - typeof (entry as Record).year === 'string' && + typeof (entry as Record).year === 'string' && typeof (entry as Record).title === 'string', ) ) { @@ -88,17 +97,20 @@ export async function PUT(req: NextRequest) { } } - const updatePayload: Record = { name, subjects } + const updatePayload: Record = { + name: name.trim(), + subjects: subjects.map((s) => s.trim()), + }; if (department !== undefined) updatePayload.department = department - if (phone !== undefined) updatePayload.phone = phone - if (bio !== undefined) updatePayload.bio = bio + if (phone !== undefined) updatePayload.phone = phone.trim() + if (bio !== undefined) updatePayload.bio = bio.trim() if (academicHistory !== undefined) updatePayload.academicHistory = academicHistory const teacher = await Teacher.findOneAndUpdate( { clerkId: userId }, { $set: updatePayload }, - { new: true } - ) + { new: true, runValidators: true }, + ); if (!teacher) { return NextResponse.json({ error: 'Teacher not found' }, { status: 404 }) diff --git a/models/Announcement.ts b/models/Announcement.ts index cf5260c..502988d 100644 --- a/models/Announcement.ts +++ b/models/Announcement.ts @@ -1,28 +1,42 @@ -import mongoose, { Schema, model, models } from 'mongoose' +import mongoose, { Schema, model, models } from "mongoose"; export interface IAnnouncement { - _id: mongoose.Types.ObjectId - teacherId: string - title: string - content: string - audience: string - category: 'academic' | 'events' | 'admin' | 'general' - pinned: boolean - createdAt: Date - updatedAt: Date + _id: mongoose.Types.ObjectId; + teacherId: mongoose.Types.ObjectId; + title: string; + content: string; + audience: string; + category: "academic" | "events" | "admin" | "general"; + pinned: boolean; + createdAt: Date; + updatedAt: Date; } const AnnouncementSchema = new Schema( { - teacherId: { type: String, required: true, index: true }, - title: { type: String, required: true }, - content: { type: String, required: true }, - audience: { type: String, default: 'All' }, - category: { type: String, enum: ['academic', 'events', 'admin', 'general'], default: 'general' }, + teacherId: { + type: Schema.Types.ObjectId, + ref: "Teacher", + required: true, + index: true, + }, + title: { type: String, required: true, trim: true, minlength: 3 }, + content: { type: String, required: true, trim: true, minlength: 5 }, + audience: { type: String, default: "All", trim: true }, + category: { + type: String, + enum: ["academic", "events", "admin", "general"], + default: "general", + }, pinned: { type: Boolean, default: false }, }, - { timestamps: true } -) + { timestamps: true }, +); + +AnnouncementSchema.index({ teacherId: 1, pinned: 1 }); + +AnnouncementSchema.index({ teacherId: 1, createdAt: -1 }); export const Announcement = - models.Announcement ?? model('Announcement', AnnouncementSchema) + models.Announcement ?? + model("Announcement", AnnouncementSchema); diff --git a/models/Assignment.ts b/models/Assignment.ts index 874f1fc..98bdf2e 100644 --- a/models/Assignment.ts +++ b/models/Assignment.ts @@ -2,7 +2,7 @@ import mongoose, { Schema, model, models } from 'mongoose' export interface IAssignment { _id: mongoose.Types.ObjectId - teacherId: string + teacherId: mongoose.Types.ObjectId title: string description: string subject: string @@ -17,17 +17,38 @@ export interface IAssignment { const AssignmentSchema = new Schema( { - teacherId: { type: String, required: true, index: true }, - title: { type: String, required: true }, - description: { type: String, default: '' }, - subject: { type: String, required: true }, - class: { type: String, required: true }, - deadline: { type: Date, required: true }, + teacherId: { type: Schema.Types.ObjectId, ref: 'Teacher', required: true, index: true }, + title: { type: String, required: true, trim: true, minlength: 3 }, + description: { type: String, default: '', maxlength: 1000 }, + subject: { + type: String, + enum: ['Mathematics', 'Data Structures', 'Operating Systems', 'DBMS', 'Computer Networks'], + required: true + }, + class: { + type: String, + enum: ['CS-A', 'CS-B'], + required: true + }, + deadline: { + type: Date, + required: true, + validate: { + validator: (value: Date) => value > new Date(), + message: 'Deadline must be in the future' + } + }, status: { type: String, enum: ['active', 'closed'], default: 'active' }, kanbanStatus: { type: String, enum: ['todo', 'in_progress', 'submitted'], default: 'todo' }, - maxMarks: { type: Number, default: 100 }, + maxMarks: { type: Number, default: 100, min: 1, max: 1000 }, }, { timestamps: true } ) -export const Assignment = models.Assignment ?? model('Assignment', AssignmentSchema) +// ✅ Correct indexes +AssignmentSchema.index({ teacherId: 1, class: 1 }) +AssignmentSchema.index({ status: 1 }) +AssignmentSchema.index({ deadline: 1 }) + +export const Assignment = + models.Assignment ?? model('Assignment', AssignmentSchema) diff --git a/models/Attendance.ts b/models/Attendance.ts index 3f05fb1..b01b1bc 100644 --- a/models/Attendance.ts +++ b/models/Attendance.ts @@ -2,9 +2,9 @@ import mongoose, { Schema, model, models } from 'mongoose' export interface IAttendance { _id: mongoose.Types.ObjectId - teacherId: string + teacherId: mongoose.Types.ObjectId studentId: mongoose.Types.ObjectId - studentName: string + // removed the studentName class: string date: string status: 'present' | 'absent' | 'late' @@ -14,16 +14,27 @@ export interface IAttendance { const AttendanceSchema = new Schema( { - teacherId: { type: String, required: true, index: true }, + teacherId: { type: Schema.Types.ObjectId, ref: 'Teacher', required: true, index: true }, studentId: { type: Schema.Types.ObjectId, ref: 'Student', required: true }, - studentName: { type: String, required: true }, - class: { type: String, required: true }, - date: { type: String, required: true }, + // revome the studentName because If student name changes → data becomes inconsistent + // remove because Data inconsistency + class: { type: String, required: true, trim: true }, + date: { type: Date, required: true }, status: { type: String, enum: ['present', 'absent', 'late'], required: true }, }, { timestamps: true } ) +// existing uniqueness constraint AttendanceSchema.index({ studentId: 1, date: 1 }, { unique: true }) +// NEW: optimize teacher dashboard queries +AttendanceSchema.index({ teacherId: 1, date: 1 }) + +// NEW: optimize class-wise filtering +AttendanceSchema.index({ teacherId: 1, class: 1 }) + +// NEW: optimize status filtering (present/absent/late) +AttendanceSchema.index({ teacherId: 1, status: 1 }) + export const Attendance = models.Attendance ?? model('Attendance', AttendanceSchema) diff --git a/models/Grade.ts b/models/Grade.ts index 311b757..e32cd68 100644 --- a/models/Grade.ts +++ b/models/Grade.ts @@ -2,7 +2,7 @@ import mongoose, { Schema, model, models } from 'mongoose' export interface IGrade { _id: mongoose.Types.ObjectId - teacherId: string + teacherId: mongoose.Types.ObjectId studentId: mongoose.Types.ObjectId studentName: string subject: string @@ -16,10 +16,12 @@ export interface IGrade { const GradeSchema = new Schema( { - teacherId: { type: String, required: true, index: true }, + teacherId: { type: Schema.Types.ObjectId, ref: "Teacher", required: true, index: true }, studentId: { type: Schema.Types.ObjectId, ref: "Student", required: true }, studentName: { type: String, required: true }, - subject: { type: String, required: true }, + subject: { type: String, + enum: ['Mathematics', 'Data Structures', 'Operating Systems', 'DBMS', 'Computer Networks'], + required: true,}, marks: { type: Number, required: true, @@ -27,7 +29,9 @@ const GradeSchema = new Schema( }, maxMarks: { type: Number, default: 100, min: 1 }, grade: { type: String, default: "" }, - term: { type: String, default: "Term 1" }, + term: { type: String, + enum: ["Term 1", "Term 2"], + default: "Term 1", }, }, { timestamps: true }, ); diff --git a/models/Student.ts b/models/Student.ts index 85ed899..2453869 100644 --- a/models/Student.ts +++ b/models/Student.ts @@ -17,19 +17,24 @@ export interface IStudent { const StudentSchema = new Schema( { - teacherId: { type: String, required: true, index: true }, - name: { type: String, required: true }, - rollNo: { type: String, required: true }, - class: { type: String, required: true }, - email: { type: String, default: '' }, - phone: { type: String, default: '' }, - address: { type: String, default: '' }, - parentName: { type: String, default: '' }, - parentPhone: { type: String, default: '' }, + teacherId: { type: String, required: true, index: true, minlength: 1 }, + name: { type: String, required: true, trim: true, minlength: 1 }, + rollNo: { type: String, required: true, trim: true, uppercase: true,minlength: 1 }, + class: { type: String, required: true, trim: true,minlength: 1 }, + email: { type: String, trim: true, + validate:{ + validator: (value?: string) => !value || /^\S+@\S+\.\S+$/.test(value), + message: 'Invalid email format', + } + }, + phone: { type: String, default: '', trim: true }, + address: { type: String, default: '', trim: true }, + parentName: { type: String, default: '', trim: true }, + parentPhone: { type: String, default: '', trim: true }, }, { timestamps: true } ) -StudentSchema.index({ teacherId: 1, rollNo: 1 }, { unique: true }) +StudentSchema.index({ teacherId: 1, rollNo: 1 }, { unique: true }); export const Student = models.Student ?? model('Student', StudentSchema) diff --git a/models/Teacher.ts b/models/Teacher.ts index 837bbc8..d7c64ef 100644 --- a/models/Teacher.ts +++ b/models/Teacher.ts @@ -23,18 +23,26 @@ export interface ITeacher { const TeacherSchema = new Schema( { clerkId: { type: String, required: true, unique: true }, - name: { type: String, required: true }, - email: { type: String, required: true }, - department: { type: String, default: "" }, - subjects: { type: [String], default: [] }, - phone: { type: String, default: "" }, - bio: { type: String, default: "" }, + name: { type: String, required: true, trim: true }, + email: { type: String, + required: true, + trim: true, + match: [/^\S+@\S+\.\S+$/, 'Invalid email format'], }, + department: { type: String, default: "", trim: true }, + subjects: { type: [String], + default: [], + validate: { + validator: (arr: string[]) => arr.every(s => typeof s === 'string' && s.trim().length > 0), + message: 'Subjects must be non-empty strings', + }, }, + phone: { type: String, default: "", trim: true }, + bio: { type: String, default: "", trim: true }, academicHistory: { type: [ { - year: { type: String, required: true }, - title: { type: String, required: true }, - description: { type: String }, + year: { type: String, required: true, trim: true }, + title: { type: String, required: true, trim: true }, + description: { type: String, trim: true }, }, ], default: [], @@ -43,4 +51,7 @@ const TeacherSchema = new Schema( { timestamps: true }, ); +TeacherSchema.index({ department: 1 }); +TeacherSchema.index({ subjects: 1 }); + export const Teacher = models.Teacher ?? model('Teacher', TeacherSchema)