refactor: P1 共享库迁移
- jwt.ts: 引用 shared/lib/auth/jwt(包装器保持 signSharedJwt/verifySharedJwt) - middleware.ts: 使用 shared/lib/auth/middleware 工厂 - tsconfig: 添加 @shared/* 路径 - 添加 shared symlink 独立审查通过
This commit is contained in:
parent
511e1ec9dd
commit
69e50905d8
|
|
@ -3,3 +3,5 @@ node_modules/
|
||||||
.env
|
.env
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env.local
|
.env.local
|
||||||
|
.DS_Store
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import crypto from 'crypto'
|
// oa-ai/src/lib/jwt.ts — 引用共享 JWT,保持原有接口
|
||||||
|
import { signJwt, verifyJwt } from '@shared/lib/auth/jwt'
|
||||||
|
|
||||||
const JWT_SECRET = process.env.JWT_SECRET || 'change-me-same-across-all-sites'
|
const JWT_SECRET = process.env.JWT_SECRET || 'default-secret-change-me'
|
||||||
const COOKIE_DOMAIN = process.env.COOKIE_DOMAIN || ''
|
const COOKIE_DOMAIN = process.env.COOKIE_DOMAIN || ''
|
||||||
|
|
||||||
export interface SharedSession {
|
export interface SharedSession {
|
||||||
|
|
@ -10,51 +11,34 @@ export interface SharedSession {
|
||||||
exp: number
|
exp: number
|
||||||
}
|
}
|
||||||
|
|
||||||
function base64url(str: string): string {
|
// 保持原有签名:signSharedJwt(payload, expiresIn)
|
||||||
return Buffer.from(str).toString('base64url')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function signSharedJwt(
|
export function signSharedJwt(
|
||||||
payload: { username: string; displayName: string },
|
payload: { username: string; displayName: string },
|
||||||
expiresIn: number = 7 * 24 * 60 * 60
|
expiresIn: number = 7 * 24 * 60 * 60
|
||||||
): string {
|
): string {
|
||||||
const header = { alg: 'HS256', typ: 'JWT' }
|
return signJwt({ secret: JWT_SECRET, payload, expiresInSeconds: expiresIn })
|
||||||
const now = Math.floor(Date.now() / 1000)
|
|
||||||
const body = { ...payload, iat: now, exp: now + expiresIn }
|
|
||||||
const segments = [base64url(JSON.stringify(header)), base64url(JSON.stringify(body))]
|
|
||||||
const signingInput = segments.join('.')
|
|
||||||
segments.push(
|
|
||||||
crypto.createHmac('sha256', JWT_SECRET).update(signingInput).digest('base64url')
|
|
||||||
)
|
|
||||||
return segments.join('.')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保持原有签名:verifySharedJwt(token)
|
||||||
export function verifySharedJwt(token: string): SharedSession | null {
|
export function verifySharedJwt(token: string): SharedSession | null {
|
||||||
try {
|
const payload = verifyJwt(token, JWT_SECRET)
|
||||||
const parts = token.split('.')
|
if (!payload) return null
|
||||||
if (parts.length !== 3) return null
|
|
||||||
const signingInput = parts.slice(0, 2).join('.')
|
|
||||||
const expectedSig = crypto.createHmac('sha256', JWT_SECRET)
|
|
||||||
.update(signingInput).digest('base64url')
|
|
||||||
if (parts[2] !== expectedSig) return null
|
|
||||||
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString())
|
|
||||||
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) return null
|
|
||||||
return {
|
return {
|
||||||
username: payload.username,
|
username: payload.username as string,
|
||||||
displayName: payload.displayName,
|
displayName: (payload.displayName || payload.username) as string,
|
||||||
iat: payload.iat,
|
iat: payload.iat as number,
|
||||||
exp: payload.exp,
|
exp: payload.exp as number,
|
||||||
}
|
}
|
||||||
} catch { return null }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保持原有签名:sharedCookieConfig(maxAge)
|
||||||
export function sharedCookieConfig(maxAge: number = 7 * 24 * 60 * 60) {
|
export function sharedCookieConfig(maxAge: number = 7 * 24 * 60 * 60) {
|
||||||
return {
|
return {
|
||||||
name: 'tlyq_session',
|
name: 'tlyq_session',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: process.env.NODE_ENV === 'production',
|
||||||
sameSite: 'lax' as const,
|
sameSite: 'lax' as const,
|
||||||
domain: COOKIE_DOMAIN,
|
domain: COOKIE_DOMAIN || undefined,
|
||||||
path: '/',
|
path: '/',
|
||||||
maxAge,
|
maxAge,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,9 @@
|
||||||
import { NextResponse } from 'next/server'
|
// oa-ai/src/middleware.ts — 使用共享 middleware 工厂
|
||||||
import type { NextRequest } from 'next/server'
|
import { createMiddleware } from '@shared/lib/auth/middleware'
|
||||||
|
|
||||||
function decodeJwtPayload(token: string): Record<string, unknown> | null {
|
export const middleware = createMiddleware({
|
||||||
try {
|
publicPaths: ['/login', '/api/auth', '/api/health', '/api/admin', '/setup-password', '/_next', '/favicon.ico'],
|
||||||
const parts = token.split('.')
|
adminPaths: ['/admin'],
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
function noCache(response: NextResponse) {
|
|
||||||
response.headers.set('Cache-Control', 'no-cache, no-store, must-revalidate')
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
export function middleware(request: NextRequest) {
|
|
||||||
const { pathname } = request.nextUrl
|
|
||||||
|
|
||||||
// 登录页:已登录用户自动跳转首页
|
|
||||||
if (pathname === '/login') {
|
|
||||||
const token = request.cookies.get('tlyq_session')?.value
|
|
||||||
const payload = token ? decodeJwtPayload(token) : null
|
|
||||||
if (isValidPayload(payload)) {
|
|
||||||
return NextResponse.redirect(new URL('/', request.url))
|
|
||||||
}
|
|
||||||
return NextResponse.next()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置密码/API 路径放行(API 路由自行验证)
|
|
||||||
if (pathname === '/setup-password' || pathname.startsWith('/api/auth/') || pathname.startsWith('/api/admin/')) {
|
|
||||||
return NextResponse.next()
|
|
||||||
}
|
|
||||||
// /admin 管理页面需要认证
|
|
||||||
if (pathname.startsWith('/admin')) {
|
|
||||||
const token = request.cookies.get('tlyq_session')?.value
|
|
||||||
const payload = token ? decodeJwtPayload(token) : null
|
|
||||||
if (!isValidPayload(payload)) {
|
|
||||||
return NextResponse.redirect(new URL('/login', request.url))
|
|
||||||
}
|
|
||||||
return noCache(NextResponse.next())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 静态资源放行(已有路径哈希,允许缓存)
|
|
||||||
if (pathname.startsWith('/_next/') || pathname === '/favicon.ico') {
|
|
||||||
return NextResponse.next()
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = request.cookies.get('tlyq_session')?.value
|
|
||||||
const payload = token ? decodeJwtPayload(token) : null
|
|
||||||
|
|
||||||
if (isValidPayload(payload)) {
|
|
||||||
const response = NextResponse.next()
|
|
||||||
response.cookies.set('session', JSON.stringify({ username: payload!.username }), {
|
|
||||||
httpOnly: true,
|
|
||||||
sameSite: 'lax',
|
|
||||||
path: '/',
|
|
||||||
})
|
|
||||||
return noCache(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
return noCache(NextResponse.redirect(new URL('/login', request.url)))
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico|public).*)'] }
|
export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico|public).*)'] }
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,13 @@
|
||||||
"name": "next"
|
"name": "next"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
"./src/*"
|
"./src/*"
|
||||||
|
],
|
||||||
|
"@shared/*": [
|
||||||
|
"./shared/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue