Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
4bf2dbf
fix: defer mongodb env validation
Deprasny Apr 18, 2026
786f196
fix: scope announcement mutations to current teacher
Deprasny Apr 18, 2026
f8497d9
fix: harden assignment mutations and errors
Deprasny Apr 18, 2026
e2e78e6
fix: require auth for profile reads
Deprasny Apr 18, 2026
6eece45
fix: validate and scope student writes
Deprasny Apr 18, 2026
bcdb097
fix: repair grade persistence and access control
Deprasny Apr 18, 2026
c6311ad
fix: initialize navbar theme without extra render
Deprasny Apr 18, 2026
b226041
chore: migrate middleware to proxy convention
Deprasny Apr 18, 2026
8914ff4
fix: cascade student deletions to related records
Deprasny Apr 18, 2026
0d8e23c
fix: correct dashboard totals and cgpa scale
Deprasny Apr 18, 2026
fc6af7b
fix: reject grade updates above max marks
Deprasny Apr 18, 2026
e82b944
fix: show empty state for filtered assignments
Deprasny Apr 18, 2026
9ea4918
fix: load all assignment pages for kanban board
Deprasny Apr 18, 2026
f530169
fix: load attendance rosters by exact class
Deprasny Apr 18, 2026
a7cdbb9
fix: load all students for grade workflows
Deprasny Apr 18, 2026
ab2575d
fix: rollback kanban moves on failed updates
Deprasny Apr 18, 2026
ff55180
fix: validate attendance students before writes
Deprasny Apr 18, 2026
8b88cc5
fix: validate grade student ownership
Deprasny Apr 18, 2026
055487b
fix: preserve local calendar dates in dashboards
Deprasny Apr 18, 2026
43c256a
fix: normalize attendance dates before persistence
Deprasny Apr 18, 2026
ee7249d
fix: load all student classes for filters
Deprasny Apr 18, 2026
7a21f8a
fix: enforce max marks on grade upserts
Deprasny Apr 18, 2026
7ee4890
fix: load all assignments for overview deadlines
Deprasny Apr 18, 2026
dfc35bb
fix: sort student grade history by repo terms
Deprasny Apr 18, 2026
4fccff5
fix: return 400 for invalid assignment updates
Deprasny Apr 18, 2026
6219828
fix: return 400 for invalid announcement updates
Deprasny Apr 18, 2026
b8eb247
fix: show student list load failures
Deprasny Apr 18, 2026
267235d
fix: rank grade comparisons by percentage
Deprasny Apr 18, 2026
52ddc21
fix: preserve overview term ordering for later terms
Deprasny Apr 18, 2026
2b81c1d
fix: sort overview grade distribution by scale
Deprasny Apr 18, 2026
c7c40a8
fix: require positive assignment max marks
Deprasny Apr 18, 2026
463ad4d
fix: reject unsupported assignment update fields
Deprasny Apr 18, 2026
65fc704
fix: reject unsupported announcement update fields
Deprasny Apr 18, 2026
4b13671
fix: reject unsupported student update fields
Deprasny Apr 18, 2026
d5774df
fix: reject empty grade update payloads
Deprasny Apr 18, 2026
fa49c8a
fix: return bad requests for invalid student updates
Deprasny Apr 18, 2026
d861074
fix: return bad requests for invalid grade updates
Deprasny Apr 18, 2026
2485abf
fix: stop requiring derived attendance fields
Deprasny Apr 18, 2026
d47ddd2
fix: stop requiring derived grade student names
Deprasny Apr 18, 2026
db2900a
fix: return bad requests for invalid assignment creates
Deprasny Apr 18, 2026
5924714
fix: reject empty attendance bulk payloads
Deprasny Apr 18, 2026
95010df
fix: surface assignment action errors in dashboard
Deprasny Apr 18, 2026
84bd692
fix: surface announcement action errors in dashboard
Deprasny Apr 18, 2026
5c776e3
fix: surface attendance save errors in dashboard
Deprasny Apr 18, 2026
ac290d6
fix: surface profile validation errors in dashboard
Deprasny Apr 18, 2026
eec1e9b
fix: surface student delete errors in dashboard
Deprasny Apr 18, 2026
923e838
fix: surface grade delete errors in dashboard
Deprasny Apr 18, 2026
dc33710
fix: surface grade validation errors in dashboard
Deprasny 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
20 changes: 17 additions & 3 deletions app/api/announcements/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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', 'audience', 'category', 'pinned']

export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
const { userId } = await auth()
Expand Down Expand Up @@ -35,8 +35,12 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string
}
}

