assets-ai/src/middleware.ts

94 lines
3.6 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'
// API Key 验证:检查 ALLOWED_API_KEYS 环境变量(逗号分隔明文 key
// 注意middleware 运行在 Edge Runtime不能使用 better-sqlite3 等 Node.js 原生模块
// DB 级别的 key 验证在 route handler 中进行auth.ts verifyApiKey
function verifyApiKey(key: string): boolean {
if (!key.startsWith('ak_')) return false
const allowedKeys = process.env.ALLOWED_API_KEYS || ''
if (!allowedKeys) return false
return allowedKeys.split(',').map(k => k.trim()).includes(key)
}
function decodeJwtPayload(token: string): Record<string, unknown> | null {
try {
const parts = token.split('.')
if (parts.length !== 3) return null
let payload = parts[1].replace(/-/g, '+').replace(/_/g, '/')
while (payload.length % 4) payload += '='
return JSON.parse(atob(payload))
} catch { return null }
}
function isValidPayload(payload: Record<string, unknown> | null): boolean {
if (!payload) return false
return !(payload.exp && (payload.exp as number) < Math.floor(Date.now() / 1000))
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 登录/退出路径 + 内部 API 放行(自有 key 认证)
if (pathname === '/login' || pathname.startsWith('/api/auth/login') || pathname === '/api/auth/logout' || pathname.startsWith('/api/internal/')) {
return NextResponse.next()
}
// API 路由:检查 Bearer API Key 或 session cookie
if (pathname.startsWith('/api/')) {
const authHeader = request.headers.get('authorization')
if (authHeader?.startsWith('Bearer ak_')) {
if (verifyApiKey(authHeader.slice(7))) return NextResponse.next()
return NextResponse.json({ error: '未授权' }, { status: 401 })
}
const sharedToken = request.cookies.get('tlyq_session')?.value
const sharedPayload = sharedToken ? decodeJwtPayload(sharedToken) : null
if (isValidPayload(sharedPayload)) return NextResponse.next()
const localToken = request.cookies.get('session_assets')?.value
const localPayload = localToken ? decodeJwtPayload(localToken) : null
if (isValidPayload(localPayload)) return NextResponse.next()
return NextResponse.json({ error: '未授权' }, { status: 401 })
}
// 页面路由:优先检查 tlyq_session共享 JWT回退 session_assets本地 JWT
const sharedToken = request.cookies.get('tlyq_session')?.value
const sharedPayload = sharedToken ? decodeJwtPayload(sharedToken) : null
if (isValidPayload(sharedPayload)) {
const response = NextResponse.next()
response.cookies.set('session', JSON.stringify({ username: sharedPayload.username }), {
httpOnly: true,
sameSite: 'lax',
path: '/',
})
return response
}
const localToken = request.cookies.get('session_assets')?.value
const localPayload = localToken ? decodeJwtPayload(localToken) : null
if (isValidPayload(localPayload)) {
const response = NextResponse.next()
response.cookies.set('session', JSON.stringify({ username: localPayload.username }), {
httpOnly: true,
sameSite: 'lax',
path: '/',
})
return response
}
// 未认证 → 重定向登录页
const loginUrl = new URL('/login', request.url)
const dest = pathname + (request.nextUrl.search || '')
loginUrl.searchParams.set('redirect', dest)
const response = NextResponse.redirect(loginUrl)
if (sharedToken) response.cookies.delete('tlyq_session')
if (localToken) response.cookies.delete('session_assets')
return response
}
export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico|public).*)'] }