diff --git a/src/app/api/upload/route.test.ts b/src/app/api/upload/route.test.ts index 36276c02..3d594306 100644 --- a/src/app/api/upload/route.test.ts +++ b/src/app/api/upload/route.test.ts @@ -365,4 +365,53 @@ describe("POST /api/upload", () => { expect(response.status).toBe(400) }) }) + + // 3B.3: services bucket ownership (entityId must match session.providerId) + describe("3B.3 services bucket ownership", () => { + it("T6: services upload with another provider's UUID is rejected with 403", async () => { + vi.mocked(auth).mockResolvedValue(mockProviderSession) + vi.mocked(prisma.upload.create).mockResolvedValue({ id: "upload-x" } as never) + + const otherProviderId = "a0000000-0000-4000-a000-000000000099" + const request = createMockUploadRequest( + { bucket: "services", entityId: otherProviderId }, + { name: "photo.jpg", type: "image/jpeg", size: 1024 } + ) + + const response = await POST(request) + expect(response.status).toBe(403) + const data = await response.json() + expect(data.error).toBe("Åtkomst nekad") + }) + + it("T7: services upload with provider's own providerId returns 201", async () => { + vi.mocked(auth).mockResolvedValue(mockProviderSession) + vi.mocked(prisma.upload.create).mockResolvedValue({ + id: "upload-services-1", + url: "https://storage.example.com/services/test.jpg", + path: "services/test.jpg", + } as never) + + const request = createMockUploadRequest( + { bucket: "services", entityId: "a0000000-0000-4000-a000-000000000002" }, + { name: "photo.jpg", type: "image/jpeg", size: 1024 } + ) + + const response = await POST(request) + expect(response.status).toBe(201) + }) + + it("T8: services upload from customer session (no providerId) is rejected with 403", async () => { + vi.mocked(auth).mockResolvedValue(mockSession) + + const someProviderId = "a0000000-0000-4000-a000-000000000002" + const request = createMockUploadRequest( + { bucket: "services", entityId: someProviderId }, + { name: "photo.jpg", type: "image/jpeg", size: 1024 } + ) + + const response = await POST(request) + expect(response.status).toBe(403) + }) + }) }) diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index eec09b81..66085e09 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -145,8 +145,17 @@ export async function POST(request: NextRequest) { { status: 400 } ) } + } else if (bucket === "services") { + // 3B.3: providerId namespace — entityId MUST equal session's own providerId. + // Customers (no providerId) and other providers (different providerId) → 403. + const sessionProviderId = session.user.providerId + if (!sessionProviderId || entityId !== sessionProviderId) { + return NextResponse.json( + { error: "Åtkomst nekad" }, + { status: 403 } + ) + } } - // "services" bucket - provider ownership checked via providerId // Generate unique filename — extension is derived from validated MIME // type, NOT from file.name, to prevent path traversal.