Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 19 additions & 5 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,30 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string
return NextResponse.json({ error: 'Invalid JSON request body' }, { status: 400 })
}

if (!body || typeof body !== 'object' || Array.isArray(body)) {
return NextResponse.json({ error: 'Invalid JSON request body' }, { status: 400 })
}

const payload = body as Record<string, unknown>
const unknownFields = Object.keys(payload).filter((key) => !ALLOWED_FIELDS.includes(key))
if (unknownFields.length > 0) {
return NextResponse.json({ error: `Unknown field(s): ${unknownFields.join(', ')}` }, { status: 400 })
}

// Sanitize: only allow whitelisted fields
const sanitizedBody: Record<string, unknown> = {}
for (const key of ALLOWED_FIELDS) {
if (key in body) {
sanitizedBody[key] = body[key]
if (Object.prototype.hasOwnProperty.call(payload, key)) {
sanitizedBody[key] = payload[key]
}
}

if (Object.keys(sanitizedBody).length === 0) {
return NextResponse.json({ error: 'No updatable 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 @@ -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
30 changes: 22 additions & 8 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 All @@ -18,27 +18,41 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string
return NextResponse.json({ error: 'Invalid id' }, { status: 400 })
}

await connectDB()

let body
try {
body = await req.json()
} catch {
return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 })
}

if (!body || typeof body !== 'object' || Array.isArray(body)) {
return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 })
}

const payload = body as Record<string, unknown>
const unknownFields = Object.keys(payload).filter((key) => !ALLOWED_UPDATE_FIELDS.includes(key))
if (unknownFields.length > 0) {
return NextResponse.json({ error: `Unknown field(s): ${unknownFields.join(', ')}` }, { status: 400 })
}

// Sanitize: only allow whitelisted fields
const sanitizedBody: Record<string, unknown> = {}
for (const key of ALLOWED_UPDATE_FIELDS) {
if (key in body) {
sanitizedBody[key] = body[key]
if (Object.prototype.hasOwnProperty.call(payload, key)) {
sanitizedBody[key] = payload[key]
}
}

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

await connectDB()

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)
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
6 changes: 2 additions & 4 deletions app/api/assignments/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,8 @@ export async function GET(req: NextRequest) {

return NextResponse.json({ assignments, total, page, pages: Math.ceil(total / limit) })
} catch (error) {
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 })
console.error('GET /api/assignments error:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

Expand Down
77 changes: 70 additions & 7 deletions app/api/grades/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { NextRequest, NextResponse } from 'next/server'
import mongoose from 'mongoose'
import { connectDB } from '@/lib/mongodb'
import { Grade } from '@/models/Grade'
import { calculateLetterGrade } from '@/lib/grading'

const ALLOWED_UPDATE_FIELDS = ['marks', 'maxMarks', 'grade']
const ALLOWED_UPDATE_FIELDS = ['studentId', 'studentName', 'subject', 'term', 'marks', 'maxMarks']

export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
const { userId } = await auth()
Expand All @@ -25,26 +26,88 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string
return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 })
}

if (!body || typeof body !== 'object' || Array.isArray(body)) {
return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 })
}

const payload = body as Record<string, unknown>
const unknownFields = Object.keys(payload).filter((key) => !ALLOWED_UPDATE_FIELDS.includes(key))
if (unknownFields.length > 0) {
return NextResponse.json({ error: `Unknown field(s): ${unknownFields.join(', ')}` }, { status: 400 })
}

