-
Notifications
You must be signed in to change notification settings - Fork 23
fix: resolve issues in API endpoints and dashboard components #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
8887704
e70054c
a0a8d51
742fb7f
5ae2d74
243702d
ac9c5d5
28b1f11
ebcdcb8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,7 +9,9 @@ const AssignmentSchema = z.object({ | |||||||||||||||||
| description: z.string().optional(), | ||||||||||||||||||
| subject: z.string().min(1), | ||||||||||||||||||
| class: z.string().min(1), | ||||||||||||||||||
| deadline: z.string().min(1), | ||||||||||||||||||
| deadline: z.string().min(1).refine((value) => !Number.isNaN(Date.parse(value)), { | ||||||||||||||||||
| message: 'Invalid deadline', | ||||||||||||||||||
| }), | ||||||||||||||||||
|
Comment on lines
+12
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
node - <<'NODE'
for (const value of ['2026-02-31', '2026-13-01', '1', '2026-04-18', '2026-04-18T00:00:00.000Z']) {
const ms = Date.parse(value)
console.log(value, Number.isNaN(ms) ? 'invalid' : new Date(ms).toISOString())
}
NODERepository: JavaScript-Mastery-Pro/discord-challenge Length of output: 251 🏁 Script executed: cat package.json | grep -A 2 -B 2 "zod"Repository: JavaScript-Mastery-Pro/discord-challenge Length of output: 191 🏁 Script executed: find . -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" \) -exec grep -l "z\\..*date" {} \; | head -10Repository: JavaScript-Mastery-Pro/discord-challenge Length of output: 113 🏁 Script executed: cat app/api/assignments/route.tsRepository: JavaScript-Mastery-Pro/discord-challenge Length of output: 3209 🏁 Script executed: cat app/api/attendance/route.tsRepository: JavaScript-Mastery-Pro/discord-challenge Length of output: 5226 🌐 Web query:
💡 Result: In Zod version 4.3.6, the ISO date and datetime validation API is available via the z.iso namespace on the main z object. Key methods: ISO Date (validates YYYY-MM-DD format): import { z } from 'zod'; const dateSchema = z.iso.date; dateSchema.parse('2020-01-01'); // ✅ valid dateSchema.parse('2020-1-1'); // ❌ invalid (must be padded) dateSchema.parse('2020-01-32'); // ❌ invalid day ISO Datetime (validates ISO 8601 format, default no timezone offsets except Z): const datetimeSchema = z.iso.datetime; datetimeSchema.parse('2020-01-01T06:15:00Z'); // ✅ valid datetimeSchema.parse('2020-01-01T06:15:00.123Z'); // ✅ valid (subseconds) datetimeSchema.parse('2020-01-01T06:15:00+02:00'); // ❌ invalid (offsets not allowed by default) Other related: - z.iso.time: HH:MM[:SS[.s+]] (no offsets) - z.iso.duration: ISO 8601 duration These are regex-based string validations (not semantic date checks like min/max dates; use .refine with date libraries for that). For Date objects: use z.date. To convert ISO string to Date: const stringToDate = z.codec( z.iso.datetime, z.date, { decode: (isoString) => new Date(isoString), encode: (date) => date.toISOString, } ); stringToDate.parse('2024-01-15T10:30:00.000Z'); // returns Date object Official docs: https://zod.dev/ (Zod 4 stable since 2025). Version 4.3.6 released ~Jan 2026 per npm. Citations:
Use strict ISO validation for
🛡️ Proposed fix- deadline: z.string().min(1).refine((value) => !Number.isNaN(Date.parse(value)), {
- message: 'Invalid deadline',
- }),
+ deadline: z.iso.datetime(),If dates without time components are acceptable: - deadline: z.string().min(1).refine((value) => !Number.isNaN(Date.parse(value)), {
- message: 'Invalid deadline',
- }),
+ deadline: z.union([z.iso.date(), z.iso.datetime()]),📝 Committable suggestion
Suggested change
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| maxMarks: z.number().min(1).optional(), | ||||||||||||||||||
| status: z.enum(['active', 'closed']).optional(), | ||||||||||||||||||
| kanbanStatus: z.enum(['todo', 'in_progress', 'submitted']).optional(), | ||||||||||||||||||
|
|
@@ -52,7 +54,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 }) | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,11 +1,16 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| import { auth } from '@clerk/nextjs/server' | ||||||||||||||||||||||||||||||||||||||||||||||
| import { NextRequest, NextResponse } from 'next/server' | ||||||||||||||||||||||||||||||||||||||||||||||
| import mongoose from 'mongoose' | ||||||||||||||||||||||||||||||||||||||||||||||
| import { connectDB } from '@/lib/mongodb' | ||||||||||||||||||||||||||||||||||||||||||||||
| import { Attendance } from '@/models/Attendance' | ||||||||||||||||||||||||||||||||||||||||||||||
| import { z } from 'zod' | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const StudentIdSchema = z.string().refine((value) => mongoose.Types.ObjectId.isValid(value), { | ||||||||||||||||||||||||||||||||||||||||||||||
| message: 'Invalid studentId', | ||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const AttendanceSchema = z.object({ | ||||||||||||||||||||||||||||||||||||||||||||||
| studentId: z.string().min(1), | ||||||||||||||||||||||||||||||||||||||||||||||
| studentId: StudentIdSchema, | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+3
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Yes, in Mongoose 9, mongoose.Types.ObjectId.isValid accepts 12-character non-hex strings as valid, consistent with BSON ObjectId behavior where any 12-byte string is technically valid. For example, mongoose.Types.ObjectId.isValid("zzzzzzzzzzzz") returns true. This has been the case across versions, including Mongoose 9, as no breaking changes to this behavior appear in the v9 changelog or migration guide (though v9 makes isValidObjectId stricter for numbers). Yes, mongoose.isObjectIdOrHexString is the recommended stricter validator. It returns true only if the value is a Mongoose ObjectId instance or a 24-character hex string (matching /^[0-9a-fA-F]{24}$/), and false for 12-char non-hex strings like "zzzzzzzzzzzz" or numbers. It was introduced in Mongoose 6.2.5 to address common use cases needing hex validation, and maintainers like Citations:
🏁 Script executed: # Check if the file exists and examine the relevant sections
cat -n app/api/attendance/route.ts | head -50Repository: JavaScript-Mastery-Pro/discord-challenge Length of output: 2098 Use strict ObjectId validation for
🛡️ Proposed fix-import mongoose from 'mongoose'
import { connectDB } from '@/lib/mongodb'
import { Attendance } from '@/models/Attendance'
import { z } from 'zod'
-const StudentIdSchema = z.string().refine((value) => mongoose.Types.ObjectId.isValid(value), {
+const StudentIdSchema = z.string().regex(/^[0-9a-fA-F]{24}$/, {
message: 'Invalid studentId',
})- if (studentId && !mongoose.Types.ObjectId.isValid(studentId)) {
+ if (studentId && !StudentIdSchema.safeParse(studentId).success) {
return NextResponse.json(
{ error: "Invalid studentId" },
{ status: 400 },
);Also applies to lines 38–43 (GET validation). 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| studentName: z.string().min(1), | ||||||||||||||||||||||||||||||||||||||||||||||
| class: z.string().min(1), | ||||||||||||||||||||||||||||||||||||||||||||||
| date: z.string().min(1), | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -30,6 +35,13 @@ export async function GET(req: NextRequest) { | |||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const query: Record<string, unknown> = { teacherId: userId }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (studentId && !mongoose.Types.ObjectId.isValid(studentId)) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||
| { error: "Invalid studentId" }, | ||||||||||||||||||||||||||||||||||||||||||||||
| { status: 400 }, | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Helper to validate and normalize date strings to YYYY-MM-DD format | ||||||||||||||||||||||||||||||||||||||||||||||
| const normalizeDate = (dateStr: string): string | null => { | ||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,8 +3,31 @@ import { NextRequest, NextResponse } from 'next/server' | |
| import mongoose from 'mongoose' | ||
| import { connectDB } from '@/lib/mongodb' | ||
| import { Grade } from '@/models/Grade' | ||
| import { z } from 'zod' | ||
|
|
||
| const ALLOWED_UPDATE_FIELDS = ['marks', 'maxMarks', 'grade'] | ||
| const StudentIdSchema = z.string().refine((value) => mongoose.Types.ObjectId.isValid(value), { | ||
| message: 'Invalid studentId', | ||
| }) | ||
|
|
||
| const GradeUpdateSchema = z.object({ | ||
| studentId: StudentIdSchema.optional(), | ||
| studentName: z.string().min(1).optional(), | ||
| subject: z.string().min(1).optional(), | ||
| marks: z.number().min(0).optional(), | ||
| maxMarks: z.number().min(1).optional(), | ||
| term: z.string().min(1).optional(), | ||
| }) | ||
|
|
||
| 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() | ||
|
|
@@ -15,7 +38,7 @@ export async function PUT(req: NextRequest, ctx: { params: Promise<{ id: string | |
|
|
||
| // Validate ObjectId | ||
| if (!mongoose.Types.ObjectId.isValid(id)) { | ||
| return NextResponse.json({ error: 'Not found' }, { status: 404 }) | ||
| return NextResponse.json({ error: 'Invalid id' }, { status: 400 }) | ||
| } | ||
|
|
||
| let body | ||
|
|
@@ -25,26 +48,47 @@ 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) { | ||
| sanitizedBody[key] = body[key] | ||
| } | ||
| const parsed = GradeUpdateSchema.safeParse(body) | ||
| if (!parsed.success) { | ||
| return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }) | ||
| } | ||
|
|
||
| if (Object.keys(parsed.data).length === 0) { | ||
| return NextResponse.json({ error: 'No valid fields to update' }, { status: 400 }) | ||
| } | ||
|
|
||
| await connectDB() | ||
| const existing = await Grade.findOne({ _id: id, teacherId: userId }) | ||
| if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 }) | ||
|
|
||
| const marks = parsed.data.marks ?? existing.marks | ||
| const maxMarks = parsed.data.maxMarks ?? existing.maxMarks | ||
| if (marks > maxMarks) { | ||
| return NextResponse.json( | ||
| { error: { fieldErrors: { marks: ['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 }, | ||
| { | ||
| $set: { | ||
| ...parsed.data, | ||
| grade: calcGrade(marks, maxMarks), | ||
| }, | ||
| }, | ||
| { new: true, runValidators: true, context: 'query' } | ||
| ) | ||
|
Comment on lines
60
to
82
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor TOCTOU between The If concurrent grade edits are plausible, either (a) add a schema-level Mongoose validator that compares 🤖 Prompt for AI Agents |
||
| 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 }) | ||
| } | ||
| } | ||
|
|
@@ -55,8 +99,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 }) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,23 @@ | ||
| import { auth } from '@clerk/nextjs/server' | ||
| import { NextRequest, NextResponse } from 'next/server' | ||
| import mongoose from 'mongoose' | ||
| import { connectDB } from '@/lib/mongodb' | ||
| import { Grade } from '@/models/Grade' | ||
| import { z } from 'zod' | ||
|
|
||
| const StudentIdSchema = z.string().refine((value) => mongoose.Types.ObjectId.isValid(value), { | ||
| message: 'Invalid studentId', | ||
| }) | ||
|
|
||
| const GradeSchema = z.object({ | ||
| studentId: z.string().min(1), | ||
| studentId: StudentIdSchema, | ||
|
Comment on lines
+3
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Yes, in Mongoose 9, mongoose.Types.ObjectId.isValid accepts 12-character non-hex strings as valid, because it follows BSON ObjectId semantics where any 12-byte sequence (including 12-char strings) is valid. It returns true for strings like "zzzzzzzzzzzz" or "testtesttest". Yes, mongoose.isObjectIdOrHexString is the recommended stricter validator. It returns true only for ObjectId instances or exactly 24-character hex strings (matching /^[0-9a-fA-F]{24}$/), and false for 12-char strings, numbers, or other values. This is explicitly recommended by Mongoose maintainers in multiple GitHub issues for cases needing hex string validation, and the feature was added in Mongoose 6.2.5 with no breaking changes noted in Mongoose 9 changelogs. Citations:
Replace
🛡️ Proposed fixconst StudentIdSchema = z.string().refine((value) => mongoose.isObjectIdOrHexString(value), {
message: 'Invalid studentId',
})- if (studentId && !mongoose.Types.ObjectId.isValid(studentId)) {
+ if (studentId && !mongoose.isObjectIdOrHexString(studentId)) {
return NextResponse.json({ error: 'Invalid studentId' }, { status: 400 })
}Also applies to: 48-50 🤖 Prompt for AI Agents |
||
| studentName: z.string().min(1), | ||
| subject: z.string().min(1), | ||
| marks: z.number().min(0), | ||
| maxMarks: z.number().min(1).optional(), | ||
| term: z.string().optional(), | ||
| }).refine( | ||
| (data) => !data.maxMarks || data.marks <= data.maxMarks, | ||
| (data) => data.marks <= (data.maxMarks ?? 100), | ||
| { | ||
| message: 'marks must be less than or equal to maxMarks', | ||
| path: ['marks'], | ||
|
|
@@ -40,6 +45,10 @@ export async function GET(req: NextRequest) { | |
| const studentId = searchParams.get('studentId') | ||
| const subject = searchParams.get('subject') | ||
|
|
||
| if (studentId && !mongoose.Types.ObjectId.isValid(studentId)) { | ||
| return NextResponse.json({ error: 'Invalid studentId' }, { status: 400 }) | ||
| } | ||
|
|
||
| const query: Record<string, unknown> = { teacherId: userId } | ||
| if (studentId) query.studentId = studentId | ||
| if (subject) query.subject = subject | ||
|
|
@@ -48,7 +57,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 }) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -70,19 +79,19 @@ 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: calcGrade(data.marks, max) } }, | ||
| { upsert: true, new: true, runValidators: true, context: 'query', setDefaultsOnInsert: true } | ||
| ) | ||
| 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 }) | ||
| return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 251
🏁 Script executed:
Repository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 66
🏁 Script executed:
Repository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 512
🏁 Script executed:
Repository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 101
🏁 Script executed:
Repository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 150
🏁 Script executed:
Repository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 66
🏁 Script executed:
Repository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 483
🏁 Script executed:
Repository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 2613
🏁 Script executed:
Repository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 583
🌐 Web query:
Zod 4.3.6 date validation api z.iso.date() z.iso.datetime()💡 Result:
In Zod 4.3.6, the date validation API includes z.iso.date for validating ISO 8601 date strings in YYYY-MM-DD format and z.iso.datetime for ISO 8601 datetime strings. Usage examples: import { z } from "zod"; const dateSchema = z.iso.date; dateSchema.parse("2020-01-01"); // ✅ valid dateSchema.parse("2020-1-1"); // ❌ invalid const datetimeSchema = z.iso.datetime; datetimeSchema.parse("2020-01-01T06:15:00Z"); // ✅ valid datetimeSchema.parse("2020-01-01T06:15:00.123Z"); // ✅ valid (with milliseconds) These are string validators using regex-based checks for ISO formats. They were introduced in Zod v4 (stable release announced recently, with v4.3.6 being a patch release). Ensure standard import import { z } from "zod"; and Zod v4+ installation. TypeScript recognizes these in v4.3.6 when properly installed. Note: z.date validates Date objects, while z.iso.date and z.iso.datetime validate strings. For Date coercion from strings, use z.coerce.date or codecs like z.codec(z.iso.datetime, z.date).
Citations:
🏁 Script executed:
Repository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 1777
Tighten
deadlinevalidation beyondDate.parse.Date.parseaccepts permissive inputs: date rollover (e.g., '2026-02-31' → Mar 3), numeric strings ('1' → 2001-01-01), and other ambiguous formats. Use Zod's stricter ISO validators.🛡️ Proposed fix
If this endpoint intentionally accepts full datetimes too:
This also applies to the create route at
app/api/assignments/route.ts(lines 12–14).📝 Committable suggestion
🤖 Prompt for AI Agents