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
105 changes: 70 additions & 35 deletions app/api/announcements/route.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,102 @@
import { auth } from '@clerk/nextjs/server'
import { NextRequest, NextResponse } from 'next/server'
import { connectDB } from '@/lib/mongodb'
import { Announcement } from '@/models/Announcement'
import { z } from 'zod'
import { auth } from "@clerk/nextjs/server";
import { NextRequest, NextResponse } from "next/server";
import { connectDB } from "@/lib/mongodb";
import { Announcement } from "@/models/Announcement";
import { z } from "zod";

const AnnouncementSchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
audience: z.string().optional(),
category: z.enum(['academic', 'events', 'admin', 'general']).optional(),
title: z.string().trim().min(1),
content: z.string().trim().min(1),
Comment on lines +8 to +9
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n app/api/announcements/route.ts | head -50

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 1935


🏁 Script executed:

find . -type f -name "*.ts" -o -name "*.tsx" -o -name "*.js" | xargs grep -l "AnnouncementSchema\|announcement.*schema" 2>/dev/null | head -10

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 141


🏁 Script executed:

rg "max\(" app/api/announcements/ -A 2 -B 2

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 66


🏁 Script executed:

rg "title.*max|content.*max" --type ts --type tsx -i

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 111


🏁 Script executed:

find . -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.md" \) -exec grep -l "announcement" {} \; 2>/dev/null | head -15

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 290


🏁 Script executed:

cat -n app/api/announcements/route.ts | tail -n +50 | head -80

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 1900


🏁 Script executed:

cat -n models/Announcement.ts

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 1139


🏁 Script executed:

cat -n app/dashboard/announcements/AnnouncementsClient.tsx | head -150

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 5326


🏁 Script executed:

rg "maxLength|max.*content|max.*title" --type ts -i

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 159


🏁 Script executed:

cat -n app/dashboard/announcements/AnnouncementsClient.tsx | tail -n +150 | head -200

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 8070


🏁 Script executed:

rg "register.*title|register.*content" app/dashboard/announcements/ -A 3 -B 1

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 991


🏁 Script executed:

grep -n "maxLength\|placeholder" app/dashboard/announcements/AnnouncementsClient.tsx

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 248


Add backend length constraints for title and content fields.

The schema currently accepts min(1) with no maximum, allowing arbitrarily large payloads. Client validation can be bypassed by directly calling the API, so server-side limits are essential.

🛡️ Proposed schema update
-  title: z.string().trim().min(1),
-  content: z.string().trim().min(1),
+  title: z.string().trim().min(1).max(120),
+  content: z.string().trim().min(1).max(5000),

Confirm the exact product limits for title and content length before merging.

📝 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
title: z.string().trim().min(1),
content: z.string().trim().min(1),
title: z.string().trim().min(1).max(120),
content: z.string().trim().min(1).max(5000),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/announcements/route.ts` around lines 8 - 9, The Zod schema in
app/api/announcements/route.ts currently allows unlimited lengths for title and
content; update the announcement validation schema (the Zod object that defines
title and content) to enforce server-side maximum lengths by changing title:
z.string().trim().min(1) to z.string().trim().min(1).max(<TITLE_MAX>) and
content: z.string().trim().min(1) to
z.string().trim().min(1).max(<CONTENT_MAX>), replacing <TITLE_MAX> and
<CONTENT_MAX> with the confirmed product limits before merging so the API
rejects overly large payloads.

audience: z.enum(["All", "Students", "Staff"]).optional(),
category: z.enum(["academic", "events", "admin", "general"]).optional(),
pinned: z.boolean().optional(),
})
});

export async function GET(req: NextRequest) {
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 {
await connectDB()
const { searchParams } = new URL(req.url)
await connectDB();
const { searchParams } = new URL(req.url);

// Parse and validate limit
const limitStr = searchParams.get('limit') ?? '50'
let limit = parseInt(limitStr, 10)
const limitStr = searchParams.get("limit") ?? "50";
let limit = parseInt(limitStr, 10);
if (!Number.isFinite(limit) || limit <= 0) {
limit = 50
limit = 50;
}
limit = Math.min(limit, 100) // Cap at 100
limit = Math.min(limit, 100); // Cap at 100

const announcements = await Announcement.find({ teacherId: userId })
.sort({ pinned: -1, createdAt: -1 })
.limit(limit)
.lean()
.lean();

return NextResponse.json(announcements)
return NextResponse.json(announcements);
} catch (error) {
console.error('GET /api/announcements error:', error instanceof Error ? error.message : error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
console.error(
"GET /api/announcements error:",
error instanceof Error ? error.message : error,
);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}

export async function POST(req: NextRequest) {
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 {
await connectDB()
let body
await connectDB();
let body;
try {
body = await req.json()
body = await req.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 })
return NextResponse.json(
{ error: "Invalid JSON in request body" },
{ status: 400 },
);
}

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

const announcement = await Announcement.create({ ...parsed.data, teacherId: userId })
return NextResponse.json(announcement, { status: 201 })
const recentCount = await Announcement.countDocuments({
teacherId: userId,
createdAt: { $gte: new Date(Date.now() - 60 * 1000) },
});

if (recentCount > 10) {
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
Comment on lines +67 to +73
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

Fix the rate-limit off-by-one.

With recentCount > 10, the 11th announcement in a minute is still allowed. Use >= 10 if the intended cap is 10 per minute.

This remains a soft throttle because countDocuments + create is non-atomic; use an atomic counter/token bucket if strict enforcement matters.

🐛 Proposed off-by-one fix
-    if (recentCount > 10) {
+    if (recentCount >= 10) {
       return NextResponse.json({ error: "Too many requests" }, { status: 429 });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/announcements/route.ts` around lines 67 - 73, The rate-limit check