// Sanitize: only allow whitelisted fields
const sanitizedBody: Record<string, unknown> = {}
for (const key of ALLOWED_UPDATE_FIELDS) {
if (key in body) {
sanitizedBody[key] = body[key]
if (Object.prototype.hasOwnProperty.call(payload, key)) {
sanitizedBody[key] = payload[key]
}
}

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

if ('studentId' in sanitizedBody) {
const studentId = sanitizedBody.studentId
if (typeof studentId !== 'string' || !mongoose.Types.ObjectId.isValid(studentId)) {
return NextResponse.json({ error: 'studentId must be a valid id' }, { status: 400 })
}
}
Comment on lines +51 to +56
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:

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

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 6322


🏁 Script executed:

find . -type f -name "*.ts" -path "*/models/*" | head -20

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 212


🏁 Script executed:

cat -n ./models/Student.ts

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 1302


🏁 Script executed:

cat -n ./models/Grade.ts

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 3784


Verify student ownership before accepting studentId updates.

The studentId field is shape-validated (lines 51–56) but lacks ownership verification. A teacher can update any grade to reference a student they don't own, breaking data isolation. Before accepting the studentId value, query the Student model to confirm it belongs to the authenticated teacher, and derive studentName server-side to prevent spoofed associations.

Suggested direction
 import mongoose from 'mongoose'
 import { connectDB } from '@/lib/mongodb'
 import { Grade } from '@/models/Grade'
+import { Student } from '@/models/Student'
 import { calculateLetterGrade } from '@/lib/grading'
     await connectDB()
+
+    if (typeof sanitizedBody.studentId === 'string') {
+      const student = await Student.findOne({ _id: sanitizedBody.studentId, teacherId: userId }).select('name').lean()
+      if (!student) {
+        return NextResponse.json({ error: 'Student not found' }, { status: 404 })
+      }
+      sanitizedBody.studentName = student.name
+    }
 
     const existingGrade = await Grade.findOne({ _id: id, teacherId: userId })

Also applies to: 93-101

🤖 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 51 - 56, The studentId validation
currently only checks shape; extend it to verify ownership by querying the
Student model: after confirming sanitizedBody.studentId is a valid ObjectId,
load Student.findById(studentId) and ensure its teacherId matches the
authenticated teacher's id (derive that from the request/session used in this
route), returning a 403 if not owned; if owned, set/override
sanitizedBody.studentName server-side from the Student record to prevent
spoofing. Apply the same ownership+studentName derivation for the other update
path referenced (lines ~93-101) so all incoming studentId updates are validated
and studentName is authoritative.


if ('studentName' in sanitizedBody && typeof sanitizedBody.studentName !== 'string') {
return NextResponse.json({ error: 'studentName must be a string' }, { status: 400 })
}
if ('subject' in sanitizedBody && typeof sanitizedBody.subject !== 'string') {
return NextResponse.json({ error: 'subject must be a string' }, { status: 400 })
}
if ('term' in sanitizedBody && typeof sanitizedBody.term !== 'string') {
return NextResponse.json({ error: 'term must be a string' }, { status: 400 })
}

if ('marks' in sanitizedBody) {
const marks = sanitizedBody.marks
if (typeof marks !== 'number' || Number.isNaN(marks) || marks < 0) {
return NextResponse.json({ error: 'marks must be a number >= 0' }, { status: 400 })
}
}
if ('maxMarks' in sanitizedBody) {
const maxMarks = sanitizedBody.maxMarks
if (typeof maxMarks !== 'number' || Number.isNaN(maxMarks) || maxMarks < 1) {
return NextResponse.json({ error: 'maxMarks must be a number >= 1' }, { status: 400 })
}
}

await connectDB()

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

const nextMarks = typeof sanitizedBody.marks === 'number' ? sanitizedBody.marks : existingGrade.marks
const nextMaxMarks = typeof sanitizedBody.maxMarks === 'number' ? sanitizedBody.maxMarks : (existingGrade.maxMarks ?? 100)

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

