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
77 changes: 17 additions & 60 deletions app/api/assignments/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,76 +4,33 @@ 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_FIELDS = ['title', 'description', 'dueDate', 'deadline', 'subject', 'class', 'status', 'kanbanStatus', 'maxMarks']

export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
const { userId } = await auth()
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

const { userId } = await auth(); if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
try {
const { id } = await ctx.params

// Validate ObjectId
if (!mongoose.Types.ObjectId.isValid(id)) {
return NextResponse.json({ error: 'Invalid id' }, { status: 400 })
}
if (!mongoose.Types.ObjectId.isValid(id)) return NextResponse.json({ error: 'Invalid id' }, { status: 400 })
const body = await req.json().catch(() => ({}))
const sanitized: any = {}
ALLOWED_FIELDS.forEach(k => { if (k in body) sanitized[k] = body[k] })
if (Object.keys(sanitized).length === 0) return NextResponse.json({ error: 'No valid fields' }, { status: 400 })

await connectDB()

let body
try {
body = await req.json()
} catch {
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) {
sanitizedBody[key] = body[key]
}
}

const assignment = await Assignment.findOneAndUpdate(
{ _id: id },
sanitizedBody,
{ new: true }
)
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)
}
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
const updated = await Assignment.findOneAndUpdate({ _id: id, teacherId: userId }, { $set: sanitized }, { new: true })
Comment on lines +14 to +20
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
# Confirms primitive JSON bodies can throw during `in` checks, and checks whether validators are enabled on assignment updates.

node - <<'NODE'
for (const value of [null, 1, true, 'text']) {
  try {
    console.log(typeof value, '=>', 'deadline' in value)
  } catch (error) {
    console.log(typeof value, '=> throws:', error.message)
  }
}
NODE

rg -nUP 'Assignment\.findOneAndUpdate\((?s:.*?)runValidators' 'app/api/assignments/[id]/route.ts' || true

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 385


🏁 Script executed:

rg -n "ALLOWED_FIELDS" app/api/assignments/[id]/route.ts

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 292


🏁 Script executed:

cat -n app/api/assignments/[id]/route.ts | head -50

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 2419


🏁 Script executed:

cat -n app/api/assignments/route.ts | head -80

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 3563


Validate assignment update payloads and enforce deadline consistency.

The in operator on line 16 throws when req.json() successfully parses to a non-object value (e.g., null, a string, or a number). Additionally, deadline is allowed in updates without the past-date validation applied in POST /api/assignments (lines 69–72 of route.ts), and findOneAndUpdate omits runValidators: true, bypassing Mongoose schema validation.

Proposed fix
-    const body = await req.json().catch(() => ({}))
-    const sanitized: any = {}
+    const body = await req.json().catch(() => null)
+    if (!body || typeof body !== 'object' || Array.isArray(body)) {
+      return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
+    }
+
+    const sanitized: Record<string, unknown> = {}
     ALLOWED_FIELDS.forEach(k => { if (k in body) sanitized[k] = body[k] })
     if (Object.keys(sanitized).length === 0) return NextResponse.json({ error: 'No valid fields' }, { status: 400 })
+
+    if (typeof sanitized.deadline === 'string') {
+      if (new Date(sanitized.deadline) < new Date()) {
+        return NextResponse.json({ error: 'Deadline cannot be in the past' }, { status: 400 })
+      }
+    }
 
     await connectDB()
-    const updated = await Assignment.findOneAndUpdate({ _id: id, teacherId: userId }, { $set: sanitized }, { new: true })
+    const updated = await Assignment.findOneAndUpdate(
+      { _id: id, teacherId: userId },
+      { $set: sanitized },
+      { new: true, runValidators: true }
+    )
🤖 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 14 - 20, The update handler
currently assumes req.json() returns an object and uses the `in` operator on
`body`, which throws for non-object values; fix by ensuring `body` is an object
(e.g., check typeof body === 'object' && body !== null) before iterating
`ALLOWED_FIELDS` to build `sanitized`; also enforce the same `deadline`
validation used in the POST flow (reject `deadline` if it's a past date) when
`sanitized.deadline` is present; finally, call `Assignment.findOneAndUpdate`
with the option `runValidators: true` (alongside `{ new: true }`) so Mongoose
schema validators are applied during updates.

if (!updated) return NextResponse.json({ error: 'Not found or unauthorized' }, { status: 404 })
return NextResponse.json(updated)
} catch { return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) }
}

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

