Skip to content
Merged
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
53 changes: 52 additions & 1 deletion lib/artists/__tests__/validateDeleteArtistRequest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe("validateDeleteArtistRequest", () => {
const result = await validateDeleteArtistRequest(request, validArtistId);

expect(result).toBe(authError);
expect(validateAuthContext).toHaveBeenCalledWith(request);
expect(validateAuthContext).toHaveBeenCalledWith(request, { accountId: undefined });
});

it("returns 404 when the artist does not exist", async () => {
Expand Down Expand Up @@ -125,4 +125,55 @@ describe("validateDeleteArtistRequest", () => {
requesterAccountId: authenticatedAccountId,
});
});

describe("account_id override", () => {
const overrideAccountId = "770e8400-e29b-41d4-a716-446655440000";

it("passes the account_id override to validateAuthContext and resolves against it", async () => {
vi.mocked(validateAuthContext).mockResolvedValue({
accountId: overrideAccountId,
authToken: "test-token",
orgId: null,
});
vi.mocked(selectAccounts).mockResolvedValue([{ id: validArtistId }] as never);
vi.mocked(checkAccountArtistAccess).mockResolvedValue(true);

const request = new NextRequest(`http://localhost/api/artists/${validArtistId}`, {
method: "DELETE",
headers: {
Authorization: "Bearer test-token",
"Content-Type": "application/json",
},
body: JSON.stringify({ account_id: overrideAccountId }),
});

const result = await validateDeleteArtistRequest(request, validArtistId);

expect(validateAuthContext).toHaveBeenCalledWith(request, {
accountId: overrideAccountId,
});
expect(checkAccountArtistAccess).toHaveBeenCalledWith(overrideAccountId, validArtistId);
expect(result).toEqual({
artistId: validArtistId,
requesterAccountId: overrideAccountId,
});
});

it("returns 400 when account_id is not a valid UUID", async () => {
const request = new NextRequest(`http://localhost/api/artists/${validArtistId}`, {
method: "DELETE",
headers: {
Authorization: "Bearer test-token",
"Content-Type": "application/json",
},
body: JSON.stringify({ account_id: "not-a-uuid" }),
});

const result = await validateDeleteArtistRequest(request, validArtistId);

expect(result).toBeInstanceOf(NextResponse);
expect((result as NextResponse).status).toBe(400);
expect(validateAuthContext).not.toHaveBeenCalled();
});
});
});
27 changes: 26 additions & 1 deletion lib/artists/validateDeleteArtistRequest.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { validateAccountParams } from "@/lib/accounts/validateAccountParams";
import { validateAuthContext } from "@/lib/auth/validateAuthContext";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { safeParseJson } from "@/lib/networking/safeParseJson";
import { checkAccountArtistAccess } from "@/lib/artists/checkAccountArtistAccess";
import { selectAccounts } from "@/lib/supabase/accounts/selectAccounts";

Expand All @@ -10,9 +12,17 @@ export interface DeleteArtistRequest {
requesterAccountId: string;
}

const deleteArtistBodySchema = z.object({
account_id: z.string().uuid("account_id must be a valid UUID").optional(),
});

/**
* Validates DELETE /api/artists/{id} path params and authentication.
*
* Accepts an optional `account_id` in the request body so a caller with access
* to multiple accounts (org members or Recoup admins) can delete an artist in
* another account's context. The override is authorized by `validateAuthContext`.
*
* @param request - The incoming request
* @param id - The artist account ID from the route
* @returns The validated artist ID plus requester context, or a NextResponse error
Expand All @@ -26,7 +36,22 @@ export async function validateDeleteArtistRequest(
return validatedParams;
}

const authResult = await validateAuthContext(request);
const body = await safeParseJson(request);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Malformed JSON request bodies bypass validation and proceed as empty input. This can ignore a caller’s intended account_id override and run deletion under the default auth context.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/artists/validateDeleteArtistRequest.ts, line 39:

<comment>Malformed JSON request bodies bypass validation and proceed as empty input. This can ignore a caller’s intended `account_id` override and run deletion under the default auth context.</comment>

<file context>
@@ -26,7 +36,22 @@ export async function validateDeleteArtistRequest(
   }
 
-  const authResult = await validateAuthContext(request);
+  const body = await safeParseJson(request);
+  const bodyResult = deleteArtistBodySchema.safeParse(body);
+  if (!bodyResult.success) {
</file context>

const bodyResult = deleteArtistBodySchema.safeParse(body);
if (!bodyResult.success) {
const firstError = bodyResult.error.issues[0];
return NextResponse.json(
{
status: "error",
error: firstError.message,
},
{ status: 400, headers: getCorsHeaders() },
);
}

const authResult = await validateAuthContext(request, {
accountId: bodyResult.data.account_id,
});
if (authResult instanceof NextResponse) {
return authResult;
}
Expand Down
Loading