proxy.ts 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import { type NextRequest, NextResponse } from "next/server";
  2. import createMiddleware from "next-intl/middleware";
  3. import type { Locale } from "@/i18n/config";
  4. import { routing } from "@/i18n/routing";
  5. import { AUTH_COOKIE_NAME } from "@/lib/auth";
  6. import { isDevelopment } from "@/lib/config/env.schema";
  7. import { logger } from "@/lib/logger";
  8. // Public paths that don't require authentication
  9. // Note: These paths will be automatically prefixed with locale by next-intl middleware
  10. const PUBLIC_PATH_PATTERNS = ["/login", "/usage-doc", "/api/auth/login", "/api/auth/logout"];
  11. const API_PROXY_PATH = "/v1";
  12. // Create next-intl middleware for locale detection and routing
  13. const intlMiddleware = createMiddleware(routing);
  14. function proxyHandler(request: NextRequest) {
  15. const method = request.method;
  16. const pathname = request.nextUrl.pathname;
  17. if (isDevelopment()) {
  18. logger.info("Request received", { method: method.toUpperCase(), pathname });
  19. }
  20. // API 代理路由不需要 locale 处理和 Web 鉴权(使用自己的 Bearer token)
  21. if (pathname.startsWith(API_PROXY_PATH)) {
  22. return NextResponse.next();
  23. }
  24. // Skip locale handling for static files and Next.js internals
  25. if (pathname.startsWith("/_next") || pathname === "/favicon.ico") {
  26. return NextResponse.next();
  27. }
  28. // Apply locale middleware first (handles locale detection and routing)
  29. const localeResponse = intlMiddleware(request);
  30. // Extract locale from pathname (format: /[locale]/path or just /path)
  31. const localeMatch = pathname.match(/^\/([^/]+)/);
  32. const potentialLocale = localeMatch?.[1];
  33. const isLocaleInPath = routing.locales.includes(potentialLocale as Locale);
  34. // Get the pathname without locale prefix
  35. // When isLocaleInPath is true, potentialLocale is guaranteed to be defined
  36. const pathWithoutLocale = isLocaleInPath
  37. ? pathname.slice((potentialLocale?.length ?? 0) + 1)
  38. : pathname;
  39. // Check if current path (without locale) is a public path
  40. const isPublicPath = PUBLIC_PATH_PATTERNS.some(
  41. (pattern) => pathWithoutLocale === pattern || pathWithoutLocale.startsWith(pattern)
  42. );
  43. // Public paths don't require authentication
  44. if (isPublicPath) {
  45. return localeResponse;
  46. }
  47. // Check authentication for protected routes (cookie existence only).
  48. // Full session validation (Redis lookup, key permissions, expiry) is handled
  49. // by downstream layouts (dashboard/layout.tsx, etc.) which run in Node.js
  50. // runtime with guaranteed Redis/DB access. This avoids a death loop where
  51. // the proxy deletes the cookie on transient validation failures.
  52. const authToken = request.cookies.get(AUTH_COOKIE_NAME);
  53. if (!authToken) {
  54. // Not authenticated, redirect to login page
  55. const url = request.nextUrl.clone();
  56. // Preserve locale in redirect
  57. const locale = isLocaleInPath ? potentialLocale : routing.defaultLocale;
  58. url.pathname = `/${locale}/login`;
  59. url.searchParams.set("from", pathWithoutLocale || "/dashboard");
  60. return NextResponse.redirect(url);
  61. }
  62. // Cookie exists - pass through to layout for full validation
  63. return localeResponse;
  64. }
  65. // Default export required for Next.js 16 proxy file
  66. export default proxyHandler;
  67. export const config = {
  68. matcher: [
  69. /*
  70. * Match all request paths except for the ones starting with:
  71. * - api (API routes - handled separately)
  72. * - _next/static (static files)
  73. * - _next/image (image optimization files)
  74. * - favicon.ico (favicon file)
  75. */
  76. "/((?!api|_next/static|_next/image|favicon.ico).*)",
  77. ],
  78. };