if (Object.keys(sanitizedBody).length === 0) {
return NextResponse.json({ error: 'No valid announcement fields provided' }, { status: 400 })
}

const announcement = await Announcement.findOneAndUpdate(
{ _id: id },
{ _id: id, teacherId: userId },
{ $set: sanitizedBody },
{ new: true, runValidators: true, context: 'query' }
)
Expand All @@ -46,6 +50,16 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string
if (error instanceof Error) {
console.error('PUT /api/announcements/[id] error:', error.message)
}
if (error instanceof mongoose.Error.ValidationError) {
const firstError = Object.values(error.errors)[0]
return NextResponse.json(
{ error: firstError?.message ?? 'Invalid announcement update' },
{ status: 400 }
)
}
if (error instanceof mongoose.Error.CastError) {
return NextResponse.json({ error: error.message }, { status: 400 })
}
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
Expand All @@ -63,7 +77,7 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str
}

await connectDB()
const deleted = await Announcement.findOneAndDelete({ _id: id })
const deleted = await Announcement.findOneAndDelete({ _id: id, teacherId: userId })

if (!deleted) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
Expand Down
22 changes: 18 additions & 4 deletions app/api/assignments/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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', 'deadline', 'subject', 'class', 'status', 'kanbanStatus', 'maxMarks']

export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
const { userId } = await auth()
Expand Down Expand Up @@ -35,17 +35,31 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string
}
}

if (Object.keys(sanitizedBody).length === 0) {
return NextResponse.json({ error: 'No valid assignment fields provided' }, { status: 400 })
}

const assignment = await Assignment.findOneAndUpdate(
{ _id: id },
{ _id: id, teacherId: userId },
sanitizedBody,
{ new: true }
{ 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)
}
if (error instanceof mongoose.Error.ValidationError) {
const firstError = Object.values(error.errors)[0]
return NextResponse.json(
{ error: firstError?.message ?? 'Invalid assignment update' },
{ status: 400 }
)
}
if (error instanceof mongoose.Error.CastError) {
return NextResponse.json({ error: error.message }, { status: 400 })
}
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
Expand All @@ -63,7 +77,7 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str
}

await connectDB()
const deleted = await Assignment.findOneAndDelete({ _id: id })
const deleted = await Assignment.findOneAndDelete({ _id: id, teacherId: userId })

if (!deleted) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
Expand Down
13 changes: 12 additions & 1 deletion app/api/assignments/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { auth } from '@clerk/nextjs/server'
import { NextRequest, NextResponse } from 'next/server'
import mongoose from 'mongoose'
import { connectDB } from '@/lib/mongodb'
import { Assignment } from '@/models/Assignment'
import { z } from 'zod'
Expand Down Expand Up @@ -52,7 +53,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 })
}
}

