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:
aiyimickey 2026-07-01 18:43:01 +08:00
parent 511e1ec9dd
commit 69e50905d8
6 changed files with 29 additions and 102 deletions

2
.gitignore vendored
View File

@ -3,3 +3,5 @@ node_modules/
.env .env
.DS_Store .DS_Store
.env.local .env.local
.DS_Store
tsconfig.tsbuildinfo

1
shared Symbolic link
View File

@ -0,0 +1 @@
../shared

View File

@ -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,
} }

View File

@ -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).*)'] }

View File

@ -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