gongxiangp2/middleware.ts
chenpei f8db4232ce feat: 第一阶段框架——登录落点切换为品牌中心看板首页
- 看板首页 / :Zen Minimal × Future Organic 融合(用户裁定),编号索引+柔光光晕+GSAP克制动效,白皮书气质
- 素材库首页整体迁移至 /materials(含colocated测试),全站返回素材库链接同步清扫
- 组织架构页上移为顶层 /organization(保留云采石场契约注释),旧路径 /admin/org 服务端跳转;API 迁移 /api/organization(逻辑零改动)
- 后台删除组织架构跳转按钮(白屏问题源头消失);middleware 增 /materials 登录门、/organization 管理员门
- 品牌统一:浅色顶栏用 loginLogoUrl(深色图案版),标题「由壹点·品牌中心」;字体自托管 @fontsource
- 可访问性收尾(评审fix项):全文对比度过WCAG、焦点环、加载骨架防闪灰、移动端状态可见、::selection
- PRODUCT.md/DESIGN.md 记录方向契约;测试基线 1815 通过/6 既有失败(无劣化)
2026-08-15 11:01:27 +08:00

201 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { getToken } from 'next-auth/jwt'
import {
getAuthSessionCookieCandidates,
resolveSecureAuthCookieFromRequest,
} from '@/lib/auth-cookie'
const API_CORS_ALLOWED_HEADERS =
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-API-Key, Authorization, Cookie'
function getAllowedCorsOrigins(request: NextRequest): Set<string> {
const values = [
request.nextUrl.origin,
process.env.APP_PUBLIC_URL,
process.env.NEXTAUTH_URL,
...(process.env.CORS_ALLOWED_ORIGINS || '').split(','),
]
return new Set(values.map((value) => String(value || '').trim().replace(/\/+$/, '')).filter(Boolean))
}
function isAllowedCorsOrigin(request: NextRequest, requestOrigin: string | null): boolean {
if (!requestOrigin) return true
return getAllowedCorsOrigins(request).has(requestOrigin.replace(/\/+$/, ''))
}
// 注意:不在middleware中启动内存监控
// middleware使用Edge Runtime,不支持Node.js API
// 内存监控会在lib/memory-monitor.ts模块加载时自动启动(仅生产环境)
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
const requestOrigin = request.headers.get('origin')
const isAuthEntryPage =
pathname === '/login' ||
pathname === '/dingtalk-entry' ||
pathname === '/dingtalk-callback' ||
pathname.startsWith('/performance-survey')
// 处理API路由的CORS
if (pathname.startsWith('/api/')) {
const corsOriginAllowed = isAllowedCorsOrigin(request, requestOrigin)
if (!corsOriginAllowed) {
return NextResponse.json({ error: '不允许的跨域来源' }, { status: 403 })
}
// 处理OPTIONS预检请求
if (request.method === 'OPTIONS') {
const preflightResponse = new NextResponse(null, {
status: 204,
headers: {
'Access-Control-Allow-Methods': 'GET,DELETE,PATCH,POST,PUT,OPTIONS',
'Access-Control-Allow-Headers': API_CORS_ALLOWED_HEADERS,
'Access-Control-Max-Age': '86400',
'Vary': 'Origin',
},
})
if (requestOrigin) {
preflightResponse.headers.set('Access-Control-Allow-Credentials', 'true')
preflightResponse.headers.set('Access-Control-Allow-Origin', requestOrigin)
}
return preflightResponse
}
// 为所有API响应添加CORS头
const response = NextResponse.next()
response.headers.set('Vary', 'Origin')
if (requestOrigin) {
response.headers.set('Access-Control-Allow-Credentials', 'true')
response.headers.set('Access-Control-Allow-Origin', requestOrigin)
}
response.headers.set('Access-Control-Allow-Methods', 'GET,DELETE,PATCH,POST,PUT,OPTIONS')
response.headers.set('Access-Control-Allow-Headers', API_CORS_ALLOWED_HEADERS)
return response
}
// 处理需要认证的页面
// /materials(素材库)与 /organization(组织管理)与首页同级,需登录;
// /organization 沿用管理员角色门(与 /admin 一致)
if (
pathname === '/' ||
pathname === '/materials' ||
pathname.startsWith('/admin') ||
pathname.startsWith('/organization')
) {
const preferredSecureCookie = resolveSecureAuthCookieFromRequest(request)
const cookieCandidates = getAuthSessionCookieCandidates(preferredSecureCookie)
let token = null
for (const cookieName of cookieCandidates) {
token = await getToken({
req: request,
secret: process.env.NEXTAUTH_SECRET,
cookieName,
secureCookie: cookieName.startsWith('__Secure-'),
})
if (token) {
break
}
}
if (!token) {
const url = new URL('/login', request.url)
const redirectResponse = NextResponse.redirect(url)
// 为重定向响应也添加缓存控制头
redirectResponse.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0')
redirectResponse.headers.set('Pragma', 'no-cache')
redirectResponse.headers.set('Expires', '0')
return redirectResponse
}
// 管理员页面需要管理员权限
if (pathname.startsWith('/admin') || pathname.startsWith('/organization')) {
const role = String((token as any)?.role || '')
if (!['ADMIN', 'SYSTEM_ADMIN', 'DEPT_ADMIN'].includes(role)) {
const url = new URL('/', request.url)
const redirectResponse = NextResponse.redirect(url)
// 为重定向响应也添加缓存控制头
redirectResponse.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0')
redirectResponse.headers.set('Pragma', 'no-cache')
redirectResponse.headers.set('Expires', '0')
return redirectResponse
}
}
}
// 检查是否有新版本部署(通过环境变量控制)
// 只有在部署新版本时才禁用缓存,否则允许浏览器缓存以提高性能
const disableCache = process.env.DISABLE_CACHE === 'true'
const deploymentVersion = process.env.DEPLOYMENT_VERSION || process.env.BUILD_ID || ''
// 获取客户端存储的版本号(通过 cookie)
const clientVersion = request.cookies.get('app-version')?.value || ''
// 判断是否需要禁用缓存:有新版本部署且客户端版本与服务器版本不同
const shouldDisableCache = disableCache && deploymentVersion && clientVersion !== deploymentVersion
const response = NextResponse.next()
// 如果检测到新版本,设置版本号 cookie
if (shouldDisableCache && deploymentVersion) {
response.cookies.set('app-version', deploymentVersion, {
path: '/',
maxAge: 60 * 60 * 24 * 365, // 1年
sameSite: 'lax',
})
}
// 对于 HTML 页面,根据是否有新版本决定缓存策略
if (
pathname === '/' ||
pathname === '/materials' ||
pathname.startsWith('/admin') ||
pathname.startsWith('/organization') ||
pathname.startsWith('/share') ||
pathname.startsWith('/edit') ||
isAuthEntryPage ||
pathname.endsWith('.html')
) {
if (isAuthEntryPage || shouldDisableCache) {
// 有新版本:禁用缓存,强制浏览器获取最新代码
response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0')
response.headers.set('Pragma', 'no-cache')
response.headers.set('Expires', '0')
} else {
// 无新版本:允许浏览器缓存,提高性能
response.headers.set('Cache-Control', 'public, max-age=3600, must-revalidate') // 缓存1小时
}
}
// 对于 JS/CSS 等静态资源,使用版本号或时间戳来强制更新
if (pathname.match(/\.(js|css|json|ico|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)$/)) {
if (shouldDisableCache) {
// 有新版本:禁用缓存
response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0')
} else {
// 无新版本:长期缓存(Next.js 会自动添加版本号到文件名)
response.headers.set('Cache-Control', 'public, max-age=31536000, immutable')
}
}
return response
}
export const config = {
matcher: [
'/',
'/login',
'/materials',
'/organization/:path*',
'/admin/:path*',
'/api/:path*',
'/dingtalk-entry',
'/dingtalk-callback',
'/performance-survey/:path*',
],
}