diff --git a/web/src/app/api/library/import/route.ts b/web/src/app/api/library/import/route.ts index b4e49e6..67656f4 100644 --- a/web/src/app/api/library/import/route.ts +++ b/web/src/app/api/library/import/route.ts @@ -6,6 +6,7 @@ import { pipeline } from "node:stream/promises"; import { randomUUID } from "node:crypto"; import Busboy from "busboy"; import { prisma } from "@/lib/db"; +import { isAuthed } from "@/lib/auth"; // Manually import an album (a ripped CD / files already on the PC): upload the audio (+ an // optional cover) with artist/album/year, and Lyra writes it into the library. The web @@ -25,6 +26,12 @@ function safeName(s: string): string { } export async function POST(request: Request) { + // This route is excluded from the auth middleware (to avoid the 10MB body cap), so it must + // authenticate itself. + if (!(await isAuthed(request))) { + return Response.json({ error: "unauthorized" }, { status: 401 }); + } + const ct = request.headers.get("content-type") ?? ""; if (!ct.includes("multipart/form-data") || !request.body) { return Response.json({ error: "expected multipart form data" }, { status: 400 }); diff --git a/web/src/lib/auth.ts b/web/src/lib/auth.ts index edeb2b6..9c65f69 100644 --- a/web/src/lib/auth.ts +++ b/web/src/lib/auth.ts @@ -37,6 +37,16 @@ export async function expectedToken(): Promise { return token; } +/** Authenticate a request directly (for routes excluded from the auth middleware, e.g. large + * uploads). Returns true when auth is disabled or the request carries a valid session cookie. */ +export async function isAuthed(request: Request): Promise { + const token = await expectedToken(); + if (!token) return true; // auth disabled + const cookie = request.headers.get("cookie") ?? ""; + const m = cookie.match(new RegExp(`(?:^|;\\s*)${AUTH_COOKIE}=([^;]+)`)); + return safeEqual(m ? decodeURIComponent(m[1]) : "", token); +} + /** Constant-time string compare (both operands are our own derived tokens). */ export function safeEqual(a: string, b: string): boolean { if (a.length !== b.length) return false; diff --git a/web/src/middleware.ts b/web/src/middleware.ts index fa40c22..ee1df9f 100644 --- a/web/src/middleware.ts +++ b/web/src/middleware.ts @@ -26,6 +26,8 @@ export async function middleware(req: NextRequest) { } export const config = { - // Run on everything except Next internals and static assets (which carry no secrets). - matcher: ["/((?!_next/static|_next/image|favicon.ico|icon.svg|fonts/).*)"], + // Run on everything except Next internals and static assets (which carry no secrets), plus + // /api/library/import — routing large album uploads through middleware caps the body at + // 10MB, so it's excluded here and authenticates itself inline instead. + matcher: ["/((?!_next/static|_next/image|favicon.ico|icon.svg|fonts/|api/library/import).*)"], };