const { userId } = await auth(); if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
try {
const { id } = await ctx.params

// Validate ObjectId
if (!mongoose.Types.ObjectId.isValid(id)) {
return NextResponse.json({ error: 'Invalid id' }, { status: 400 })
}

if (!mongoose.Types.ObjectId.isValid(id)) return NextResponse.json({ error: 'Invalid id' }, { status: 400 })
await connectDB()
const deleted = await Assignment.findOneAndDelete({ _id: id })

if (!deleted) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}

const deleted = await Assignment.findOneAndDelete({ _id: id, teacherId: userId })
if (!deleted) return NextResponse.json({ error: 'Not found or unauthorized' }, { status: 404 })
return NextResponse.json({ success: true })
} catch (error) {
if (error instanceof Error) {
console.error('DELETE /api/assignments/[id] error:', error.message)
}
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
} catch { return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) }
}
55 changes: 22 additions & 33 deletions app/api/assignments/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ const AssignmentSchema = z.object({
subject: z.string().min(1),
class: z.string().min(1),
deadline: z.string().min(1),
maxMarks: z.number().min(1).optional(),
status: z.enum(['active', 'closed']).optional(),
kanbanStatus: z.enum(['todo', 'in_progress', 'submitted']).optional(),
maxMarks: z.number().min(1).default(100),
status: z.enum(['active', 'closed']).default('active'),
kanbanStatus: z.enum(['todo', 'in_progress', 'submitted']).default('todo'),
})

