import { NextResponse, type NextRequest } from "next/server"; import { extractClientIpAsync, safeRedirect } from "./security"; import { runWithStore, createStore, getRequestId } from "./request-context"; import { logger } from "@/lib/logger"; type MiddlewareHandler = (req: NextRequest) => Promise; export function chain(...handlers: MiddlewareHandler[]): MiddlewareHandler { return async (req: NextRequest) => { for (const handler of handlers) { const result = await handler(req); if (result) return result; } return NextResponse.next(); }; } export function withRequestContext(handler: MiddlewareHandler): MiddlewareHandler { return async (req: NextRequest) => { const ip = await extractClientIpAsync(); const store = createStore(ip); return runWithStore(store, async () => { const start = Date.now(); try { const result = await handler(req); const duration = Date.now() - start; logger.info("Request completed", { method: req.method, path: req.nextUrl.pathname, status: result?.status ?? 200, durationMs: duration, requestId: getRequestId(), }); return result; } catch (error) { const duration = Date.now() - start; logger.error("Request failed", { method: req.method, path: req.nextUrl.pathname, durationMs: duration, requestId: getRequestId(), error: error instanceof Error ? error.message : String(error), }); const safeUrl = new URL(safeRedirect(req.nextUrl.pathname, "/"), req.url); return NextResponse.redirect(safeUrl); } }); }; } export function protectAdminRoutes(req: NextRequest): NextResponse | null { const { pathname } = req.nextUrl; if (!pathname.startsWith("/admin")) return null; const authToken = req.cookies.get("next-auth.session-token")?.value ?? req.cookies.get("__Secure-next-auth.session-token")?.value; if (!authToken) { const loginUrl = new URL("/login", req.url); loginUrl.searchParams.set("callbackUrl", pathname); return NextResponse.redirect(loginUrl); } return null; } export function addSecurityHeaders(req: NextRequest): NextResponse | null { if (req.method === "OPTIONS") return null; const response = NextResponse.next(); const csp = [ "default-src 'self'", "script-src 'self' 'unsafe-eval' 'unsafe-inline'", "style-src 'self' 'unsafe-inline'", "img-src 'self' data: https: http:", "font-src 'self' https:", "connect-src 'self' https: wss:", "frame-src 'self'", "object-src 'none'", "base-uri 'self'", "form-action 'self'", ].join("; "); response.headers.set("Content-Security-Policy", csp); response.headers.set("X-Content-Type-Options", "nosniff"); response.headers.set("X-Frame-Options", "DENY"); response.headers.set("X-XSS-Protection", "0"); response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin"); response.headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=()"); return response; }