import { NextRequest, NextResponse } from "next/server"; import { getToken } from "next-auth/jwt"; import { rateLimit } from "@/lib/rate-limit"; export async function proxy(request: NextRequest) { const pathname = request.nextUrl.pathname; // ── ADMIN GUARD ────────────────────────────────────────────────────────── if (pathname.startsWith("/admin")) { // Allow the login page and NextAuth API routes through without session check if ( pathname === "/admin/login" || pathname.startsWith("/api/auth") ) { return NextResponse.next(); } const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET, }); if (!token) { const loginUrl = new URL("/admin/login", request.url); loginUrl.searchParams.set("callbackUrl", pathname); return NextResponse.redirect(loginUrl); } return NextResponse.next(); } // ── CLIENT TOKEN/SLUG GUARD ────────────────────────────────────────────── if (pathname.startsWith("/client/")) { const slugOrTokenMatch = pathname.match(/^\/client\/([a-zA-Z0-9_-]+)/); if (!slugOrTokenMatch) { return NextResponse.rewrite(new URL("/not-found", request.url)); } const slugOrToken = slugOrTokenMatch[1]; try { // Use localhost to avoid hairpin NAT issues in Docker. // request.url is the external hostname (via Traefik); inside the container the app is on localhost. const port = process.env.PORT ?? "3000"; const base = `http://localhost:${port}`; const internalHeaders = { "x-internal-secret": process.env.INTERNAL_SECRET ?? "", }; // Try slug first (D-06) — user-friendly slugs before token fallback let res = await fetch( `${base}/api/internal/validate-slug?slug=${encodeURIComponent(slugOrToken)}`, { headers: internalHeaders } ); // If slug not found, fall back to token validation (existing links continue to work) if (!res.ok) { res = await fetch( `${base}/api/internal/validate-token?token=${encodeURIComponent(slugOrToken)}`, { headers: internalHeaders } ); } if (!res.ok) { return NextResponse.rewrite(new URL("/not-found", request.url)); } return NextResponse.next(); } catch { return NextResponse.rewrite(new URL("/not-found", request.url)); } } // ── PUBLIC QUOTE ROUTES (rate limited) ─────────────────────────────────── if (pathname.match(/^\/quote\/[a-zA-Z0-9_-]{21}\/?$/)) { const ip = request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip") || "unknown"; const allowed = rateLimit(ip, 3, 60 * 1000); // 3 views per minute if (!allowed) { return NextResponse.json( { error: "Troppi accessi. Riprova tra un minuto." }, { status: 429 } ); } } return NextResponse.next(); } export const config = { matcher: ["/admin/:path*", "/client/:path*", "/quote/:path*"], };