/** * Edge-level route protection for Route Commerce (proxy.ts for Next.js 16+). * * This middleware runs at the edge (before any server code) and protects * /admin/* and /api/admin/* routes by checking for a valid Neon Auth * session cookie. * * Neon Auth session cookies: * - __Secure-neon-auth.session-token (secure/HTTPS) * - __Secure-neon-auth.session_token (secure/HTTPS) * - neon-auth.session-token (development) * - neon-auth.session_token (development) * * If no session is found for a protected route, the user is redirected * to /login with the original path as a query parameter. */ import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; // ── Routes ───────────────────────────────────────────────────────────────── /** Routes that require an authenticated session */ const PROTECTED_ROUTES = ["/admin", "/api/admin"] as const; /** Routes that are always accessible without authentication */ const PUBLIC_ROUTES = [ "/", "/login", "/logout", "/register", "/forgot-password", "/reset-password", "/change-password", "/tuxedo", "/indian-river-direct", "/wholesale", "/water", "/api/auth", "/api/auth/[...nextauth]", "/api/forgot-password", "/api/reset-password", "/api/change-password", "/api/stripe/webhook", "/api/water-qr", "/api/water-photo-upload", "/api/wholesale/checkout", "/api/wholesale/webhook", "/api/resend/webhook", "/api/square/webhook", "/api/cron", "/changelog", "/pricing", "/contact", "/blog", "/roadmap", "/privacy-policy", "/terms-and-conditions", "/waitlist", "/test", "/test-simple.html", "/robots.txt", "/sitemap.xml", ] as const; // ── Helpers ───────────────────────────────────────────────────────────────── /** Returns true when the given pathname starts with any protected route prefix. */ function isProtectedRoute(pathname: string): boolean { return PROTECTED_ROUTES.some((route) => pathname.startsWith(route)); } /** Returns true when the given pathname matches a public route exactly. */ function isPublicRoute(pathname: string): boolean { return PUBLIC_ROUTES.some( (route) => pathname === route || // Handle wildcard patterns like /api/auth/* (route.includes("[...") && pathname.startsWith(route.replace("/[...nextauth]", ""))) ); } /** * Checks for any Neon Auth session cookie. * Neon Auth sets cookies with names like: * - __Secure-neon-auth.session-token * - __Secure-neon-auth.session_token * - neon-auth.session-token * - neon-auth.session_token * * Also checks for dev_session cookie in development mode. */ function hasNeonAuthSession(request: NextRequest): boolean { const cookies = request.cookies; // Check for Neon Auth session cookies if ( cookies.get("__Secure-neon-auth.session-token")?.value || cookies.get("__Secure-neon-auth.session_token")?.value || cookies.get("neon-auth.session-token")?.value || cookies.get("neon-auth.session_token")?.value ) { return true; } // Development bypass: allow dev_session cookie for local testing // This bypass only works when NODE_ENV is not "production" if (process.env.NODE_ENV !== "production") { const devSession = cookies.get("dev_session")?.value; if (devSession === "platform_admin" || devSession === "brand_admin" || devSession === "store_employee") { return true; } } return false; } // ── Middleware ─────────────────────────────────────────────────────────────── export default async function proxy(request: NextRequest) { const { pathname } = request.nextUrl; // Allow public routes without any auth check if (isPublicRoute(pathname)) { return NextResponse.next(); } // For protected routes, verify the session if (isProtectedRoute(pathname)) { if (!hasNeonAuthSession(request)) { // No session — redirect to login, preserving the intended destination const loginUrl = new URL("/login", request.url); loginUrl.searchParams.set("redirect", pathname); return NextResponse.redirect(loginUrl, 307); } } // Allow all other routes through (storefront, etc.) return NextResponse.next(); } // ── Matcher ────────────────────────────────────────────────────────────────── /** * Next.js matcher configuration. * * Excludes: * - Static files and Next.js internals * - _next/static (webpack HMR) * - _next/image (image optimization) * - favicon.ico, etc. */ export const config = { matcher: [ /* * Match all request paths except: * - _next/static (static files) * - _next/image (image optimization) * - favicon.ico, sitemap.xml, robots.txt (static assets) * - .*\..* (files with extensions like .js, .css, .png) */ "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|.*\\..*).*)", ], };