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
4 changes: 2 additions & 2 deletions app/api/announcements/[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 announcement = await Announcement.findOneAndUpdate(
{ _id: id },
{ _id: id , teacherId: userId}, // ← add this to ensure users can only update their own announcements
{ $set: sanitizedBody },
{ new: true, runValidators: true, context: 'query' }
)
Expand All @@ -63,7 +63,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 }) // ← add this to ensure users can only delete their own announcements

if (!deleted) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
Expand Down
5 changes: 3 additions & 2 deletions app/api/assignments/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string
}

const assignment = await Assignment.findOneAndUpdate(
{ _id: id },
// FIXED
{ _id: id, teacherId: userId },
sanitizedBody,
{ new: true }
)
Comment on lines 38 to 43
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

🌐 Web query:

Does Mongoose run schema validators by default for findOneAndUpdate, or is runValidators: true required?

💡 Result:

No, Mongoose does not run schema validators by default for findOneAndUpdate. The runValidators option must be set to true to enable update validators.

Citations:


Add runValidators: true to the findOneAndUpdate call.

Mongoose does not run schema validators by default for findOneAndUpdate. Without the runValidators option, a teacher can submit allowed fields with invalid values (such as an invalid status enum or negative maxMarks) and persist them to the database. The ownership filter is correct, but data validation is still needed.

🛡️ Proposed fix
     const assignment = await Assignment.findOneAndUpdate(
-// FIXED
-{ _id: id, teacherId: userId },
-      sanitizedBody,
-      { new: true }
+      { _id: id, teacherId: userId },
+      { $set: sanitizedBody },
+      { new: true, runValidators: true, context: 'query' }
     )
