| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495 |
- import { type NextRequest, NextResponse } from "next/server";
- import createMiddleware from "next-intl/middleware";
- import type { Locale } from "@/i18n/config";
- import { routing } from "@/i18n/routing";
- import { AUTH_COOKIE_NAME } from "@/lib/auth";
- import { isDevelopment } from "@/lib/config/env.schema";
- import { logger } from "@/lib/logger";
- // Public paths that don't require authentication
- // Note: These paths will be automatically prefixed with locale by next-intl middleware
- const PUBLIC_PATH_PATTERNS = ["/login", "/usage-doc", "/api/auth/login", "/api/auth/logout"];
- const API_PROXY_PATH = "/v1";
- // Create next-intl middleware for locale detection and routing
- const intlMiddleware = createMiddleware(routing);
- function proxyHandler(request: NextRequest) {
- const method = request.method;
- const pathname = request.nextUrl.pathname;
- if (isDevelopment()) {
- logger.info("Request received", { method: method.toUpperCase(), pathname });
- }
- // API 代理路由不需要 locale 处理和 Web 鉴权(使用自己的 Bearer token)
- if (pathname.startsWith(API_PROXY_PATH)) {
- return NextResponse.next();
- }
- // Skip locale handling for static files and Next.js internals
- if (pathname.startsWith("/_next") || pathname === "/favicon.ico") {
- return NextResponse.next();
- }
- // Apply locale middleware first (handles locale detection and routing)
- const localeResponse = intlMiddleware(request);
- // Extract locale from pathname (format: /[locale]/path or just /path)
- const localeMatch = pathname.match(/^\/([^/]+)/);
- const potentialLocale = localeMatch?.[1];
- const isLocaleInPath = routing.locales.includes(potentialLocale as Locale);
- // Get the pathname without locale prefix
- // When isLocaleInPath is true, potentialLocale is guaranteed to be defined
- const pathWithoutLocale = isLocaleInPath
- ? pathname.slice((potentialLocale?.length ?? 0) + 1)
- : pathname;
- // Check if current path (without locale) is a public path
- const isPublicPath = PUBLIC_PATH_PATTERNS.some(
- (pattern) => pathWithoutLocale === pattern || pathWithoutLocale.startsWith(pattern)
- );
- // Public paths don't require authentication
- if (isPublicPath) {
- return localeResponse;
- }
- // Check authentication for protected routes (cookie existence only).
- // Full session validation (Redis lookup, key permissions, expiry) is handled
- // by downstream layouts (dashboard/layout.tsx, etc.) which run in Node.js
- // runtime with guaranteed Redis/DB access. This avoids a death loop where
- // the proxy deletes the cookie on transient validation failures.
- const authToken = request.cookies.get(AUTH_COOKIE_NAME);
- if (!authToken) {
- // Not authenticated, redirect to login page
- const url = request.nextUrl.clone();
- // Preserve locale in redirect
- const locale = isLocaleInPath ? potentialLocale : routing.defaultLocale;
- url.pathname = `/${locale}/login`;
- url.searchParams.set("from", pathWithoutLocale || "/dashboard");
- return NextResponse.redirect(url);
- }
- // Cookie exists - pass through to layout for full validation
- return localeResponse;
- }
- // Default export required for Next.js 16 proxy file
- export default proxyHandler;
- export const config = {
- matcher: [
- /*
- * Match all request paths except for the ones starting with:
- * - api (API routes - handled separately)
- * - _next/static (static files)
- * - _next/image (image optimization files)
- * - favicon.ico (favicon file)
- */
- "/((?!api|_next/static|_next/image|favicon.ico).*)",
- ],
- };
|