monitor-ai/src/app/api/auth/callback/route.ts

108 lines
4.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.

// GET /api/auth/callback — OIDC callback 处理
import { NextRequest, NextResponse } from 'next/server'
import { exchangeCodeForToken, getUserinfo } from '@shared/lib/auth/oidc'
import { signJwt } from '@shared/lib/auth/jwt'
import { syncOidcUser } from '@shared/lib/auth/user-sync'
import { authConfig } from '@/lib/auth-config'
import { dbQueryParams, dbExec, escapeSql } from '@/lib/db'
import { writeAuditLog } from '@shared/lib/audit/write-audit-log'
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const code = searchParams.get('code')
const state = searchParams.get('state')
const error = searchParams.get('error')
// 使用 OIDC_REDIRECT_URI 构造公共 URLLESSONS-LEARNED #16
const baseUrl = authConfig.oidcRedirectUri?.replace(/\/api\/auth\/callback.*/, '') || 'http://localhost:6181'
if (error) {
return NextResponse.redirect(new URL(`/login?error=${encodeURIComponent(error)}`, baseUrl))
}
if (!code || !state) {
return NextResponse.redirect(new URL('/login?error=missing_params', baseUrl))
}
// 验证 state
const savedState = request.cookies.get('oidc_state')?.value
if (!savedState || savedState !== state) {
return NextResponse.redirect(new URL('/login?error=state_mismatch', baseUrl))
}
// 取出 code_verifier
const codeVerifier = request.cookies.get('oidc_code_verifier')?.value
if (!codeVerifier) {
return NextResponse.redirect(new URL('/login?error=missing_verifier', baseUrl))
}
// 换取 token
const tokenResult = await exchangeCodeForToken(
{ autheliaUrl: authConfig.autheliaUrl, clientId: authConfig.oidcClientId, clientSecret: authConfig.oidcClientSecret, redirectUri: authConfig.oidcRedirectUri },
code, codeVerifier,
)
if (!tokenResult.success) {
return NextResponse.redirect(new URL(`/login?error=${encodeURIComponent(tokenResult.error || 'token_exchange_failed')}`, baseUrl))
}
// 验证 nonce
const savedNonce = request.cookies.get('oidc_nonce')?.value
if (savedNonce && tokenResult.nonce && savedNonce !== tokenResult.nonce) {
return NextResponse.redirect(new URL('/login?error=nonce_mismatch', baseUrl))
}
// 获取 userinfo带错误处理
let userinfo
try {
userinfo = await getUserinfo(authConfig.autheliaUrl, tokenResult.accessToken)
} catch (e) {
const msg = e instanceof Error ? e.message : 'userinfo_fetch_failed'
return NextResponse.redirect(new URL(`/login?error=${encodeURIComponent(msg)}`, baseUrl))
}
// 用户同步(使用参数化查询)
const user = syncOidcUser({
getUser: (username) => {
const rows = dbQueryParams<{ id: number; role: string }>('SELECT id, role FROM users WHERE username = ?', [username])
return rows[0] ?? null
},
createUser: (username, displayName, email) => {
dbExec('INSERT INTO users (username, display_name, email, role) VALUES (?, ?, ?, ?)', [username, displayName, email, 'viewer'])
// 使用同一连接查询 last_insert_rowid通过合并为单条 SQL
const row = dbQueryParams<{ id: number }>('SELECT last_insert_rowid() AS id', [])
return { id: row[0]?.id ?? 0, role: 'viewer' }
},
updateUser: (username, displayName, email) => {
dbExec('UPDATE users SET display_name = ?, email = ?, updated_at = datetime(\'now\', \'+8 hours\') WHERE username = ?', [displayName, email, username])
},
}, userinfo)
// 签发 JWT
const token = signJwt({
secret: authConfig.jwtSecret,
payload: { username: userinfo.preferred_username, displayName: userinfo.name, role: user.role },
})
const response = NextResponse.redirect(new URL('/dashboard', baseUrl))
// 签发 tlyq_session cookie
response.cookies.set('tlyq_session', token, {
httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', domain: process.env.NODE_ENV === 'production' ? authConfig.cookieDomain : undefined, path: '/', maxAge: 604800,
})
// 清理 OIDC 临时 cookie
response.cookies.delete('oidc_code_verifier')
response.cookies.delete('oidc_state')
response.cookies.delete('oidc_nonce')
// 审计日志
writeAuditLog({ exec: dbExec }, {
userId: user.id, username: userinfo.preferred_username, action: 'login',
entityType: 'auth', details: { method: 'oidc', isNew: user.isNew },
ipAddress: request.headers.get('x-forwarded-for') || '127.0.0.1',
})
return response
}