📝 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 assignment = await Assignment.findOneAndUpdate(
{ _id: id },
// FIXED
{ _id: id, teacherId: userId },
sanitizedBody,
{ new: true }
)
const assignment = await Assignment.findOneAndUpdate(
{ _id: id, teacherId: userId },
{ $set: sanitizedBody },
{ new: true, runValidators: true, context: 'query' }
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/assignments/`[id]/route.ts around lines 38 - 43, The findOneAndUpdate
call on Assignment is missing the runValidators option so Mongoose schema
validators (e.g., enum for status, min for maxMarks) won't be enforced; update
the Assignment.findOneAndUpdate invocation that uses the filter {_id: id,
teacherId: userId}, sanitizedBody and options to include runValidators: true
(alongside new: true) so updates are validated against the schema before
persisting.

Expand All @@ -63,7 +64,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
35 changes: 24 additions & 11 deletions app/api/grades/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string
try {
const { id } = await ctx.params

// Validate ObjectId
if (!mongoose.Types.ObjectId.isValid(id)) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
Expand All @@ -25,7 +24,6 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string
return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 })
}

// Sanitize: only allow whitelisted fields
const sanitizedBody: Record<string, unknown> = {}
for (const key of ALLOWED_UPDATE_FIELDS) {
if (key in body) {
Expand All @@ -34,13 +32,27 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string
}

await connectDB()
const grade = await Grade.findOneAndUpdate(
{ _id: id },
sanitizedBody,
{ new: true }
)
if (!grade) return NextResponse.json({ error: 'Not found' }, { status: 404 })
return NextResponse.json(grade)

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

// 2. Apply whitelisted updates
Object.assign(existingGrade, sanitizedBody)

// 3. Recalculate grade string if marks changed
if (existingGrade.marks != null && existingGrade.maxMarks != null) {
const percentage = (existingGrade.marks / existingGrade.maxMarks) * 100
if (percentage >= 90) existingGrade.grade = 'A+'
else if (percentage >= 80) existingGrade.grade = 'A'
else if (percentage >= 70) existingGrade.grade = 'B'
else existingGrade.grade = 'F'
}
Comment on lines +43 to +50
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

Keep PUT grading consistent with POST creation.

app/api/grades/route.ts:22-30 assigns B+, B, C, and D, but this PUT path collapses those into B/F. A grade created at 75% becomes B+, then the same marks saved through PUT become B.

🐛 Proposed fix
     if (existingGrade.marks != null && existingGrade.maxMarks != null) {
       const percentage = (existingGrade.marks / existingGrade.maxMarks) * 100
       if (percentage >= 90) existingGrade.grade = 'A+'
       else if (percentage >= 80) existingGrade.grade = 'A'
-      else if (percentage >= 70) existingGrade.grade = 'B'
+      else if (percentage >= 70) existingGrade.grade = 'B+'
+      else if (percentage >= 60) existingGrade.grade = 'B'
+      else if (percentage >= 50) existingGrade.grade = 'C'
+      else if (percentage >= 40) existingGrade.grade = 'D'
       else existingGrade.grade = 'F'
     }
🤖 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 43 - 50, The PUT handler's
grade-recalculation block (where existingGrade, percentage are used) currently
collapses several tiers into 'B'/'F'; update the conditional mapping in this
block to match the POST creation logic used earlier (the same thresholds for
'A+', 'A', 'B+', 'B', 'C', 'D', 'F' from the POST section around lines 22-30) so
that a 75% remains 'B+' when re-saved via PUT; locate the recalculation using
existingGrade.marks/existingGrade.maxMarks and replace its if/else thresholds
with the exact same comparisons and grade assignments as the POST path.


// 4. Save the document
await existingGrade.save()
return NextResponse.json(existingGrade)

} catch (error) {
if (error instanceof Error) {
console.error('PUT /api/grades/[id] error:', error.message)
Expand All @@ -49,14 +61,15 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string
}
}

// Only teachers can delete their own grades
export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
const { userId } = await auth()
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

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 70 to +72
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:

#!/bin/bash
python - <<'PY'
from pathlib import Path

path = Path("app/api/grades/[id]/route.ts")
text = path.read_text()
delete_handler = text[text.index("export async function DELETE"):]

print("ObjectId.isValid in DELETE handler:", "ObjectId.isValid" in delete_handler)
print(delete_handler[:700])
PY

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 826


🏁 Script executed:

#!/bin/bash
python - <<'PY'
from pathlib import Path

path = Path("app/api/grades/[id]/route.ts")
text = path.read_text()

# Extract PUT handler
if "export async function PUT" in text:
    put_start = text.index("export async function PUT")
    # Find the next export to determine where PUT ends
    next_export_idx = text.find("export async function", put_start + 1)
    if next_export_idx == -1:
        put_handler = text[put_start:]
    else:
        put_handler = text[put_start:next_export_idx]
    
    print("=== PUT HANDLER ===")
    print("ObjectId.isValid in PUT handler:", "ObjectId.isValid" in put_handler)
    print("\nFirst 1200 characters:")
    print(put_handler[:1200])
else:
    print("PUT handler not found")
PY

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 1366


🏁 Script executed:

#!/bin/bash
python - <<'PY'
from pathlib import Path

path = Path("app/api/grades/[id]/route.ts")
text = path.read_text()

# Extract full DELETE handler including catch block
if "export async function DELETE" in text:
    delete_start = text.index("export async function DELETE")
    # Find end of function
    delete_section = text[delete_start:]
    
    # Get more of the delete handler to see the catch block
    lines = delete_section.split('\n')
    handler_lines = []
    brace_count = 0
    found_start = False
    
    for line in lines[:50]:  # First 50 lines should be enough
        if '{' in line:
            brace_count += line.count('{')
            found_start = True
        if '}' in line:
            brace_count -= line.count('}')
        handler_lines.append(line)
        if found_start and brace_count == 0:
            break
    
    full_handler = '\n'.join(handler_lines)
    print(full_handler)
else:
    print("DELETE handler not found")
PY

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 820


🏁 Script executed:

#!/bin/bash
# Verify Mongoose behavior with malformed ObjectIds
python - <<'PY'
import subprocess
import sys

# Check if there's a package.json to see mongoose version
try:
    with open("package.json", "r") as f:
        content = f.read()
        if "mongoose" in content:
            print("✓ Mongoose is in dependencies")
            # Extract mongoose version
            import re
            match = re.search(r'"mongoose":\s*"([^"]+)"', content)
            if match:
                print(f"  Version: {match.group(1)}")
except:
    pass

# Also check if there are any test files that might show how errors are handled
print("\n=== Checking for test files or usage patterns ===")
result = subprocess.run(["find", ".", "-name", "*.test.ts", "-o", "-name", "*.test.js", "-o", "-name", "*.spec.ts"], 
                       capture_output=True, text=True)
if result.stdout:
    print("Test files found:")
    print(result.stdout)
else:
    print("No obvious test files found")

# Search for any error handling patterns with malformed ObjectIds
print("\n=== Searching for CastError handling ===")
result = subprocess.run(["rg", "CastError|isValid.*ObjectId", "-n"], 
                       capture_output=True, text=True)
if result.stdout:
    print(result.stdout)
else:
    print("No CastError patterns found in codebase")
PY

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 292


Add ObjectId validation to DELETE handler for consistent error handling.

The PUT handler validates the grade id with mongoose.Types.ObjectId.isValid(id) and returns 404 for malformed IDs. The DELETE handler skips this check and passes the id directly to findOneAndDelete, causing Mongoose to throw a CastError that gets caught and returns 500 instead of 404. This creates an inconsistency in error responses between the two endpoints.

Proposed fix
    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 70 - 72, DELETE handler is missing
ObjectId validation so malformed ids cause a CastError; before calling
connectDB() and Grade.findOneAndDelete({ _id: id, teacherId: userId }) validate
the id using mongoose.Types.ObjectId.isValid(id) (same check as the PUT handler)
and return a 404 for invalid ids; place the check at the start of the DELETE
route (using the existing id variable from ctx.params) to ensure consistent
error handling.


if (!deleted) {
return NextResponse.json({ error: 'Grade not found' }, { status: 404 })
Expand All @@ -69,4 +82,4 @@ export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: str
}
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
}
8 changes: 4 additions & 4 deletions app/api/grades/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,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 @@ -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 @@ -72,8 +72,8 @@ export async function POST(req: NextRequest) {
const data = parsed.data
const max = data.maxMarks!
const term = data.term ?? 'Term 1'

const grade = Grade.findOneAndUpdate(
//Fixed
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 Down
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
15 changes: 8 additions & 7 deletions app/api/students/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string
}
}

await connectDB()
const student = await Student.findOneAndUpdate(
{ _id: id },
sanitizedBody,
{ new: true }
)
// FIXED
const student = await Student.findOneAndUpdate(
{ _id: id, teacherId: userId }, // ← add this
sanitizedBody,
{ new: true }
)
Comment on lines +36 to +41
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

Connect to MongoDB before updating.

This PUT path reaches Student.findOneAndUpdate without await connectDB(), unlike the DELETE handler and other routes. In a cold request, the update can fail or hang on connection buffering.

🐛 Proposed fix
-   // FIXED
-const student = await Student.findOneAndUpdate(
-  { _id: id, teacherId: userId },  // ← add this
-  sanitizedBody,
-  { new: true }
-)
+    await connectDB()
+    const student = await Student.findOneAndUpdate(
+      { _id: id, teacherId: userId },
+      sanitizedBody,
+      { new: true }
+    )
📝 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
// FIXED
const student = await Student.findOneAndUpdate(
{ _id: id, teacherId: userId }, // ← add this
sanitizedBody,
{ new: true }
)
await connectDB()
const student = await Student.findOneAndUpdate(
{ _id: id, teacherId: userId },
sanitizedBody,
{ new: true }
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/students/`[id]/route.ts around lines 36 - 41, The PUT handler calls
Student.findOneAndUpdate (with filter {_id: id, teacherId: userId}) without
ensuring a DB connection; add an awaited connectDB() call at the start of the
PUT route handler (same place the DELETE handler does) so the database is
connected before invoking Student.findOneAndUpdate, keeping the existing
sanitizedBody and { new: true } options.

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

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

if (!deleted) {
return NextResponse.json({ error: 'Student not found' }, { status: 404 })
Expand Down
23 changes: 14 additions & 9 deletions app/api/students/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export async function GET(req: NextRequest) {
const search = (searchParams.get("search") ?? "").replace(/\s+/g, ' ');
const classFilter = searchParams.get("class") ?? "";

// Parse and validate pagination
const pageStr = searchParams.get("page") ?? "1";
const limitStr = searchParams.get("limit") ?? "20";

Expand All @@ -38,19 +37,17 @@ export async function GET(req: NextRequest) {

if (Number.isNaN(page) || page < 1) page = 1;
if (Number.isNaN(limit) || limit < 1) limit = 20;
limit = Math.min(limit, 100); // Cap at 100
limit = Math.min(limit, 100);

const query: Record<string, unknown> = { teacherId: userId };
if (search) {
// Escape regex special characters to prevent ReDoS
const escapedSearch = escapeRegex(search);
query.$or = [
{ name: { $regex: escapedSearch, $options: "i" } },
{ rollNo: { $regex: escapedSearch, $options: "i" } },
{ class: { $regex: escapedSearch, $options: "i" } },
];
}
// Add class filter if provided and not 'all'
if (classFilter && classFilter !== "all") {
query.class = classFilter;
}
Expand Down Expand Up @@ -84,17 +81,25 @@ export async function POST(req: NextRequest) {

try {
await connectDB()

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

StudentSchema.safeParse(body)

const student = await Student.create({ ...(body as Record<string, unknown>), teacherId: userId })
// 1. Perform actual validation and catch errors
const validation = StudentSchema.safeParse(body)
if (!validation.success) {
return NextResponse.json({
error: 'Validation failed',
details: validation.error.format()
}, { status: 400 })
}

// 2. Only use validated data (prevents field injection)
const student = await Student.create({ ...validation.data, teacherId: userId })
Comment on lines +92 to +102
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:

#!/bin/bash
rg -n -C2 'name:\s*z\.string\(\)\.min\(1\)|rollNo:\s*z\.string\(\)\.min\(1\)|class:\s*z\.string\(\)\.min\(1\)' --glob 'route.ts'

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 1147


🏁 Script executed:

#!/bin/bash
# Check Zod version in package files
fd -e json 'package' | head -5 | xargs -I {} grep -l 'zod' {} 2>/dev/null || echo "Checking package.json directly"
cat package.json 2>/dev/null | grep -A2 -B2 '"zod"' || echo "package.json not found"

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 222


🏁 Script executed:

#!/bin/bash
# Get the full StudentSchema definition to see all validation rules
cat -n app/api/students/route.ts | head -20

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 851


🏁 Script executed:

#!/bin/bash
# Check if trim is used anywhere in the students route file
rg -n 'trim|\.trim\(\)' app/api/students/route.ts

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 66


🏁 Script executed:

#!/bin/bash
# Check the validation call and how validation.data is used (around lines 92-102)
cat -n app/api/students/route.ts | sed -n '85,110p'

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 1192


🏁 Script executed:

#!/bin/bash
# Check if body is preprocessed before validation
cat -n app/api/students/route.ts | sed -n '1,95p' | tail -30

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 1095


Add .trim() to required string fields in StudentSchema.

The validation accepts whitespace-only strings for name, rollNo, and class because z.string().min(1) validates string length without trimming. This allows creating student records with blank-looking values.

Suggested fix
const StudentSchema = z.object({
-  name: z.string().min(1),
-  rollNo: z.string().min(1),
-  class: z.string().min(1),
+  name: z.string().trim().min(1),
+  rollNo: z.string().trim().min(1),
+  class: z.string().trim().min(1),
   email: z.string().email().optional().or(z.literal('')),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/students/route.ts` around lines 92 - 102, Update the StudentSchema to
trim whitespace before validating required string fields so whitespace-only
values are rejected: change the schema definitions for name, rollNo, and class
to use z.string().trim().min(1) (or z.string().trim().nonempty()) instead of
z.string().min(1), then keep using StudentSchema.safeParse and Student.create({
...validation.data, teacherId: userId }) so only the trimmed, validated values
are persisted.

return NextResponse.json(student, { status: 201 })
} catch (error) {
if (error instanceof Error) {
Expand All @@ -105,4 +110,4 @@ export async function POST(req: NextRequest) {
}
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
}
48 changes: 23 additions & 25 deletions components/dashboard/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,42 @@
'use client'
"use client";

import { useState, useEffect } from 'react'
import { useState, useEffect } from "react";

interface NavbarProps {
onMenuClick: () => void
title: string
onMenuClick: () => void;
title: string;
}

export function Navbar({ onMenuClick, title }: NavbarProps) {
const [dark, setDark] = useState(false);
function getInitialDark(): boolean {
if (typeof window === "undefined") return false;
try {
const stored = localStorage.getItem("theme");
if (stored) return stored === "dark";
return window.matchMedia("(prefers-color-scheme: dark)").matches;
} catch {
return false;
}
}

// Initialize theme from localStorage and system preference on client-side only
useEffect(() => {
try {
const stored = localStorage.getItem("theme");
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
const isDark = stored ? stored === "dark" : prefersDark;
setDark(isDark);
document.documentElement.classList.toggle("dark", isDark);
} catch (e) {
// Silently fail if localStorage is not available
}
}, []);
export function Navbar({ onMenuClick, title }: NavbarProps) {
const [dark, setDark] = useState<boolean>(getInitialDark);

// Sync dark class to <html> whenever dark state changes
useEffect(() => {
document.documentElement.classList.toggle("dark", dark);
}, [dark]);

const toggleDark = () => {
const newDark = !dark;
setDark(newDark);
document.documentElement.classList.toggle("dark", newDark);
localStorage.setItem("theme", newDark ? "dark" : "light");
try {
localStorage.setItem("theme", newDark ? "dark" : "light");
} catch {
// Silently fail if localStorage is not available
}
};

return (
<header className="flex h-16 items-center justify-between border-b border-gray-200 dark:border-slate-700 bg-white dark:bg-slate-900 px-4 lg:px-6 shrink-0">
<header className="flex h-16 items-center justify-between border-b border-gray-200 dark:border-slate-700 bg-white dark:bg-slate-900 px-4 lg:px-6 shrink-0 transition-colors duration-200">
{/* Left: Hamburger + Title */}
<div className="flex items-center gap-3">
<button
Expand Down Expand Up @@ -106,4 +104,4 @@ export function Navbar({ onMenuClick, title }: NavbarProps) {
</div>
</header>
);
}
}
14 changes: 8 additions & 6 deletions lib/mongodb.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import mongoose from 'mongoose'

const MONGODB_URI = process.env.MONGODB_URI!

if (!MONGODB_URI) {
throw new Error('Please define the MONGODB_URI environment variable')
}
// Remove the global throw from here!

interface MongooseCache {
conn: typeof mongoose | null
Expand All @@ -21,6 +17,12 @@ global.mongooseCache = cached
export async function connectDB(): Promise<typeof mongoose> {
if (cached.conn) return cached.conn

// MOVE THE CHECK HERE
const MONGODB_URI = process.env.MONGODB_URI
if (!MONGODB_URI) {
throw new Error('Please define the MONGODB_URI environment variable')
}

if (!cached.promise) {
cached.promise = mongoose
.connect(MONGODB_URI, { bufferCommands: false })
Expand All @@ -37,4 +39,4 @@ export async function connectDB(): Promise<typeof mongoose> {
cached.promise = null
throw error
}
}
}
4 changes: 2 additions & 2 deletions models/Attendance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const AttendanceSchema = new Schema<IAttendance>(
},
{ timestamps: true }
)

AttendanceSchema.index({ studentId: 1, date: 1 }, { unique: true })
//FIXED
AttendanceSchema.index({ teacherId: 1, studentId: 1, date: 1 }, { unique: true })
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: Look for a migration or startup index-sync path that removes/replaces
# the old Attendance unique index.

rg -n -C3 'studentId_1_date_1|dropIndex|dropIndexes|syncIndexes|diffIndexes|AttendanceSchema\.index' --glob '!node_modules/**'

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 805


🏁 Script executed:

#!/bin/bash
# Search for migration files and db initialization logic
fd -e 'mjs|js|ts' -t f | xargs grep -l 'migrate\|migration\|dropIndex\|syncIndexes\|Attendance.*index' 2>/dev/null | head -20

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 66


🏁 Script executed:

#!/bin/bash
# Check if there's database initialization or connection setup that syncs indexes
rg -A 10 'connect.*mongoose|connection.*open|db.*init|syncIndexes' --type ts --type js -i

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 576


🏁 Script executed:

#!/bin/bash
# Check the seed.mjs file context - is it auto-generated or manually maintained?
head -20 scripts/seed.mjs

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 664


🏁 Script executed:

#!/bin/bash
# Look for any index migration or database update scripts
fd -e 'js' -e 'mjs' -e 'ts' scripts/ migrations/ db/ 2>/dev/null | xargs cat 2>/dev/null | head -100

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 66


🏁 Script executed:

#!/bin/bash
# Get full connectDB function to check for index sync
sed -n '1,50p' lib/mongodb.ts

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 1051


🏁 Script executed:

#!/bin/bash
# Check git history for Attendance.ts changes
git log --oneline models/Attendance.ts 2>/dev/null | head -10

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 149


🏁 Script executed:

#!/bin/bash
# Check if seed.mjs is actively used or just example code
grep -r "seed" package.json README.md 2>/dev/null

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 132


🏁 Script executed:

#!/bin/bash
# Look for any TODO or FIXME comments related to indexes
rg 'TODO|FIXME|XXX' models/Attendance.ts scripts/seed.mjs lib/mongodb.ts

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 66


🏁 Script executed:

#!/bin/bash
# Check full seed.mjs context around the Attendance schema - does it create/sync indexes?
sed -n '95,115p' scripts/seed.mjs

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 963


🏁 Script executed:

#!/bin/bash
# Look for .syncIndexes() calls anywhere in the codebase
rg 'syncIndexes|ensureIndexes|dropIndex' -t ts -t js -t mjs

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 111


🏁 Script executed:

#!/bin/bash
# Check if there's a production vs dev distinction for seed.mjs
grep -B5 -A5 "seed" README.md 2>/dev/null | head -30

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 66


Update seed.mjs and add index migration.

The schema fix on line 27 is correct, but the seed script still creates the old { studentId: 1, date: 1 } index (scripts/seed.mjs:105). Additionally, existing deployed databases will retain the old unique index and need an explicit migration to drop it and create the new { teacherId: 1, studentId: 1, date: 1 } index. Without both changes, fresh seeds and existing production data will continue to fail with cross-teacher duplicate key errors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/Attendance.ts` at line 27, The schema change adds
AttendanceSchema.index({ teacherId: 1, studentId: 1, date: 1 }, { unique: true
}) but the seed script still creates the old { studentId: 1, date: 1 } unique
index and deployed DBs keep the old index; update the seed logic that currently
creates the { studentId:1, date:1} index to create the new { teacherId:1,
studentId:1, date:1 } unique index instead, and add a one-off migration (e.g.,
"add-teacher-to-attendance-index") that drops the existing unique index on {
studentId:1, date:1 } if present and then creates the new unique index on {
teacherId:1, studentId:1, date:1 } in an idempotent way (handle index-not-found
errors gracefully).


export const Attendance = models.Attendance ?? model<IAttendance>('Attendance', AttendanceSchema)
Loading
Loading