/** * Next.js middleware — refreshes the Supabase auth session on every request, * so Server Components see the latest user state. * * Skipped on static assets and the static-file API to keep them fast. */ import { NextResponse, type NextRequest } from "next/server"; import { createServerClient, type CookieOptions } from "@supabase/ssr"; import { log, correlationId } from "@/lib/logger"; export async function middleware(request: NextRequest) { const t0 = Date.now(); const url = process.env.NEXT_PUBLIC_SUPABASE_URL; const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; const reqId = correlationId(request); let response = NextResponse.next({ request }); // Stamp every response so downstream handlers and the client see the same id. response.headers.set("x-correlation-id", reqId); if (!url || !key) { // Supabase not configured — skip auth refresh entirely return response; } const supabase = createServerClient(url, key, { cookies: { getAll() { return request.cookies.getAll(); }, setAll(toSet: Array<{ name: string; value: string; options?: CookieOptions }>) { toSet.forEach(({ name, value }) => request.cookies.set(name, value)); response = NextResponse.next({ request }); toSet.forEach(({ name, value, options }) => response.cookies.set(name, value, options)); }, }, }); // Trigger refresh (silently if token still valid) const { data: { user } } = await supabase.auth.getUser(); // Gate /admin/* AND /api/admin/* by role. Non-admin (including anonymous) // gets a public 404, not a redirect — we don't want to leak the existence // of the route. (Audit W0-F1 — fechado 2026-05-23.) const pathname = request.nextUrl.pathname; if (pathname.startsWith("/admin") || pathname.startsWith("/api/admin")) { if (!user) { return new NextResponse("Not Found", { status: 404 }); } const { data: profile } = await supabase .from("profiles") .select("role") .eq("id", user.id) .maybeSingle(); if (profile?.role !== "admin") { return new NextResponse("Not Found", { status: 404 }); } } // Log API requests with correlation id + timing. Skip noisy paths (assets, // crops) and prefer one structured line per request so Glitchtip / log // aggregators can correlate. if (pathname.startsWith("/api/") && !pathname.startsWith("/api/static") && !pathname.startsWith("/api/crop")) { log.info( { event: "http_request", method: request.method, path: pathname, correlation_id: reqId, duration_ms: Date.now() - t0, }, `${request.method} ${pathname}`, ); } return response; } export const config = { matcher: [ // Match everything EXCEPT static files + the static-file API "/((?!_next/static|_next/image|favicon.ico|api/static).*)", ], };