-
Notifications
You must be signed in to change notification settings - Fork 23
fix profile api #13
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?
fix profile api #13
Changes from all commits
79d70ef
70a35dc
d557c22
0e47069
b5f7343
e078598
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 | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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), | ||||||||||||||||||||||||||
| 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
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. Fix the rate-limit off-by-one. With This remains a soft throttle because 🐛 Proposed off-by-one fix- if (recentCount > 10) {
+ if (recentCount >= 10) {
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| 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
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. Keep the default audience consistent with validation. Line 87 stores 🐛 Proposed casing fix- audience: data.audience ?? "all",
+ audience: data.audience ?? "All",📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| 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 }, | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| 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
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
# 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 -C2Repository: JavaScript-Mastery-Pro/discord-challenge Length of output: 3506 🏁 Script executed: cat -n lib/utils.ts | head -20Repository: 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:
💡 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.tsRepository: JavaScript-Mastery-Pro/discord-challenge Length of output: 1154 Replace deprecated In Zod 4, the 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 |
||
|
|
||
| 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 }, | ||
| ); | ||
| } | ||
| } | ||
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:
cat -n app/api/announcements/route.ts | head -50Repository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 1935
🏁 Script executed:
Repository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 141
🏁 Script executed:
rg "max\(" app/api/announcements/ -A 2 -B 2Repository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 66
🏁 Script executed:
rg "title.*max|content.*max" --type ts --type tsx -iRepository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 111
🏁 Script executed:
Repository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 290
🏁 Script executed:
Repository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 1900
🏁 Script executed:
Repository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 1139
🏁 Script executed:
cat -n app/dashboard/announcements/AnnouncementsClient.tsx | head -150Repository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 5326
🏁 Script executed:
rg "maxLength|max.*content|max.*title" --type ts -iRepository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 159
🏁 Script executed:
Repository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 8070
🏁 Script executed:
rg "register.*title|register.*content" app/dashboard/announcements/ -A 3 -B 1Repository: JavaScript-Mastery-Pro/discord-challenge
Length of output: 991
🏁 Script executed:
grep -n "maxLength\|placeholder" app/dashboard/announcements/AnnouncementsClient.tsxRepository: 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
Confirm the exact product limits for title and content length before merging.
📝 Committable suggestion
🤖 Prompt for AI Agents