uses recentCount > 10 which allows an 11th announcement; update the condition in
the request handler where recentCount is computed (the
Announcement.countDocuments call and the subsequent if) to use >= 10 so that
creating the 11th item is blocked when the intended cap is 10 per minute, and
consider replacing the non-atomic countDocuments + create flow with an atomic
counter/token-bucket later if strict enforcement is required.

}

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

const announcement = await Announcement.create({
title: data.title,
content: data.content,
audience: data.audience ?? "all",
category: data.category ?? "general",
pinned: data.pinned ?? false,
Comment on lines +84 to +89
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 the default audience consistent with validation.

Line 87 stores "all", but the schema accepts "All" | "Students" | "Staff" and the model default is "All". Omitted audience values will be saved in a form the API itself rejects later.

🐛 Proposed casing fix
-      audience: data.audience ?? "all",
+      audience: data.audience ?? "All",
📝 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 announcement = await Announcement.create({
title: data.title,
content: data.content,
audience: data.audience ?? "all",
category: data.category ?? "general",
pinned: data.pinned ?? false,
const announcement = await Announcement.create({
title: data.title,
content: data.content,
audience: data.audience ?? "All",
category: data.category ?? "general",
pinned: data.pinned ?? false,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/announcements/route.ts` around lines 84 - 89, The announcement
creation uses audience: data.audience ?? "all" which mismatches the
validation/model allowed values ("All" | "Students" | "Staff") and the model
default "All"; update the create call in Announcement.create to either omit the
audience property when data.audience is undefined or set it to the
correctly-cased default "All" (e.g., use data.audience ?? "All") so stored
records match the schema and model defaults; locate this change around the
Announcement.create invocation to adjust the audience assignment.

teacherId: userId,
});
return NextResponse.json(announcement, { status: 201 });
} catch (error) {
if (error instanceof Error) {
console.error('POST /api/announcements error:', error.message)
console.error("POST /api/announcements error:", error.message);
}
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}
137 changes: 83 additions & 54 deletions app/api/assignments/route.ts
Original file line number Diff line number Diff line change
@@ -1,91 +1,120 @@
import { auth } from '@clerk/nextjs/server'
import { NextRequest, NextResponse } from 'next/server'
import { connectDB } from '@/lib/mongodb'
import { Assignment } from '@/models/Assignment'
import { z } from 'zod'
import { auth } from "@clerk/nextjs/server";
import { NextRequest, NextResponse } from "next/server";
import { connectDB } from "@/lib/mongodb";
import { Assignment } from "@/models/Assignment";
import { z } from "zod";

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),
maxMarks: z.number().min(1).optional(),
status: z.enum(['active', 'closed']).optional(),
kanbanStatus: z.enum(['todo', 'in_progress', 'submitted']).optional(),
})
title: z.string().trim().min(1),
description: z.string().trim().optional(),
subject: z.string().trim().min(1),
class: z.string().trim().min(1),
deadline: z.coerce.date(),
maxMarks: z.coerce.number().min(1).optional(),
status: z.enum(["active", "closed"]).default("active"),
kanbanStatus: z.enum(["todo", "in_progress", "submitted"]).default("todo"),
});

export async function GET(req: NextRequest) {
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 {
await connectDB()
const { searchParams } = new URL(req.url)
const status = searchParams.get('status')
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
const pageStr = searchParams.get("page") ?? "1";
const limitStr = searchParams.get("limit") ?? "20";

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

const query: Record<string, unknown> = { teacherId: userId }
if (status) query.status = status
if (!Number.isFinite(page) || page < 1) page = 1;
if (!Number.isFinite(limit) || limit < 1) limit = 20;
limit = Math.min(limit, 100); // Cap at 100

const query: Record<string, unknown> = { teacherId: userId };
if (status && !["active", "closed"].includes(status)) {
return NextResponse.json({ error: "Invalid status" }, { status: 400 });
}
if (status) query.status = status;

const skip = (page - 1) * limit
const skip = (page - 1) * limit;
const assignments = await Assignment.find(query)
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit)
.lean()

const total = await Assignment.countDocuments(query)
.lean();

return NextResponse.json({ assignments, total, page, pages: Math.ceil(total / limit) })
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)
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 },
);
}
}

export async function POST(req: NextRequest) {
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 {
await connectDB()
let body
await connectDB();

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

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

const parsed = AssignmentSchema.safeParse(body);
if (!parsed.success)
return NextResponse.json(
{ error: parsed.error.flatten() },
{ status: 400 },
);
Comment on lines +89 to +94
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
# Check whether a shared Zod error formatter exists in the repo and how the other routes surface validation errors.
fd -t f 'formatZodError' -x cat {}
rg -nP '\b(flatten|flattenError|treeifyError|formatZodError)\b' --type=ts -C2

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 3506


🏁 Script executed:

cat -n lib/utils.ts | head -20

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 605


🏁 Script executed:

# Check what Zod version is being used
cat package.json | grep -A2 -B2 '"zod"'

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 191


🌐 Web query:

Zod 4 migration guide .flatten() deprecated z.flattenError

💡 Result:

In Zod 4, the .flatten method on ZodError has been deprecated. Replace it with the top-level z.flattenError(error) function. z.flattenError returns an object with: - formErrors: array of top-level errors - fieldErrors: object mapping field names to arrays of error messages Example: const flattened = z.flattenError(result.error); console.log(flattened.fieldErrors.username); // ['Expected string, received number'] Note: There is no z.flattenError type or function mentioned in the docs. The replacement is z.flattenError, which produces a flattened structure suitable for form handling. For nested schemas, use z.treeifyError instead. The official migration guide confirms: ".flatten The .flatten method on ZodError has also been deprecated. Instead use the top-level z.treeifyError function. Read the Formatting errors docs for more information." But the formatting docs clarify z.flattenError as the direct functional equivalent for flat errors.

Citations:


🏁 Script executed:

head -30 app/api/assignments/route.ts

Repository: JavaScript-Mastery-Pro/discord-challenge

Length of output: 1154


Replace deprecated parsed.error.flatten() with z.flattenError(parsed.error).

In Zod 4, the .flatten() instance method on ZodError is deprecated. Use the top-level z.flattenError() function instead—both produce identical output structure with formErrors and fieldErrors properties.

Proposed fix
    const parsed = AssignmentSchema.safeParse(body);
    if (!parsed.success)
      return NextResponse.json(
-        { error: parsed.error.flatten() },
+        { error: z.flattenError(parsed.error) },
         { 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 89 - 94, The code uses the
deprecated instance method parsed.error.flatten(); update the error flattening
to the Zod 4 top-level function by replacing parsed.error.flatten() with
z.flattenError(parsed.error). Locate the AssignmentSchema.safeParse(...) usage
and the subsequent NextResponse.json(...) call and pass
z.flattenError(parsed.error) as the error payload so the response continues to
return the same formErrors and fieldErrors structure.


try {
const assignment = await Assignment.create({ ...parsed.data, teacherId: userId })
return NextResponse.json(assignment, { status: 201 })
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)
console.error("Assignment.create error:", dbError.message);
}
return NextResponse.json({ error: 'Failed to create assignment' }, { status: 500 })
return NextResponse.json(
{ error: "Failed to create assignment" },
{ status: 500 },
);
}
} catch (error) {
if (error instanceof Error) {
console.error('POST /api/assignments error:', error.message)
console.error("POST /api/assignments error:", error.message);
}
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}
Loading