-
Notifications
You must be signed in to change notification settings - Fork 23
fix: resolve security, logic, and data integrity issues #10
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
c4e1ecc
98eb302
bb1ec67
0058c5e
85d3e73
d902424
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 |
|---|---|---|
|
|
@@ -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) { | ||
|
|
@@ -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 | ||
|
|
@@ -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 }) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -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
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
# 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() })
NODERepository: 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 -20Repository: JavaScript-Mastery-Pro/discord-challenge Length of output: 139 🏁 Script executed: # Look for AssignmentSchema and deadline validation
rg "AssignmentSchema" --type tsRepository: 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 -100Repository: 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 2Repository: 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:
Use 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 |
||
| } | ||
|
|
||
| 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 }) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) { | ||
|
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
# 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' || trueRepository: JavaScript-Mastery-Pro/discord-challenge Length of output: 2428 Restore The route handler no longer exports 🤖 Prompt for AI Agents |
||
| 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
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
# 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 })
}
NODERepository: JavaScript-Mastery-Pro/discord-challenge Length of output: 259 🏁 Script executed: cat -n app/api/attendance/route.ts | head -60Repository: 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 zodRepository: 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 -20Repository: 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 -20Repository: 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);
}
EOFRepository: 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 -10Repository: 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 -10Repository: 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 -30Repository: 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 -20Repository: JavaScript-Mastery-Pro/discord-challenge Length of output: 664 🌐 Web query:
💡 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 Replace 🤖 Prompt for AI Agents |
||
| } | ||
| 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 } }, | ||
|
|
@@ -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 }) } | ||
| } | ||
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: 385
🏁 Script executed:
rg -n "ALLOWED_FIELDS" app/api/assignments/[id]/route.tsRepository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 292
🏁 Script executed:
cat -n app/api/assignments/[id]/route.ts | head -50Repository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 2419
🏁 Script executed:
cat -n app/api/assignments/route.ts | head -80Repository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 3563
Validate assignment update payloads and enforce deadline consistency.
The
inoperator on line 16 throws whenreq.json()successfully parses to a non-object value (e.g.,null, a string, or a number). Additionally,deadlineis allowed in updates without the past-date validation applied in POST/api/assignments(lines 69–72 ofroute.ts), andfindOneAndUpdateomitsrunValidators: true, bypassing Mongoose schema validation.Proposed fix
🤖 Prompt for AI Agents