diff --git a/src/app/api/affiliates/offers/route.test.ts b/src/app/api/affiliates/offers/route.test.ts index 73a348f3..603e9636 100644 --- a/src/app/api/affiliates/offers/route.test.ts +++ b/src/app/api/affiliates/offers/route.test.ts @@ -170,6 +170,23 @@ describe("POST /api/affiliates/offers", () => { vi.clearAllMocks(); }); + it("returns 400 for malformed JSON before touching affiliate offers", async () => { + mockGetAuthContext.mockResolvedValue({ user: { id: "user1" } }); + + const req = new NextRequest("http://localhost/api/affiliates/offers", { + method: "POST", + body: "{not valid json", + headers: { "Content-Type": "application/json" }, + }); + + const res = await POST(req); + const body = await res.json(); + + expect(res.status).toBe(400); + expect(body.error).toBe("Invalid JSON body"); + expect(mockFrom).not.toHaveBeenCalled(); + }); + it("rejects javascript: URL in product_url (#18)", async () => { mockGetAuthContext.mockResolvedValue({ user: { id: "user1" } }); diff --git a/src/app/api/affiliates/offers/route.ts b/src/app/api/affiliates/offers/route.ts index e82abb0e..46332514 100644 --- a/src/app/api/affiliates/offers/route.ts +++ b/src/app/api/affiliates/offers/route.ts @@ -6,7 +6,7 @@ import { escapePostgrestSearchValue } from "@/lib/security/sanitize"; // eslint-disable-next-line @typescript-eslint/no-explicit-any type AnySupabase = any; -import { validateOfferInput } from "@/lib/affiliates/validation"; +import { validateOfferInput, type OfferInput } from "@/lib/affiliates/validation"; function parsePaginationParam( value: string | null, @@ -28,6 +28,18 @@ function slugify(text: string): string { return slug || "offer"; } +async function readJsonObject(request: NextRequest) { + try { + const body = await request.json(); + if (!body || typeof body !== "object" || Array.isArray(body)) { + return null; + } + return body as Record; + } catch { + return null; + } +} + /** * GET /api/affiliates/offers - List affiliate offers (public marketplace) */ @@ -148,8 +160,11 @@ export async function POST(request: NextRequest) { const rl = checkRateLimit(getRateLimitIdentifier(request, auth.user.id), "write"); if (!rl.allowed) return rateLimitExceeded(rl); - const body = await request.json(); - const validation = validateOfferInput(body); + const body = await readJsonObject(request); + if (!body) { + return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); + } + const validation = validateOfferInput(body as unknown as OfferInput); if (!validation.ok) { return NextResponse.json({ error: validation.errors.join("; ") }, { status: 400 });