Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e61d2a6
fix: await grade upsert before returning JSON response
hdRutvik114 Apr 18, 2026
c424525
fix: default max marks when omitted on grade POST
hdRutvik114 Apr 18, 2026
66c3847
fix: stop returning error stacks from grades API
hdRutvik114 Apr 18, 2026
2499be1
fix: stop returning error stack from assignments GET
hdRutvik114 Apr 18, 2026
4359359
fix: drop non-schema grade field from student update whitelist
hdRutvik114 Apr 18, 2026
147e951
fix: scope student PUT and DELETE by authenticated teacher
hdRutvik114 Apr 18, 2026
8203822
fix: enforce Zod validation on student POST body
hdRutvik114 Apr 18, 2026
81a0c65
fix: scope assignment PUT and DELETE by authenticated teacher
hdRutvik114 Apr 18, 2026
79c2997
fix: scope announcement PUT and DELETE by teacher
hdRutvik114 Apr 18, 2026
2c1ac34
fix: map announcement body to content and trim allow list
hdRutvik114 Apr 18, 2026
0e7a273
fix: load profile only for signed-in user session
hdRutvik114 Apr 18, 2026
1803a9f
fix: scope grade PUT and DELETE by authenticated teacher
hdRutvik114 Apr 18, 2026
bac0c62
fix: validate ObjectId before grade DELETE query
hdRutvik114 Apr 18, 2026
35d9d1b
fix: recompute letter grade and validate marks on grade PUT
hdRutvik114 Apr 18, 2026
bd4f8ec
fix: scope attendance uniqueness per teacher and student
hdRutvik114 Apr 18, 2026
2916629
fix: parse YYYY-MM-DD attendance dates without UTC shift
hdRutvik114 Apr 18, 2026
76d57ed
fix: load class roster via class query not search
hdRutvik114 Apr 18, 2026
6fdc891
fix: handle non-OK responses when loading students list
hdRutvik114 Apr 18, 2026
9fe5c6b
fix: sort drawer recent grades using term order list
hdRutvik114 Apr 18, 2026
921a9ac
fix: check dashboard API responses before parsing bodies
hdRutvik114 Apr 18, 2026
4c2d878
fix: align overview D grade point with grades page
hdRutvik114 Apr 18, 2026
952b002
fix: guard invalid assignment deadlines in days-until badge
hdRutvik114 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
14 changes: 10 additions & 4 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 All @@ -27,16 +27,22 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string
return NextResponse.json({ error: 'Invalid JSON request body' }, { status: 400 })
}

// Sanitize: only allow whitelisted fields
// Sanitize: only allow whitelisted fields (map legacy `body` → `content`)
const sanitizedBody: Record<string, unknown> = {}
for (const key of ALLOWED_FIELDS) {
if (key in body) {
sanitizedBody[key] = body[key]
}
}
if (
typeof body.body === 'string' &&
sanitizedBody.content === undefined
) {
sanitizedBody.content = body.body
}