export async function GET(req: NextRequest) {
Expand All @@ -23,17 +23,16 @@ export async function GET(req: NextRequest) {
await connectDB()
const { searchParams } = new URL(req.url)
const status = searchParams.get('status')

// Parse and validate pagination

const pageStr = searchParams.get('page') ?? '1'
const limitStr = searchParams.get('limit') ?? '20'

let page = parseInt(pageStr, 10)
let limit = parseInt(limitStr, 10)

if (!Number.isFinite(page) || page < 1) page = 1
if (!Number.isFinite(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 (status) query.status = status
Expand All @@ -44,15 +43,14 @@ export async function GET(req: NextRequest) {
.skip(skip)
.limit(limit)
.lean()

const total = await Assignment.countDocuments(query)

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 instanceof Error ? error.message : error)
// FIX: Removed .stack leak
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

Expand All @@ -62,30 +60,21 @@ export async function POST(req: NextRequest) {

try {
await connectDB()

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

const body = await req.json().catch(() => null)
if (!body) return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })

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

try {
const assignment = await Assignment.create({ ...parsed.data, teacherId: userId })
return NextResponse.json(assignment, { status: 201 })
} catch (dbError) {
if (dbError instanceof Error) {
console.error('Assignment.create error:', dbError.message)
}
return NextResponse.json({ error: 'Failed to create assignment' }, { status: 500 })
// FIX: Deadline validation (Prevent past dates)
if (new Date(parsed.data.deadline) < new Date()) {
return NextResponse.json({ error: 'Deadline cannot be in the past' }, { status: 400 });
Comment on lines +69 to +71
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
# Demonstrates why timestamp comparison rejects same-day date-only strings
# and why invalid strings need schema-level validation.

node - <<'NODE'
const today = new Date().toISOString().slice(0, 10)
console.log({ today, parsedAsDate: new Date(today).toISOString(), rejectedByTimestampCompare: new Date(today) < new Date() })

const invalid = new Date('not-a-date')
console.log({ invalidDateIsNaN: Number.isNaN(invalid.getTime()), invalidComparisonResult: invalid < new Date() })
NODE

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 248


🏁 Script executed:

# Search for AssignmentSchema definition and the deadline validation code
cd /repo && find . -type f -name "*.ts" -o -name "*.js" | head -20

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 139


🏁 Script executed:

# Look for AssignmentSchema and deadline validation
rg "AssignmentSchema" --type ts

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 419


🏁 Script executed:

# Check the specific file and lines mentioned
cat -n app/api/assignments/route.ts | head -100

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 3563


🏁 Script executed:

# Check if z.date() or similar Zod date validation exists
rg "z\.(date|iso)" --type ts -A 2

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 66


Fix deadline validation to handle date-only strings and invalid dates correctly.

The current validation has two problems:

  1. Same-day deadlines are rejected: new Date('YYYY-MM-DD') parses at midnight UTC, so deadlines with today's date are treated as past times after midnight.
  2. Invalid dates bypass validation: Invalid Date < new Date() returns false, allowing malformed dates to reach the database and cause casting errors.

Use z.coerce.date() in the schema and compare dates as ISO strings:

Proposed fix
 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),
+  deadline: z.coerce.date(),
   maxMarks: z.number().min(1).default(100),
   status: z.enum(['active', 'closed']).default('active'),
   kanbanStatus: z.enum(['todo', 'in_progress', 'submitted']).default('todo'),
 })
-    // FIX: Deadline validation (Prevent past dates)
-    if (new Date(parsed.data.deadline) < new Date()) {
+    // Deadline validation (compare date-only values)
+    const today = new Date().toISOString().split('T')[0]
+    const deadlineDate = parsed.data.deadline.toISOString().split('T')[0]
+    if (deadlineDate < today) {
       return NextResponse.json({ error: 'Deadline cannot be in the past' }, { status: 400 });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/assignments/route.ts` around lines 69 - 71, The deadline validation
rejects same-day ISO date strings and lets invalid dates through; update the
schema to use z.coerce.date() for parsed.data.deadline so invalid/malformed
dates fail validation, and change the comparison in the route (the block
checking parsed.data.deadline) to compare date-only ISO strings (e.g.,
deadline.toISOString().slice(0,10) >= new Date().toISOString().slice(0,10))
instead of comparing Date objects directly so a "YYYY-MM-DD" deadline for today
is accepted; ensure you return the existing 400 error for invalid/parsed
failures.

}

const assignment = await Assignment.create({ ...parsed.data, teacherId: userId })
return NextResponse.json(assignment, { status: 201 })
} catch (error) {
if (error instanceof Error) {
console.error('POST /api/assignments error:', error.message)
}
console.error('POST /api/assignments error:', error instanceof Error ? error.message : error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
}
126 changes: 17 additions & 109 deletions app/api/attendance/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,119 +12,30 @@ const AttendanceSchema = z.object({
status: z.enum(['present', 'absent', 'late']),
})

const BulkSchema = z.array(AttendanceSchema)

export async function GET(req: NextRequest) {
const { userId } = await auth()
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

try {
await connectDB();
const { searchParams } = new URL(req.url);
const date = searchParams.get("date");
const cls = searchParams.get("class");
const studentId = searchParams.get("studentId");

const startDate = searchParams.get("startDate");
const endDate = searchParams.get("endDate");

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);
if (normalized) {
query.date = normalized;
} else {
return NextResponse.json(
{ error: "Invalid date format. Use YYYY-MM-DD or ISO 8601" },
{ status: 400 },
);
}
} else if (startDate || endDate) {
const dateRange: Record<string, string> = {};
if (startDate) {
const normalized = normalizeDate(startDate);
if (normalized) dateRange.$gte = normalized;
else
return NextResponse.json(
{ error: "Invalid startDate format. Use YYYY-MM-DD or ISO 8601" },
{ status: 400 },
);
}
if (endDate) {
const normalized = normalizeDate(endDate);
if (normalized) dateRange.$lte = normalized;
else
return NextResponse.json(
{ error: "Invalid endDate format. Use YYYY-MM-DD or ISO 8601" },
{ status: 400 },
);
}
query.date = dateRange;
}
if (cls) query.class = cls;
if (studentId) query.studentId = studentId;

const records = await Attendance.find(query)
.sort({ date: -1, studentName: 1 })
.lean();
return NextResponse.json(records);
} catch (err) {
console.error(
"GET /api/attendance error:",
err instanceof Error ? err.message : err,
);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}

export async function POST(req: NextRequest) {
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirms remaining GET consumers and whether the route still exports GET.

rg -n -C2 'fetch\((`|")/api/attendance' 
rg -nP 'export\s+async\s+function\s+GET\b' 'app/api/attendance/route.ts' || true

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 2428


Restore GET /api/attendance before merging.

The route handler no longer exports GET, but four consumers still depend on it: StudentsClient.tsx:165 (student attendance), AttendanceClient.tsx:103 (history), AttendanceClient.tsx:131 (heatmap), and OverviewClient.tsx:198 (dashboard stats). Removing it without providing an alternative will break all attendance read operations.

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

In `@app/api/attendance/route.ts` at line 15, The POST-only route removed the
exported GET handler that four consumers (StudentsClient.tsx,
AttendanceClient.tsx twice for history and heatmap, and OverviewClient.tsx for
dashboard stats) still call; restore an exported async function GET(req:
NextRequest) that performs the attendance read logic (parsing query params like
studentId/date/range as the clients expect), invokes the same attendance
retrieval service/DB used elsewhere, and returns a proper NextResponse/JSON
payload with status codes and CORS/headers consistent with POST so those
components continue to work. Ensure the exported symbol name is GET and that its
response shape matches what StudentsClient.tsx:165,
AttendanceClient.tsx:103/131, and OverviewClient.tsx:198 expect.

const { userId } = await auth()
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

try {
await connectDB()
const body = await req.json().catch(() => null)
if (!body) return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })

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

// Support both single and bulk
const isBulk = Array.isArray(body)
if (isBulk && body.length > 500) {
return NextResponse.json(
{ error: "Bulk payload exceeds maximum of 500 records" },
{ status: 400 },
);
const parsed = isBulk ? z.array(AttendanceSchema).safeParse(body) : AttendanceSchema.safeParse(body)
if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 })

const today = new Date().toISOString().split('T')[0]
const data = parsed.data as any

// BUG FIX: Prevent future dating
const isFuture = (d: string) => d > today
if (isBulk ? data.some((r: any) => isFuture(r.date)) : isFuture(data.date)) {
return NextResponse.json({ error: "Cannot mark attendance for future dates" }, { status: 400 })
Comment on lines +28 to +34
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
# Demonstrates that raw string comparison rejects same-day datetime strings.

node - <<'NODE'
const today = '2026-04-18'
for (const value of ['2026-04-18', '2026-04-18T12:00:00Z', 'not-a-date']) {
  console.log({ value, isFutureByStringCompare: value > today })
}
NODE

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 259


🏁 Script executed:

cat -n app/api/attendance/route.ts | head -60

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 2633


🏁 Script executed:

# Check Zod version and available date validation methods
fd 'package.json' | head -5 | xargs cat | grep -i zod

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 103


🏁 Script executed:

# Verify if z.string().date() is available in Zod 4.3.6
# Check Zod documentation or examples in the codebase
rg -i "z\.string.*\.date\|z\.date" | head -20

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 66


🏁 Script executed:

# Check Zod 4.3.6 API - look for how to validate date strings
# Try searching for common Zod patterns or check if there are examples elsewhere
rg "z\." app/ | grep -i "date\|parse\|refine" | head -20

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 271


🏁 Script executed:

# Let's also verify the exact Zod version and its capabilities
# by checking if z.string().date() or similar exists
node <<'EOF'
try {
  const z = require('zod');
  // Check available methods on z
  console.log('z.string() methods:', Object.getOwnPropertyNames(z.ZodString.prototype).filter(m => m.includes('date')));
  // Try to use z.string().date() 
  const schema = z.string().date();
  console.log('z.string().date() exists:', !!schema);
} catch(e) {
  console.log('Error:', e.message);
}
EOF

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 159


🏁 Script executed:

# Search for any date validation patterns already used in the codebase
rg "regex|refine|transform" app/ | grep -i date | head -10

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 66


🏁 Script executed:

# Check if there are any tests or other examples of date validation
fd -e ts -e tsx -e js | xargs grep -l "date" | head -10

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 376


🏁 Script executed:

# Let's check Zod's actual API in version 4.3.6 by looking at its usage patterns
# Search for any validation patterns that might give us clues about date handling
rg "z\.string" app/ -A 2 | head -30

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 1728


🏁 Script executed:

# Check if there's a package-lock.json that might give us exact Zod version info
cat package-lock.json | grep -A 10 '"zod"' | head -20

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 664


🌐 Web query:

Zod 4.3.6 date validation string.date() API

💡 Result:

In Zod 4.3.6, there is no string.date method on z.string. Date validation for strings uses top-level factory functions under the z.iso namespace, such as z.iso.date for ISO date strings in YYYY-MM-DD format. These are regex-based validations for common date formats: - z.iso.date: Validates YYYY-MM-DD format. Example: const date = z.iso.date; date.parse("2020-01-01"); // ✅ date.parse("2020-1-1"); // ❌ date.parse("2020-01-32"); // ❌ - z.iso.datetime: Validates ISO 8601 datetime strings (with options for precision, offset). Example: const datetime = z.iso.datetime; datetime.parse("2020-01-01T06:15:00Z"); // ✅ - z.iso.time: Validates HH:MM[:SS[.s+]] format. To coerce strings to Date objects, use z.coerce.date or z.string.pipe(z.coerce.date). For bidirectional string-to-Date conversion, use z.codec(z.iso.datetime, z.date, { decode: (iso) => new Date(iso), encode: (date) => date.toISOString }). Zod 4.3.6 was released January 4, 2026, and these APIs are unchanged from earlier v4 releases. No z.string.date API exists; date validations are via z.iso.* factory functions, not chainable string methods.

Citations:


Use Zod's ISO date validator to prevent string-comparison vulnerabilities.

The schema accepts any non-empty string for date (line 11), but the future-date check uses lexicographic string comparison (line 32). This causes false positives: '2026-04-18T12:00:00Z' > '2026-04-18' is true (same-day datetime flagged as future), and 'not-a-date' > '2026-04-18' is true (malformed string flagged as future). Attendance records may be incorrectly rejected or accepted.

Replace date: z.string().min(1) with date: z.iso.date() to enforce YYYY-MM-DD format at validation time, ensuring the string comparison logic receives only valid dates.

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

In `@app/api/attendance/route.ts` around lines 28 - 34, The current date
validation accepts any non-empty string and then uses lexicographic comparison
in isFuture (used with parsed and isBulk), which misclassifies datetimes or
malformed strings; update the Zod schema's date field (replace the existing
date: z.string().min(1)) to use an ISO date validator (e.g., z.iso.date() per
the review) so only YYYY-MM-DD strings pass validation, then keep the existing
isFuture check (or, optionally, replace it by parsing validated strings to Date
for clarity) so parsed.data dates are guaranteed to be valid ISO dates before
the comparison in isFuture.

}
const parsed = isBulk ? BulkSchema.safeParse(body) : AttendanceSchema.safeParse(body)
if (!parsed.success)
return NextResponse.json(
{ error: parsed.error.flatten() },
{ status: 400 },
);

if (isBulk) {
const ops = (parsed.data as z.infer<typeof BulkSchema>).map((record) => ({
const ops = data.map((record: any) => ({
updateOne: {
filter: { teacherId: userId, studentId: record.studentId, date: record.date },
update: { $set: { ...record, teacherId: userId } },
Expand All @@ -135,14 +46,11 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ success: true, count: ops.length })
} else {
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: data.studentId, date: data.date },
{ $set: { ...data, teacherId: userId } },
{ upsert: true, new: true }
)
return NextResponse.json(record, { status: 201 })
}
} catch (err) {
console.error(err)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
} catch { return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) }
}
Loading
Loading