fix(web): exclude large-upload route from auth middleware (10MB body cap)
The real cause of the failed 584MB album upload: Next.js caps the request body at 10MB for any route that passes through middleware, and the import route went through the auth gate. Exclude /api/library/import from the middleware matcher so the body streams uncapped to busboy, and authenticate it inline instead (new lib/auth.isAuthed reads + verifies the session cookie from the request). web 161 tests, tsc + build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { pipeline } from "node:stream/promises";
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import Busboy from "busboy";
|
import Busboy from "busboy";
|
||||||
import { prisma } from "@/lib/db";
|
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
|
// 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
|
// 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) {
|
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") ?? "";
|
const ct = request.headers.get("content-type") ?? "";
|
||||||
if (!ct.includes("multipart/form-data") || !request.body) {
|
if (!ct.includes("multipart/form-data") || !request.body) {
|
||||||
return Response.json({ error: "expected multipart form data" }, { status: 400 });
|
return Response.json({ error: "expected multipart form data" }, { status: 400 });
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ export async function expectedToken(): Promise<string> {
|
|||||||
return token;
|
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<boolean> {
|
||||||
|
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). */
|
/** Constant-time string compare (both operands are our own derived tokens). */
|
||||||
export function safeEqual(a: string, b: string): boolean {
|
export function safeEqual(a: string, b: string): boolean {
|
||||||
if (a.length !== b.length) return false;
|
if (a.length !== b.length) return false;
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export async function middleware(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
// Run on everything except Next internals and static assets (which carry no secrets).
|
// Run on everything except Next internals and static assets (which carry no secrets), plus
|
||||||
matcher: ["/((?!_next/static|_next/image|favicon.ico|icon.svg|fonts/).*)"],
|
// /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).*)"],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user