const announcement = await Announcement.findOneAndUpdate(
{ _id: id },
{ _id: id, teacherId: userId },
Comment on lines +30 to +45
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Harden against non-object JSON bodies.

req.json() happily resolves for null, arrays, strings, etc. On those inputs, key in body (line 33) throws TypeError for primitives and silently iterates array indices, and body.body (line 38) works unexpectedly. It's caught by the outer try and surfaces as a 500 instead of a clean 400.

🛠️ Suggested fix
     let body
     try {
       body = await req.json()
     } catch {
       return NextResponse.json({ error: 'Invalid JSON request body' }, { status: 400 })
     }
+    if (body === null || typeof body !== 'object' || Array.isArray(body)) {
+      return NextResponse.json({ error: 'Request body must be a JSON object' }, { status: 400 })
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/announcements/`[id]/route.ts around lines 30 - 45, The handler must
validate that the parsed request body is a plain object before iterating or
accessing properties: before the ALLOWED_FIELDS loop and the legacy map (the
code that writes to sanitizedBody and checks body.body), add a guard that checks
typeof body === 'object' && body !== null && !Array.isArray(body); if that check
fails, return a 400 error (bad request) instead of proceeding. Update the logic
that builds sanitizedBody (the for...of over ALLOWED_FIELDS and the legacy
body→content mapping) to run only after this guard so
Announcement.findOneAndUpdate is never called with a malformed body.

{ $set: sanitizedBody },
{ new: true, runValidators: true, context: 'query' }
)
Expand All @@ -63,7 +69,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
4 changes: 2 additions & 2 deletions app/api/assignments/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string
}

const assignment = await Assignment.findOneAndUpdate(
{ _id: id },
{ _id: id, teacherId: userId },
sanitizedBody,
{ new: true }
)
Expand All @@ -63,7 +63,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
2 changes: 1 addition & 1 deletion app/api/assignments/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
}

Expand Down
22 changes: 18 additions & 4 deletions app/api/attendance/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,27 @@ 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
// Normalize calendar dates to YYYY-MM-DD without UTC day-shift (date-only input)
const normalizeDate = (dateStr: string): string | null => {
const trimmed = dateStr.trim();
const ymd = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
if (ymd) {
const y = Number(ymd[1]);
const m = Number(ymd[2]);
const day = Number(ymd[3]);
const local = new Date(y, m - 1, day);
if (
local.getFullYear() === y &&
local.getMonth() === m - 1 &&
local.getDate() === day
) {
return `${ymd[1]}-${ymd[2]}-${ymd[3]}`;
}
return null;
}
try {
// Try to parse as ISO date (YYYY-MM-DD or full ISO 8601)
const d = new Date(dateStr);
const d = new Date(trimmed);
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;
Expand Down
48 changes: 43 additions & 5 deletions app/api/grades/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,18 @@ 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']

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'
}

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

await connectDB()
const existing = await Grade.findOne({ _id: id, teacherId: userId }).lean()
if (!existing) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}

const nextMarks =
typeof sanitizedBody.marks === 'number' ? sanitizedBody.marks : existing.marks
const nextMax =
typeof sanitizedBody.maxMarks === 'number'
? sanitizedBody.maxMarks
: existing.maxMarks

if (nextMarks > nextMax) {
return NextResponse.json(
{ error: 'marks must be less than or equal to maxMarks' },
{ status: 400 },
)
}

const updatePayload: Record<string, unknown> = { ...sanitizedBody }
updatePayload.grade = calcGrade(nextMarks, nextMax)

const grade = await Grade.findOneAndUpdate(
{ _id: id },
sanitizedBody,
{ new: true }
{ _id: id, teacherId: userId },
{ $set: updatePayload },
{ new: true },
Comment on lines +53 to +73
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect grade update paths for explicit Mongoose update validation.
rg -n -C3 'Grade\.findOneAndUpdate|runValidators|marks must be less than or equal to maxMarks' --type=ts

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 3404


🏁 Script executed:

cat -n app/api/grades/[id]/route.ts | head -100

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 3928


🏁 Script executed:

# Check the Zod schema definition for grades
rg -n -B5 -A10 'marks|maxMarks' app/api/grades/\[id\]/route.ts | head -60

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 1548


🏁 Script executed:

# Check the complete Mongoose Grade schema and validators
cat -n models/Grade.ts

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 2735


🏁 Script executed:

# Check if there are other PUT/PATCH handlers for grades that might have similar issues
rg -n 'Grade\.findOneAndUpdate|runValidators' app/api/grades/ --type=ts

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 238


Add explicit validation for marks and maxMarks before spreading them into the update payload.

Currently, non-number values pass through sanitizedBody into updatePayload (line 67) without type validation, even though nextMarks/nextMax skip them. Additionally, the findOneAndUpdate call lacks runValidators: true, so schema constraints (min: 0 for marks, min: 1 for maxMarks) are bypassed. The pre-hook only validates marks > maxMarks but ignores negative or zero values, risking invalid grades and division-by-zero in calcGrade.

Proposed fix
+    if (
+      'marks' in sanitizedBody &&
+      (typeof sanitizedBody.marks !== 'number' || sanitizedBody.marks < 0)
+    ) {
+      return NextResponse.json(
+        { error: 'marks must be a non-negative number' },
+        { status: 400 },
+      )
+    }
+
+    if (
+      'maxMarks' in sanitizedBody &&
+      (typeof sanitizedBody.maxMarks !== 'number' || sanitizedBody.maxMarks < 1)
+    ) {
+      return NextResponse.json(
+        { error: 'maxMarks must be a positive number' },
+        { status: 400 },
+      )
+    }
+
     const nextMarks =
       typeof sanitizedBody.marks === 'number' ? sanitizedBody.marks : existing.marks
     const nextMax =
       typeof sanitizedBody.maxMarks === 'number'
         ? sanitizedBody.maxMarks
         : existing.maxMarks
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/grades/`[id]/route.ts around lines 53 - 73, Sanitize and enforce
numeric/range validation for marks and maxMarks before building updatePayload:
ensure sanitizedBody.marks and sanitizedBody.maxMarks are numbers (use
nextMarks/nextMax logic) and within schema bounds (marks >= 0, maxMarks >= 1,
and maxMarks !== 0) — drop any non-number or out-of-range values from
updatePayload instead of blindly spreading sanitizedBody; compute
updatePayload.grade with calcGrade(nextMarks, nextMax) only when both numbers
are valid; and call Grade.findOneAndUpdate with the option { new: true,
runValidators: true } so Mongoose schema validators (min: 0 / min: 1) are
enforced.

)
if (!grade) return NextResponse.json({ error: 'Not found' }, { status: 404 })
return NextResponse.json(grade)
Expand All @@ -55,8 +88,13 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str

try {
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 })
const deleted = await Grade.findOneAndDelete({ _id: id, teacherId: userId })

