Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
c97f95b
fix: use ObjectId for teacherId reference
theniteshdev Apr 18, 2026
a34e2a4
fix: restrict audience with enum values
theniteshdev Apr 18, 2026
dd9f090
feat: add compound indexes for teacherId and pinned queries to improv…
theniteshdev Apr 18, 2026
d419ed8
fix: replace teacherId string with ObjectId reference
theniteshdev Apr 18, 2026
93125d9
fix: add validation for title, description, subject, and class fields
theniteshdev Apr 18, 2026
07e9f16
fix: enforce deadline to be in the future
theniteshdev Apr 18, 2026
89e8c8d
fix: add constraints to maxMarks field
theniteshdev Apr 18, 2026
2f5d289
feat: add indexes for performance optimization
theniteshdev Apr 18, 2026
ed44128
fix: change attendance date type from string to Date
theniteshdev Apr 18, 2026
d4fe8e1
fix: strengthen unique index with teacherId
theniteshdev Apr 18, 2026
9e9c614
fix: replace teacherId with ObjectId reference
theniteshdev Apr 18, 2026
bababd3
fix: remove redundant studentName and class fields
theniteshdev Apr 18, 2026
dd0b2ec
fix: improve attendance schema consistency and data integrity
theniteshdev Apr 18, 2026
9186586
fix: replace teacherId with ObjectId reference
theniteshdev Apr 18, 2026
eb1dace
fix: remove redundant studentName
theniteshdev Apr 18, 2026
d12bc5e
fix: add enum validation for subject and term fields
theniteshdev Apr 18, 2026
2c48ed0
fix: define subject field type with enum validation
theniteshdev Apr 18, 2026
fcf5426
fix: add type and default value for term field
theniteshdev Apr 18, 2026
6e0aaba
fix: restore studentName field to prevent data inconsistency issues
theniteshdev Apr 18, 2026
fef2da7
fix: add trimming to required student fields to ensure data consistency
theniteshdev Apr 18, 2026
4d4e8da
fix: add email format validation to student schema
theniteshdev Apr 18, 2026
ecb9abe
fix: normalize optional student fields by adding trimming
theniteshdev Apr 18, 2026
be7c74f
fix: normalize rollNo to uppercase to enforce uniqueness consistency
theniteshdev Apr 18, 2026
56ad31b
fix: add index for teacherId and class to optimize student queries
theniteshdev Apr 18, 2026
b5aa1a0
fix: enforce minimum length validation on required student fields
theniteshdev Apr 18, 2026
f259b72
fix: add trimming to teacher fields for data consistency
theniteshdev Apr 18, 2026
22dab3e
fix: add email format validation to teacher schema
theniteshdev Apr 18, 2026
2f6f66d
fix: validate subjects array to prevent empty or invalid values
theniteshdev Apr 18, 2026
a9b06a0
fix: add trimming to academicHistory fields
theniteshdev Apr 18, 2026
ac53676
fix: normalize optional teacher fields by adding trimming
theniteshdev Apr 18, 2026
39fab09
fix: add indexes for department and subjects to improve query perform…
theniteshdev Apr 18, 2026
3601932
fix: add missing await for findOneAndUpdate in grades POST endpoint
theniteshdev Apr 18, 2026
e415ec1
fix: provide default value for maxMarks to prevent runtime errors
theniteshdev Apr 18, 2026
7706a23
fix: correct grade calculation boundary for A+ grade
theniteshdev Apr 18, 2026
0eef839
fix: validate studentId as a valid MongoDB ObjectId
theniteshdev Apr 18, 2026
9511c9a
fix: convert studentId to ObjectId in GET query for correct filtering
theniteshdev Apr 18, 2026
dab7aea
fix: prevent exposure of internal error stack in API response
theniteshdev Apr 18, 2026
cf32081
fix: normalize input fields by trimming strings before database opera…
theniteshdev Apr 18, 2026
d0a8b67
fix: handle null currentUser to prevent creating invalid teacher records
theniteshdev Apr 18, 2026
cf3581b
fix: strengthen input validation by trimming string fields
theniteshdev Apr 18, 2026
20e910e
fix: validate subjects array to prevent empty or invalid values
theniteshdev Apr 18, 2026
fd57eab
fix: normalize input data before updating teacher profile
theniteshdev Apr 18, 2026
78912f0
fix: enable schema validation in findOneAndUpdate operation
theniteshdev Apr 18, 2026
4998e95
fix: trim optional fields before updating teacher profile
theniteshdev Apr 18, 2026
c7ce708
fix: resolve teacher ObjectId from clerkId for correct grade queries …
theniteshdev Apr 18, 2026
5299665
fix: Enhance subjects validation in profile route
theniteshdev Apr 18, 2026
7ffd902
fix: Change teacherId type to ObjectId in Announcement model and remo…
theniteshdev Apr 18, 2026
d03cbf2
Refactor: teacherId type and update schema fields
theniteshdev Apr 18, 2026
dc4009c
refactor: Attendance model to use ObjectId and remove fields
theniteshdev Apr 18, 2026
cad1c1d
remove: studentName from IAttendance interface
theniteshdev Apr 18, 2026
cecc46f
Change teacherId type from string to ObjectId
theniteshdev Apr 18, 2026
077de99
fix: email validation and update unique index
theniteshdev Apr 18, 2026
7a799a3
Refactor teacher lookup into a separate function
theniteshdev Apr 18, 2026
9038946
Enhance studentId validation in grades route
theniteshdev Apr 18, 2026
7095a26
fix: validation for subjects array in route.ts
theniteshdev Apr 18, 2026
6978b98
Update route.ts
theniteshdev Apr 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions app/api/assignments/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
})
Expand Down
46 changes: 34 additions & 12 deletions app/api/grades/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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'
Expand All @@ -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<string, unknown> = { teacherId: userId }
if (studentId) query.studentId = studentId
const query: Record<string, unknown> = { 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 })
}
}

