import { NextResponse, type NextRequest } from "next/server"; import { canAccessPath, getDefaultPathForRole, parseSessionCookie, SESSION_COOKIE, type UserRole } from "@/lib/auth"; import { DEFAULT_LOCALE, isLocale, LOCALE_COOKIE } from "@/lib/i18n"; import { getRequestBaseUrl } from "@/lib/request-url"; const AUTH_DEBUG = process.env.AUTH_DEBUG === "true" || process.env.AUTH_DEBUG === "1"; const publicPaths = ["/login", "/forgot-password", "/reset-password", "/unauthorized", "/invite", "/auth"]; function isPublicPath(pathname: string) { return publicPaths.some((path) => pathname === path || pathname.startsWith(`${path}/`)); } function shouldUseSecureCookies(request: NextRequest) { const explicit = process.env.COOKIE_SECURE?.toLowerCase() ?? ""; if (explicit === "true" || explicit === "1") { return true; } if (explicit === "false" || explicit === "0") { return false; } const forwardedProto = request.headers.get("x-forwarded-proto"); return request.nextUrl.protocol === "https:" || (forwardedProto?.split(",")[0]?.toLowerCase() === "https"); } function debugAuth(message: string, details: Record = {}) { if (!AUTH_DEBUG) { return; } console.warn(`[AUTH] ${message}`, details); } function setDebugHeaders(response: NextResponse, headers: Record) { if (!AUTH_DEBUG) { return; } Object.entries(headers).forEach(([key, value]) => { response.headers.set(key, value); }); } async function decodeSessionCookie(value: string) { const parsed = (await parseSessionCookie(value)) as null | { role: UserRole }; if (!parsed) { debugAuth("invalid_session_cookie", { cookieLength: value.length, cookiePreview: `${value.slice(0, 28)}...${value.slice(-14)}` }); } return parsed; } export async function middleware(request: NextRequest) { const { pathname } = request.nextUrl; const baseUrl = getRequestBaseUrl(request); const response = NextResponse.next(); if (pathname.startsWith("/_next") || pathname.includes(".")) { return response; } const sessionCookie = request.cookies.get(SESSION_COOKIE)?.value; const session = sessionCookie ? await decodeSessionCookie(sessionCookie) : null; const localeCookie = request.cookies.get(LOCALE_COOKIE)?.value; const acceptLanguage = request.headers.get("accept-language")?.toLowerCase() || ""; if (!localeCookie) { const detected = acceptLanguage.includes("id") ? "id" : acceptLanguage.includes("en") ? "en" : DEFAULT_LOCALE; response.cookies.set(LOCALE_COOKIE, detected, { path: "/", maxAge: 365 * 24 * 60 * 60, secure: shouldUseSecureCookies(request), sameSite: "lax" }); } else if (!isLocale(localeCookie)) { response.cookies.set(LOCALE_COOKIE, DEFAULT_LOCALE, { path: "/", maxAge: 365 * 24 * 60 * 60, secure: shouldUseSecureCookies(request), sameSite: "lax" }); } if (!session && !isPublicPath(pathname) && pathname !== "/") { const clientIp = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || request.headers.get("x-real-ip") || "unknown"; debugAuth("missing_or_invalid_session", { pathname, method: request.method, ip: clientIp, userAgent: request.headers.get("user-agent") || "unknown", hasSessionCookie: Boolean(sessionCookie), sessionCookieLength: sessionCookie?.length || 0, host: request.headers.get("host") || "unknown", protocol: request.nextUrl.protocol, forwardedProto: request.headers.get("x-forwarded-proto") || "unknown", secureCookies: shouldUseSecureCookies(request) }); const loginUrl = new URL("/login", baseUrl); loginUrl.searchParams.set("next", pathname); return NextResponse.redirect(loginUrl); } if (session && (pathname === "/" || pathname === "/login")) { const requested = request.nextUrl.searchParams.get("next"); const hasSafeNext = typeof requested === "string" && requested.startsWith("/") && !requested.startsWith("//"); const nextPath = hasSafeNext ? requested : null; const destination = nextPath && canAccessPath(session.role, nextPath) ? nextPath : getDefaultPathForRole(session.role); const redirectResponse = NextResponse.redirect(new URL(destination, baseUrl)); setDebugHeaders(redirectResponse, { "X-Auth-Session": "valid", "X-Auth-Session-Has-Cookie": String(Boolean(sessionCookie)), "X-Auth-Session-Role": session.role, "X-Auth-Path": pathname, "X-Auth-Base-Url": baseUrl.toString() }); return redirectResponse; } if (session && !isPublicPath(pathname) && !canAccessPath(session.role, pathname)) { debugAuth("role_forbidden", { pathname, role: session.role }); const forbiddenResponse = NextResponse.redirect(new URL("/unauthorized", baseUrl)); setDebugHeaders(forbiddenResponse, { "X-Auth-Session": "forbidden", "X-Auth-Session-Has-Cookie": String(Boolean(sessionCookie)), "X-Auth-Session-Role": session.role, "X-Auth-Path": pathname, "X-Auth-Base-Url": baseUrl.toString() }); return forbiddenResponse; } setDebugHeaders(response, { "X-Auth-Session": session ? "valid" : "missing", "X-Auth-Session-Has-Cookie": String(Boolean(sessionCookie)), "X-Auth-Session-Valid-Role": session?.role || "n/a", "X-Auth-Path": pathname, "X-Auth-Base-Url": baseUrl.toString(), "X-Auth-Host": request.headers.get("host") || "unknown" }); return response; } export const config = { matcher: ["/((?!api).*)"] };