From 7a3b589bcd06cfbdcb394ab9e96637dcca4e3e39 Mon Sep 17 00:00:00 2001 From: jsdavid278-cyber Date: Sat, 13 Jun 2026 16:05:43 -0600 Subject: [PATCH] Handle malformed affiliate payout JSON --- .../offers/[id]/conversions/pay/route.test.ts | 30 +++++++++++++++++++ .../offers/[id]/conversions/pay/route.ts | 30 ++++++++++++------- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/app/api/affiliates/offers/[id]/conversions/pay/route.test.ts b/src/app/api/affiliates/offers/[id]/conversions/pay/route.test.ts index 0eacf737..45a56a1b 100644 --- a/src/app/api/affiliates/offers/[id]/conversions/pay/route.test.ts +++ b/src/app/api/affiliates/offers/[id]/conversions/pay/route.test.ts @@ -32,6 +32,17 @@ function makePostRequest(id: string, body: Record) { ); } +function makeRawPostRequest(id: string, body: string) { + return new NextRequest( + `http://localhost/api/affiliates/offers/${id}/conversions/pay`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body, + } + ); +} + function makeParams(id: string) { return { params: Promise.resolve({ id }) }; } @@ -54,6 +65,25 @@ describe("POST /api/affiliates/offers/[id]/conversions/pay", () => { vi.clearAllMocks(); }); + it("rejects malformed JSON before querying the affiliate offer", async () => { + mockGetAuthContext.mockResolvedValue({ + user: { id: "seller-1", authMethod: "session" }, + }); + + const res = await POST( + makeRawPostRequest("offer-1", "{not valid json"), + makeParams("offer-1") + ); + + expect(res.status).toBe(400); + await expect(res.json()).resolves.toEqual({ + error: "Invalid JSON body", + }); + expect(mockFrom).not.toHaveBeenCalled(); + expect(mockGetUserLnWallet).not.toHaveBeenCalled(); + expect(mockInternalTransfer).not.toHaveBeenCalled(); + }); + it("rejects non-string conversion_id before querying conversions (#422)", async () => { mockGetAuthContext.mockResolvedValue({ user: { id: "seller-1", authMethod: "session" }, diff --git a/src/app/api/affiliates/offers/[id]/conversions/pay/route.ts b/src/app/api/affiliates/offers/[id]/conversions/pay/route.ts index e2b37652..74847dd3 100644 --- a/src/app/api/affiliates/offers/[id]/conversions/pay/route.ts +++ b/src/app/api/affiliates/offers/[id]/conversions/pay/route.ts @@ -21,6 +21,25 @@ export async function POST( return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } + let body: unknown; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); + } + const conversion_id = + body && typeof body === "object" && !Array.isArray(body) + ? (body as Record).conversion_id + : undefined; + + if (typeof conversion_id !== "string" || conversion_id.trim() === "") { + return NextResponse.json( + { error: "conversion_id must be a non-empty string" }, + { status: 400 } + ); + } + const conversionId = conversion_id.trim(); + const admin = createServiceClient(); // Verify seller ownership @@ -34,17 +53,6 @@ export async function POST( return NextResponse.json({ error: "Not authorized" }, { status: 403 }); } - const body = await request.json(); - const { conversion_id } = body; - - if (typeof conversion_id !== "string" || conversion_id.trim() === "") { - return NextResponse.json( - { error: "conversion_id must be a non-empty string" }, - { status: 400 } - ); - } - const conversionId = conversion_id.trim(); - // Get the conversion const { data: conv } = await (admin as AnySupabase) .from("affiliate_conversions")