Expand Down Expand Up @@ -80,6 +81,16 @@ export async function POST(req: NextRequest) {
if (dbError instanceof Error) {
console.error('Assignment.create error:', dbError.message)
}
if (dbError instanceof mongoose.Error.ValidationError) {
const firstError = Object.values(dbError.errors)[0]
return NextResponse.json(
{ error: firstError?.message ?? 'Invalid assignment payload' },
{ status: 400 }
)
}
if (dbError instanceof mongoose.Error.CastError) {
return NextResponse.json({ error: dbError.message }, { status: 400 })
}
return NextResponse.json({ error: 'Failed to create assignment' }, { status: 500 })
}
} catch (error) {
Expand Down
114 changes: 89 additions & 25 deletions app/api/attendance/route.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { auth } from '@clerk/nextjs/server'
import { NextRequest, NextResponse } from 'next/server'
import mongoose from 'mongoose'
import { connectDB } from '@/lib/mongodb'
import { Attendance } from '@/models/Attendance'
import { Student } from '@/models/Student'
import { normalizeDateOnly } from '@/lib/date'
import { z } from 'zod'

const AttendanceSchema = z.object({
studentId: z.string().min(1),
studentName: z.string().min(1),
class: z.string().min(1),
date: z.string().min(1),
status: z.enum(['present', 'absent', 'late']),
})

const BulkSchema = z.array(AttendanceSchema)
const BulkSchema = z.array(AttendanceSchema).min(1, 'At least one attendance record is required')

export async function GET(req: NextRequest) {
const { userId } = await auth()
Expand All @@ -30,21 +31,8 @@ export async function GET(req: NextRequest) {

const query: Record<string, unknown> = { teacherId: userId };

// Helper to validate and normalize date strings to YYYY-MM-DD format
const normalizeDate = (dateStr: string): string | null => {
try {
// Try to parse as ISO date (YYYY-MM-DD or full ISO 8601)
const d = new Date(dateStr);
if (isNaN(d.getTime())) return null;
// Return in YYYY-MM-DD format for MongoDB string comparison
return d.toISOString().split("T")[0];
} catch {
return null;
}
};

if (date) {
const normalized = normalizeDate(date);
const normalized = normalizeDateOnly(date);
if (normalized) {
query.date = normalized;
} else {
Expand All @@ -56,7 +44,7 @@ export async function GET(req: NextRequest) {
} else if (startDate || endDate) {
const dateRange: Record<string, string> = {};
if (startDate) {
const normalized = normalizeDate(startDate);
const normalized = normalizeDateOnly(startDate);
if (normalized) dateRange.$gte = normalized;
else
return NextResponse.json(
Expand All @@ -65,7 +53,7 @@ export async function GET(req: NextRequest) {
);
}
if (endDate) {
const normalized = normalizeDate(endDate);
const normalized = normalizeDateOnly(endDate);
if (normalized) dateRange.$lte = normalized;
else
return NextResponse.json(
Expand All @@ -76,7 +64,12 @@ export async function GET(req: NextRequest) {
query.date = dateRange;
}
if (cls) query.class = cls;
if (studentId) query.studentId = studentId;
if (studentId) {
if (!mongoose.Types.ObjectId.isValid(studentId)) {
return NextResponse.json({ error: 'Invalid studentId' }, { status: 400 })
}
query.studentId = studentId;
}

const records = await Attendance.find(query)
.sort({ date: -1, studentName: 1 })
Expand Down Expand Up @@ -123,20 +116,91 @@ export async function POST(req: NextRequest) {
{ status: 400 },
);

const records = isBulk
? (parsed.data as z.infer<typeof BulkSchema>)
: [parsed.data as z.infer<typeof AttendanceSchema>]

const normalizedRecords = records.map((record) => {
const normalizedDate = normalizeDateOnly(record.date)
if (!normalizedDate) {
return null
}

return {
...record,
date: normalizedDate,
}
})

if (normalizedRecords.some((record) => record === null)) {
return NextResponse.json(
{ error: 'Invalid date format. Use YYYY-MM-DD or ISO 8601' },
{ status: 400 },
)
}

const safeRecords = normalizedRecords as (z.infer<typeof AttendanceSchema> & { date: string })[]

const studentIds = [...new Set(safeRecords.map((record) => record.studentId))]
if (!studentIds.every((studentId) => mongoose.Types.ObjectId.isValid(studentId))) {
return NextResponse.json({ error: 'Invalid studentId' }, { status: 400 })
}

const students = await Student.find({
_id: { $in: studentIds },
teacherId: userId,
})
.select('_id name class')
.lean()

const studentMap = new Map(
students.map((student) => [String(student._id), student]),
)

if (studentMap.size !== studentIds.length) {
return NextResponse.json(
{ error: 'One or more students were not found for this teacher' },
{ status: 400 },
)
}

if (isBulk) {
const ops = (parsed.data as z.infer<typeof BulkSchema>).map((record) => ({
const ops = safeRecords.map((record) => {
const student = studentMap.get(record.studentId)!

return {
updateOne: {
filter: { teacherId: userId, studentId: record.studentId, date: record.date },
update: { $set: { ...record, teacherId: userId } },
update: {
$set: {
teacherId: userId,
studentId: record.studentId,
studentName: student.name,
class: student.class,
date: record.date,
status: record.status,
},
},
upsert: true,
},
}))
}})
await Attendance.bulkWrite(ops)
return NextResponse.json({ success: true, count: ops.length })
} else {
const recordData = safeRecords[0]
const student = studentMap.get(recordData.studentId)!
const record = await Attendance.findOneAndUpdate(
{ teacherId: userId, studentId: (parsed.data as z.infer<typeof AttendanceSchema>).studentId, date: (parsed.data as z.infer<typeof AttendanceSchema>).date },
{ $set: { ...(parsed.data as z.infer<typeof AttendanceSchema>), teacherId: userId } },
{ teacherId: userId, studentId: recordData.studentId, date: recordData.date },
{
$set: {
teacherId: userId,
studentId: recordData.studentId,
studentName: student.name,
class: student.class,
date: recordData.date,
status: recordData.status,
},
},
{ upsert: true, new: true }
)
return NextResponse.json(record, { status: 201 })
Expand Down
Loading