@@ -6,26 +6,21 @@ import { ROUTES } from '@/utils/constants/routes';
66// Constants
77// ============================================================================
88
9- /**
10- * 认证相关的 Cookie 键名
11- */
129const AUTH_COOKIES = {
1310 TOKEN : 'auth_token' ,
14- REFRESH_TOKEN : 'refresh_token' ,
1511 EXPIRES_IN : 'expires_in' ,
16- REFRESH_EXPIRES_IN : 'refresh_expires_in' ,
1712 TIMESTAMP : 'auth_timestamp' ,
1813} as const ;
1914
20- /**
21- * 默认的 token 过期时间(7天,单位:毫秒)
22- */
2315const DEFAULT_TOKEN_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000 ;
2416
17+ const INVALID_TOKEN_VALUES = new Set ( [ 'undefined' , 'null' , '' ] ) ;
18+
2519/**
26- * 无效的 token 字符串值
20+ * Public routes that never require authentication.
21+ * Proxy will always allow access to these paths.
2722 */
28- const INVALID_TOKEN_VALUES = new Set ( [ 'undefined ', 'null ' , '' ] ) ;
23+ const PUBLIC_ROUTES = new Set ( [ ROUTES . AUTH , '/auth/callback ', '/ ' , '/blog' , '/share '] ) ;
2924
3025// ============================================================================
3126// Type Definitions
@@ -41,29 +36,16 @@ interface AuthCookies {
4136// Helper Functions
4237// ============================================================================
4338
44- /**
45- * 检查 token 是否有效(非空且非无效值)
46- */
4739function isValidToken ( token : string | undefined ) : token is string {
48- if ( ! token || INVALID_TOKEN_VALUES . has ( token ) ) {
49- return false ;
50- }
40+ if ( ! token || INVALID_TOKEN_VALUES . has ( token ) ) return false ;
5141
5242 return token . trim ( ) . length > 0 ;
5343}
5444
55- /**
56- * 清除所有认证相关的 cookies
57- */
5845function clearAuthCookies ( response : NextResponse ) : void {
59- Object . values ( AUTH_COOKIES ) . forEach ( ( cookieName ) => {
60- response . cookies . delete ( cookieName ) ;
61- } ) ;
46+ Object . values ( AUTH_COOKIES ) . forEach ( ( name ) => response . cookies . delete ( name ) ) ;
6247}
6348
64- /**
65- * 从请求中提取认证相关的 cookies
66- */
6749function extractAuthCookies ( request : NextRequest ) : AuthCookies {
6850 return {
6951 token : request . cookies . get ( AUTH_COOKIES . TOKEN ) ?. value ,
@@ -72,108 +54,96 @@ function extractAuthCookies(request: NextRequest): AuthCookies {
7254 } ;
7355}
7456
75- /**
76- * 构建带有重定向参数的认证 URL
77- */
78- function buildAuthUrl ( request : NextRequest ) : URL {
79- const { pathname, search } = request . nextUrl ;
80- const redirectPath = pathname + search ;
57+ function isTokenExpired ( { timestamp, expiresIn } : AuthCookies ) : boolean {
58+ if ( ! timestamp ) return false ;
8159
82- const authUrl = new URL ( ROUTES . AUTH , request . url ) ;
60+ const authTime = Number ( timestamp ) ;
61+ if ( Number . isNaN ( authTime ) ) return true ;
8362
84- if ( redirectPath && redirectPath !== ROUTES . AUTH ) {
85- authUrl . searchParams . set ( 'redirect_to' , encodeURIComponent ( redirectPath ) ) ;
86- }
63+ const expiryMs = expiresIn ? Number ( expiresIn ) * 1000 : DEFAULT_TOKEN_EXPIRY_MS ;
8764
88- return authUrl ;
65+ return Date . now ( ) - authTime > expiryMs ;
8966}
9067
9168/**
92- * 重定向到认证页面
69+ * Check if a pathname is a public route (no auth required).
70+ * Handles exact matches and prefix matches (e.g. /blog/123, /share/abc).
9371 */
94- function redirectToAuth (
95- request : NextRequest ,
96- options : { clearCookies ?: boolean ; reason ?: string } = { } ,
97- ) : NextResponse {
98- const { clearCookies = false , reason } = options ;
99-
100- const authUrl = buildAuthUrl ( request ) ;
101- const response = NextResponse . redirect ( authUrl ) ;
102-
103- if ( clearCookies ) {
104- clearAuthCookies ( response ) ;
105- }
106-
107- if ( reason && process . env . NODE_ENV === 'development' ) {
108- console . log ( `[Middleware] Redirecting to auth: ${ reason } ` ) ;
109- }
72+ function isPublicRoute ( pathname : string ) : boolean {
73+ if ( PUBLIC_ROUTES . has ( pathname ) ) return true ;
11074
111- return response ;
75+ return (
76+ pathname . startsWith ( '/blog/' ) || pathname . startsWith ( '/share/' ) || pathname . startsWith ( '/auth/' )
77+ ) ;
11278}
11379
11480/**
115- * 检查 token 是否过期
81+ * Redirect to the auth page.
82+ * Only sets redirect_to for protected routes — never for public routes,
83+ * which would otherwise cause an infinite redirect loop.
11684 */
117- function isTokenExpired ( authCookies : AuthCookies ) : boolean {
118- const { timestamp, expiresIn } = authCookies ;
85+ function buildAuthRedirect (
86+ request : NextRequest ,
87+ options : { clearCookies ?: boolean } = { } ,
88+ ) : NextResponse {
89+ const { pathname, search } = request . nextUrl ;
90+ const authUrl = new URL ( ROUTES . AUTH , request . url ) ;
11991
120- if ( ! timestamp ) {
121- return false ;
92+ if ( ! isPublicRoute ( pathname ) ) {
93+ // Let URLSearchParams handle encoding — avoid double-encoding (%252F)
94+ authUrl . searchParams . set ( 'redirect_to' , pathname + search ) ;
12295 }
12396
124- const authTime = Number ( timestamp ) ;
97+ const response = NextResponse . redirect ( authUrl ) ;
12598
126- if ( Number . isNaN ( authTime ) ) {
127- return true ;
99+ if ( options . clearCookies ) {
100+ clearAuthCookies ( response ) ;
128101 }
129102
130- const now = Date . now ( ) ;
131- const expiryMs = expiresIn ? Number ( expiresIn ) * 1000 : DEFAULT_TOKEN_EXPIRY_MS ;
132-
133- return now - authTime > expiryMs ;
103+ return response ;
134104}
135105
136106// ============================================================================
137- // Main Middleware
107+ // Proxy (Next.js 16+)
138108// ============================================================================
139109
140- /**
141- * 认证中间件
142- * 验证用户的 token,未登录或 token 过期时重定向到登录页
143- */
144110export function proxy ( request : NextRequest ) : NextResponse {
111+ const { pathname } = request . nextUrl ;
112+
113+ // Always allow public routes through — no token check needed
114+ if ( isPublicRoute ( pathname ) ) {
115+ return NextResponse . next ( ) ;
116+ }
117+
145118 const authCookies = extractAuthCookies ( request ) ;
146119
147- // 验证 token 存在性和有效性
148120 if ( ! isValidToken ( authCookies . token ) ) {
149- return redirectToAuth ( request , {
150- reason : 'No valid token found' ,
151- } ) ;
121+ return buildAuthRedirect ( request ) ;
152122 }
153123
154- // 验证 token 是否过期
155124 if ( isTokenExpired ( authCookies ) ) {
156- return redirectToAuth ( request , {
157- clearCookies : true ,
158- reason : 'Token expired' ,
159- } ) ;
125+ return buildAuthRedirect ( request , { clearCookies : true } ) ;
160126 }
161127
162- // Token 有效,放行请求
163128 return NextResponse . next ( ) ;
164129}
165130
166131// ============================================================================
167- // Middleware Configuration
132+ // Proxy Configuration
168133// ============================================================================
169134
170- /**
171- * 配置需要认证保护的路径
172- */
173135export const config = {
174136 matcher : [
175- '/docs/:path*' , // 文档页面
176- '/dashboard/:path*' , // 控制台页面
177- '/chat-ai/:path*' , // AI 聊天页面
137+ /*
138+ * Only run on protected routes. Public routes excluded:
139+ * - Homepage: /
140+ * - Auth: /auth, /auth/callback
141+ * - Public content: /blog/*, /share/*
142+ * - Next.js internals: /_next/*, /api/*
143+ */
144+ '/docs/:path*' ,
145+ '/dashboard/:path*' ,
146+ '/chat-ai/:path*' ,
147+ '/rooms/:path*' ,
178148 ] ,
179149} ;
0 commit comments