// middleware.ts import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; const LAUNCH_ONLY_ROOT = process.env.NEXT_PUBLIC_LAUNCH_ONLY_ROOT === "true"; // ✅ Forever allow list (always reachable even during launch-only mode) const ALWAYS_ALLOW = new Set([ "/", "/favicon.ico", "/robots.txt", "/sitemap.xml", ]); const PUBLIC_FILE = /\.(?:svg|png|jpg|jpeg|gif|webp|ico|css|js|map|txt|xml|json|woff|woff2|ttf|eot)$/i; export function middleware(req: NextRequest) { const { pathname } = req.nextUrl; console.log("LAUNCH_ONLY_ROOT:", process.env.NEXT_PUBLIC_LAUNCH_ONLY_ROOT); // ========================= // 1) Admin gate (always on) // ========================= if (pathname.startsWith("/admin")) { const token = req.cookies.get("bb_access_token")?.value; const role = req.cookies.get("bb_role")?.value; // optional const ok = !!token && (!role || role === "ADMIN"); if (!ok) { const url = req.nextUrl.clone(); url.pathname = "/login"; url.searchParams.set("next", pathname); return NextResponse.redirect(url); } return NextResponse.next(); } // ===================================== // 2) Launch-only gate (your existing one) // ===================================== if (!LAUNCH_ONLY_ROOT) return NextResponse.next(); // ✅ Always allow Next internals + static assets if ( pathname.startsWith("/_next") || pathname.startsWith("/favicon") || pathname === "/robots.txt" || pathname === "/sitemap.xml" || PUBLIC_FILE.test(pathname) ) { return NextResponse.next(); } if (ALWAYS_ALLOW.has(pathname)) return NextResponse.next(); return NextResponse.rewrite(new URL("/404", req.url)); } ` 1` export const config = { matcher: [ // run on everything except Next internals + obvious static files "/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|css|js|map|txt|xml|json|woff|woff2|ttf|eot)).*)", ], };