const grade = await Grade.findOneAndUpdate(
{ _id: id },
sanitizedBody,
{ new: true }
{ _id: id, teacherId: userId },
{
...sanitizedBody,
maxMarks: nextMaxMarks,
grade: calculateLetterGrade(nextMarks, nextMaxMarks),
},
{ 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)
}
if ((error as { code?: number }).code === 11000) {
return NextResponse.json({ error: 'A grade for this student, subject, and term already exists' }, { status: 409 })
}
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
Expand All @@ -56,7 +119,7 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str
try {
const { id } = await ctx.params
await connectDB()
const deleted = await Grade.findOneAndDelete({ _id: id })
const deleted = await Grade.findOneAndDelete({ _id: id, teacherId: userId })
Comment on lines 120 to +122
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

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the DELETE route handler
cat -n app/api/grades/[id]/route.ts | head -150

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 6322


Add ObjectId validation to DELETE endpoint before querying Mongoose.

The PUT endpoint validates the id using mongoose.Types.ObjectId.isValid() at line 18 and returns 404 for invalid IDs, but the DELETE endpoint at line 122 passes the raw id directly to Mongoose. Invalid ObjectIds will trigger a cast error caught by the catch block, returning 500 instead of 404.

Proposed fix
   try {
     const { id } = await ctx.params
+    if (!mongoose.Types.ObjectId.isValid(id)) {
+      return NextResponse.json({ error: 'Grade not found' }, { status: 404 })
+    }
+
     await connectDB()
     const deleted = await Grade.findOneAndDelete({ _id: id, teacherId: userId })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { id } = await ctx.params
await connectDB()
const deleted = await Grade.findOneAndDelete({ _id: id })
const deleted = await Grade.findOneAndDelete({ _id: id, teacherId: userId })
const { id } = await ctx.params
if (!mongoose.Types.ObjectId.isValid(id)) {
return NextResponse.json({ error: 'Grade not found' }, { status: 404 })
}
await connectDB()
const deleted = await Grade.findOneAndDelete({ _id: id, teacherId: userId })
🤖 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 120 - 122, The DELETE handler
currently passes the raw id from ctx.params into Grade.findOneAndDelete which
allows invalid ObjectId strings to reach Mongoose and throw a cast error; before
calling connectDB() / Grade.findOneAndDelete, validate the id using
mongoose.Types.ObjectId.isValid(id) (same approach as the PUT handler) and
return a 404 for invalid ids. Update the code that reads const { id } = await
ctx.params to perform the isValid check and short-circuit with the same 404
response used by the PUT route, then only call connectDB() and
Grade.findOneAndDelete({ _id: id, teacherId: userId }) when the id is valid.


if (!deleted) {
return NextResponse.json({ error: 'Grade not found' }, { status: 404 })
Expand Down
30 changes: 9 additions & 21 deletions app/api/grades/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { auth } from '@clerk/nextjs/server'
import { NextRequest, NextResponse } from 'next/server'
import { connectDB } from '@/lib/mongodb'
import { Grade } from '@/models/Grade'
import { calculateLetterGrade } from '@/lib/grading'
import { z } from 'zod'

const GradeSchema = z.object({
Expand All @@ -19,17 +20,6 @@ const GradeSchema = z.object({
}
)

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 GET(req: NextRequest) {
const { userId } = await auth()
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
Expand All @@ -47,8 +37,8 @@ export async function GET(req: NextRequest) {
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)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

Expand All @@ -70,19 +60,17 @@ 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 }
{ $set: { ...data, maxMarks: max, term, teacherId: userId, grade: calculateLetterGrade(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)
}
return NextResponse.json({ error: error instanceof Error ? error.stack : 'Internal server error' }, { status: 500 })
console.error('POST /api/grades error:', error)
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() {
const { userId } = await auth()
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

try {
Expand Down
26 changes: 20 additions & 6 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 All @@ -25,19 +25,33 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string
return NextResponse.json({ error: 'Bad Request' }, { status: 400 })
}

if (!body || typeof body !== 'object' || Array.isArray(body)) {
return NextResponse.json({ error: 'Bad Request' }, { status: 400 })
}

const payload = body as Record<string, unknown>
const unknownFields = Object.keys(payload).filter((key) => !ALLOWED_UPDATE_FIELDS.includes(key))
if (unknownFields.length > 0) {
return NextResponse.json({ error: `Unknown field(s): ${unknownFields.join(', ')}` }, { status: 400 })
}

// Sanitize: only allow whitelisted fields
const sanitizedBody: Record<string, unknown> = {}
for (const key of ALLOWED_UPDATE_FIELDS) {
if (key in body) {
sanitizedBody[key] = body[key]
if (Object.prototype.hasOwnProperty.call(payload, key)) {
sanitizedBody[key] = payload[key]
}
}

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

await connectDB()
const student = await Student.findOneAndUpdate(
{ _id: id },
{ _id: id, teacherId: userId },
sanitizedBody,
{ new: true }
{ new: true, runValidators: true, context: 'query' }
)
if (!student) return NextResponse.json({ error: 'Not found' }, { status: 404 })
return NextResponse.json(student)
Expand Down Expand Up @@ -65,7 +79,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
Loading
Loading