issue-ai/src/middleware.ts

95 lines
3.4 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 { NextRequest, NextResponse } from 'next/server'
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))
}
// 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 buildLoginRedirect(request: NextRequest) {
const { pathname } = request.nextUrl
const loginUrl = new URL('/login', request.url)
const dest = pathname + (request.nextUrl.search || '')
loginUrl.searchParams.set('redirect', dest)
return NextResponse.redirect(loginUrl)
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 登录/退出路径 + 内部 API 放行(自有 key 认证)
if (pathname.startsWith('/login') || pathname === '/' ||
pathname === '/api/auth/login' || pathname === '/api/auth/logout' ||
pathname.startsWith('/api/internal/')) {
return NextResponse.next()
}
// API Key 认证(外部系统调用)
const authHeader = request.headers.get('authorization')
if (authHeader?.startsWith('Bearer ak_')) {
const key = authHeader.slice(7)
if (verifyApiKey(key)) return NextResponse.next()
// 无效 keyAPI 路由返回 401
if (pathname.startsWith('/api/')) {
return NextResponse.json({ error: '未授权' }, { status: 401 })
}
}
// 优先检查 tlyq_session共享 JWT
const sharedToken = request.cookies.get('tlyq_session')?.value
const sharedPayload = sharedToken ? decodeJwtPayload(sharedToken) : null
if (isValidPayload(sharedPayload)) {
const response = pathname.startsWith('/api/') ? NextResponse.next() : NextResponse.next()
response.cookies.set('session', JSON.stringify({ username: sharedPayload.username }), {
httpOnly: true, sameSite: 'lax', path: '/',
})
return response
}
// 回退 session_issue本地 JWT
const localToken = request.cookies.get('session_issue')?.value
const localPayload = localToken ? decodeJwtPayload(localToken) : null
if (pathname.startsWith('/api/')) {
if (!isValidPayload(localPayload)) {
return NextResponse.json({ error: '未登录' }, { status: 401 })
}
return NextResponse.next()
}
if (!isValidPayload(localPayload)) {
const response = buildLoginRedirect(request)
if (localToken) response.cookies.delete('session_issue')
return response
}
const response = NextResponse.next()
response.cookies.set('session', JSON.stringify({ username: localPayload.username }), {
httpOnly: true, sameSite: 'lax', path: '/',
})
return response
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
}