diff --git a/frontend/e2e/admin-upload-auth.spec.ts b/frontend/e2e/admin-upload-auth.spec.ts new file mode 100644 index 0000000..29e7575 --- /dev/null +++ b/frontend/e2e/admin-upload-auth.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from "@playwright/test"; + +/** + * Admin upload file-serve auth guard — regression test for GitHub issue #65. + * + * Verifies that `GET /api/admin/upload/{filename}` rejects unauthenticated + * requests with 401 Unauthorized. Before this fix the route returned uploaded + * admin assets to any caller that knew the filename. + * + * Only the unauthenticated boundary is asserted here. The non-admin (403) and + * admin (200) cases depend on seeded NextAuth session state that isn't + * available in the standalone e2e environment used by this suite. + * + * Requires: + * - Next.js dev/preview server on the baseURL defined in playwright.config + * (any AUTH_MODE — the check runs at route entry before mode branching) + */ + +test.describe("Admin upload file-serve — auth guard", () => { + test("unauthenticated GET returns 401 Unauthorized", async ({ request }) => { + const response = await request.get( + "/api/admin/upload/does-not-matter.png", + ); + + expect(response.status()).toBe(401); + await expect(response.json()).resolves.toEqual({ error: "Unauthorized" }); + }); +}); diff --git a/frontend/src/app/api/admin/upload/[filename]/route.ts b/frontend/src/app/api/admin/upload/[filename]/route.ts index 1244ffc..9e6ad78 100644 --- a/frontend/src/app/api/admin/upload/[filename]/route.ts +++ b/frontend/src/app/api/admin/upload/[filename]/route.ts @@ -1,6 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; import { readFile, access, constants } from "fs/promises"; import path from "path"; +import { auth } from "@/lib/auth"; +import { isAdmin } from "@/types/auth-mode"; +import type { UserRole } from "@/types/auth-mode"; // Get upload directory (same as upload route) function getUploadDir(): string { @@ -26,6 +29,14 @@ export async function GET( { params }: { params: Promise<{ filename: string }> }, ) { try { + const session = await auth(); + if (!session?.user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + if (!isAdmin(session.user.role as UserRole)) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + const { filename } = await params; // Security: prevent directory traversal