if (!deleted) {
return NextResponse.json({ error: 'Grade not found' }, { status: 404 })
Expand Down
8 changes: 4 additions & 4 deletions app/api/grades/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export async function GET(req: NextRequest) {
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 @@ -70,10 +70,10 @@ export async function POST(req: NextRequest) {
if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 })

const data = parsed.data
const max = data.maxMarks!
const max = data.maxMarks ?? 100
const term = data.term ?? 'Term 1'

const grade = Grade.findOneAndUpdate(
const grade = await 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 }
Expand All @@ -83,6 +83,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 })
}
}
11 changes: 2 additions & 9 deletions app/api/profile/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,8 @@ 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
}
export async function GET(_req: NextRequest) {
const { userId } = await auth()
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

try {
Expand Down
6 changes: 3 additions & 3 deletions app/api/students/[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 { 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()
Expand Down Expand Up @@ -35,7 +35,7 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string

await connectDB()
const student = await Student.findOneAndUpdate(
{ _id: id },
{ _id: id, teacherId: userId },
sanitizedBody,
{ new: true }
)
Expand Down Expand Up @@ -65,7 +65,7 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str
}

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

if (!deleted) {
return NextResponse.json({ error: 'Student not found' }, { status: 404 })
Expand Down
7 changes: 5 additions & 2 deletions app/api/students/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,12 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: 'Malformed JSON' }, { status: 400 })
}

StudentSchema.safeParse(body)
const parsed = StudentSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 })
}

const student = await Student.create({ ...(body as Record<string, unknown>), teacherId: userId })
const student = await Student.create({ ...parsed.data, teacherId: userId })
return NextResponse.json(student, { status: 201 })
} catch (error) {
if (error instanceof Error) {
Expand Down
58 changes: 48 additions & 10 deletions app/dashboard/OverviewClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,52 @@ export function OverviewClient() {
fetch("/api/announcements?limit=5"),
]);

const [students, assignmentsData, attendance, grades, announcements] =
await Promise.all([
studentsRes.json(),
assignmentsRes.json(),
attendanceRes.json(),
gradesRes.json(),
announcementsRes.json(),
]);
const students = await studentsRes.json();
if (!studentsRes.ok) {
throw new Error(
typeof students.error === "string"
? students.error
: `Students failed (${studentsRes.status})`,
);
}
const assignmentsData = await assignmentsRes.json();
if (!assignmentsRes.ok) {
throw new Error(
typeof assignmentsData.error === "string"
? assignmentsData.error
: `Assignments failed (${assignmentsRes.status})`,
);
}
const attendanceRaw = await attendanceRes.json();
if (!attendanceRes.ok) {
throw new Error(
typeof attendanceRaw.error === "string"
? attendanceRaw.error
: `Attendance failed (${attendanceRes.status})`,
);
}
const gradesRaw = await gradesRes.json();
if (!gradesRes.ok) {
throw new Error(
typeof gradesRaw.error === "string"
? gradesRaw.error
: `Grades failed (${gradesRes.status})`,
);
}
const announcementsRaw = await announcementsRes.json();
if (!announcementsRes.ok) {
throw new Error(
typeof announcementsRaw.error === "string"
? announcementsRaw.error
: `Announcements failed (${announcementsRes.status})`,
);
}

const attendance = Array.isArray(attendanceRaw) ? attendanceRaw : [];
const grades = Array.isArray(gradesRaw) ? gradesRaw : [];
const announcements = Array.isArray(announcementsRaw)
? announcementsRaw
: [];

const assignments = assignmentsData.assignments ?? assignmentsData;

Expand Down Expand Up @@ -254,7 +292,7 @@ export function OverviewClient() {
"B+": 8,
B: 7,
C: 6,
D: 4,
D: 5,
F: 0,
};
const termMap: Record<string, number[]> = {};
Expand Down Expand Up @@ -316,7 +354,7 @@ export function OverviewClient() {
.slice(0, 5);

setStats({
totalStudents: students.students?.length ?? 0,
totalStudents: students.total ?? students.students?.length ?? 0,
totalAssignments: Array.isArray(assignments)
? assignments.length
: (assignments.length ?? 0),
Expand Down
5 changes: 4 additions & 1 deletion app/dashboard/assignments/AssignmentsClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,14 @@ const COLUMNS: {
];

function daysUntil(deadline: string) {
return Math.ceil((new Date(deadline).getTime() - Date.now()) / (1000 * 60 * 60 * 24))
const t = new Date(deadline).getTime();
if (!Number.isFinite(t)) return NaN;
return Math.ceil((t - Date.now()) / (1000 * 60 * 60 * 24));
}

function DeadlineBadge({ deadline }: { deadline: string }) {
const days = daysUntil(deadline);
if (!Number.isFinite(days)) return <Badge variant="default">Invalid date</Badge>;
if (days < 0) return <Badge variant="danger">Overdue</Badge>;
if (days <= 2) return <Badge variant="danger">{days}d left</Badge>;
if (days <= 7) return <Badge variant="warning">{days}d left</Badge>;
Expand Down
2 changes: 1 addition & 1 deletion app/dashboard/attendance/AttendanceClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function AttendanceClient() {
}
try {
const res = await fetch(
`/api/students?search=${encodeURIComponent(selectedClass)}&limit=100`,
`/api/students?class=${encodeURIComponent(selectedClass)}&limit=100`,
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
Expand Down
Loading
Loading