Expand All @@ -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 {
Expand All @@ -69,20 +87,24 @@ 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 })
} catch (error) {
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 })
}
}
26 changes: 19 additions & 7 deletions app/api/profile/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? '',
Expand Down Expand Up @@ -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 })
}
Expand All @@ -77,7 +86,7 @@ export async function PUT(req: NextRequest) {
(entry: unknown) =>
entry !== null &&
typeof entry === 'object' &&
typeof (entry as Record<string, unknown>).year === 'string' &&
typeof (entry as Record<string, unknown>).year === 'string' &&
typeof (entry as Record<string, unknown>).title === 'string',
)
) {
Expand All @@ -88,17 +97,20 @@ export async function PUT(req: NextRequest) {
}
}

const updatePayload: Record<string, unknown> = { name, subjects }
const updatePayload: Record<string, unknown> = {
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 })
Expand Down
50 changes: 32 additions & 18 deletions models/Announcement.ts
Original file line number Diff line number Diff line change
@@ -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<IAnnouncement>(
{
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<IAnnouncement>('Announcement', AnnouncementSchema)
models.Announcement ??
model<IAnnouncement>("Announcement", AnnouncementSchema);
39 changes: 30 additions & 9 deletions models/Assignment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17,17 +17,38 @@ export interface IAssignment {

const AssignmentSchema = new Schema<IAssignment>(
{
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 },
Comment thread
theniteshdev marked this conversation as resolved.
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 },
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
{ timestamps: true }
)

export const Assignment = models.Assignment ?? model<IAssignment>('Assignment', AssignmentSchema)
// ✅ Correct indexes
AssignmentSchema.index({ teacherId: 1, class: 1 })
AssignmentSchema.index({ status: 1 })
AssignmentSchema.index({ deadline: 1 })

export const Assignment =
models.Assignment ?? model<IAssignment>('Assignment', AssignmentSchema)
23 changes: 17 additions & 6 deletions models/Attendance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -14,16 +14,27 @@ export interface IAttendance {

const AttendanceSchema = new Schema<IAttendance>(
{
teacherId: { type: String, required: true, index: true },
teacherId: { type: Schema.Types.ObjectId, ref: 'Teacher', required: true, index: true },
Comment thread
theniteshdev marked this conversation as resolved.
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 })
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// NEW: optimize status filtering (present/absent/late)
AttendanceSchema.index({ teacherId: 1, status: 1 })

export const Attendance = models.Attendance ?? model<IAttendance>('Attendance', AttendanceSchema)
12 changes: 8 additions & 4 deletions models/Grade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -16,18 +16,22 @@ export interface IGrade {

const GradeSchema = new Schema<IGrade>(
{
teacherId: { type: String, required: true, index: true },
teacherId: { type: Schema.Types.ObjectId, ref: "Teacher", required: true, index: true },
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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,
min: 0,
},
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 },
);
Expand Down
Loading