mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 01:50:44 +08:00
183 lines
6.3 KiB
JavaScript
183 lines
6.3 KiB
JavaScript
import { UdsClient } from '../uds/client.js'
|
||
import { UdsValidator } from '../uds/validator.js'
|
||
import { ROLES, computePermissions } from '../roles.js'
|
||
import { searchUserByEmpNoToken } from '../uds/user-search.js'
|
||
|
||
/**
|
||
* auth-middleware
|
||
*
|
||
* 正常登录:Cookie 中必须同时有 empNo + token,并用 userSearchUrl
|
||
* (带 X-Emp-No / X-Auth-Value)直连内网拉用户详情;成功才建会话。
|
||
* 出站请求绕过 HTTP(S)_PROXY。
|
||
*
|
||
* 兜底登录:仅认可已由 /api/fallback/login 写好的 administrator 会话。
|
||
*/
|
||
export function createAuthMiddleware(config, sessionStore, rolesStore) {
|
||
const udsClient = new UdsClient(config.udsAuth)
|
||
const validatorConfig = {
|
||
...config.udsAuth,
|
||
authMode: config.udsAuth?.authMode || 'token+profile',
|
||
}
|
||
const udsValidator = new UdsValidator(validatorConfig)
|
||
const { cookieMaxAge, slidingExpiration } = config.session
|
||
const userSearchUrl = config.userSearchUrl || config.udsAuth?.userSearchUrl
|
||
const uacBaseUrl = config.udsAuth?.baseUrl || ''
|
||
|
||
function extractEmpNo(cookieHeader) {
|
||
const udsMatch = cookieHeader?.match(/(?:PORTALSSOUser|ZTEDPGSSOUser)=([^;]+)/)
|
||
if (udsMatch) return { empNo: decodeURIComponent(udsMatch[1].trim()), kind: 'uds' }
|
||
const fbMatch = cookieHeader?.match(/UDS_FALLBACK_USER=([^;]+)/)
|
||
if (fbMatch) return { empNo: decodeURIComponent(fbMatch[1].trim()), kind: 'fallback' }
|
||
return null
|
||
}
|
||
|
||
function applyProfile(userContext, profile, credentials) {
|
||
userContext.userId = profile.empNo
|
||
userContext.empNo = profile.empNo
|
||
userContext.username = profile.username
|
||
userContext.displayName = profile.username
|
||
userContext.department = profile.department
|
||
userContext.organization = profile.organization || profile.department || ''
|
||
userContext.email = profile.email
|
||
userContext.phone = profile.phone
|
||
userContext.token = credentials.token
|
||
userContext.lang = credentials.lang || userContext.lang || 'zh-CN'
|
||
userContext.isAuthenticated = true
|
||
userContext.authMode = 'token+profile'
|
||
userContext.lastActiveAt = new Date().toISOString()
|
||
return userContext
|
||
}
|
||
|
||
async function verifyEmpNoAndToken(empNo, token) {
|
||
const out = await searchUserByEmpNoToken({
|
||
userSearchUrl,
|
||
empNo,
|
||
token,
|
||
empNoHeader: config.udsAuth?.empNoHeader || 'X-Emp-No',
|
||
authValueHeader: config.udsAuth?.authValueHeader || 'X-Auth-Value',
|
||
origin: uacBaseUrl || undefined,
|
||
})
|
||
if (!out.ok) {
|
||
console.warn('[uds-auth] verifyEmpNoAndToken failed:', out)
|
||
return null
|
||
}
|
||
return out.profile
|
||
}
|
||
|
||
function needsProfileUpgrade(userContext) {
|
||
if (!userContext) return true
|
||
if (userContext.authMode === 'trust') return true
|
||
if (userContext.authMode !== 'token+profile') return true
|
||
const name = userContext.displayName || userContext.username || ''
|
||
if (!name || name === userContext.empNo) return true
|
||
return false
|
||
}
|
||
|
||
async function authMiddleware(ctx, next) {
|
||
const { req } = ctx
|
||
const cookieHeader = req.headers.cookie || ''
|
||
const extracted = extractEmpNo(cookieHeader)
|
||
|
||
if (!extracted) return next()
|
||
|
||
let userContext = await sessionStore.get(extracted.empNo)
|
||
|
||
// 旧 trust 会话 / 无姓名部门:强制用 token 重查用户信息
|
||
if (userContext && extracted.kind === 'uds' && needsProfileUpgrade(userContext)) {
|
||
const credentials = udsValidator.extractCredentials(req)
|
||
if (credentials && credentials.empNo === extracted.empNo && udsValidator.validateCredentials(credentials)) {
|
||
const profile = await verifyEmpNoAndToken(credentials.empNo, credentials.token)
|
||
if (profile) {
|
||
userContext = applyProfile(userContext, profile, credentials)
|
||
await sessionStore.setex(
|
||
profile.empNo,
|
||
Math.floor(cookieMaxAge / 1000),
|
||
userContext,
|
||
)
|
||
} else {
|
||
// 查不到资料则作废 trust 会话,避免“假登录”
|
||
await sessionStore.delete(extracted.empNo)
|
||
userContext = null
|
||
}
|
||
} else if (userContext.authMode === 'trust') {
|
||
await sessionStore.delete(extracted.empNo)
|
||
userContext = null
|
||
}
|
||
}
|
||
|
||
if (userContext) {
|
||
if (slidingExpiration) {
|
||
userContext.lastActiveAt = new Date().toISOString()
|
||
await sessionStore.setex(
|
||
extracted.empNo,
|
||
Math.floor(cookieMaxAge / 1000),
|
||
userContext,
|
||
)
|
||
}
|
||
const role = rolesStore.getRole(extracted.empNo)
|
||
ctx.userContext = userContext
|
||
ctx.empNo = extracted.empNo
|
||
ctx.role = role
|
||
ctx.permissions = computePermissions(role)
|
||
return next()
|
||
}
|
||
|
||
if (extracted.kind === 'fallback') {
|
||
return next()
|
||
}
|
||
|
||
const credentials = udsValidator.extractCredentials(req)
|
||
if (!credentials || !udsValidator.validateCredentials(credentials)) {
|
||
return next()
|
||
}
|
||
if (credentials.empNo !== extracted.empNo) {
|
||
return next()
|
||
}
|
||
|
||
const profile = await verifyEmpNoAndToken(credentials.empNo, credentials.token)
|
||
if (!profile) {
|
||
return next()
|
||
}
|
||
|
||
userContext = udsValidator.buildUserContext({
|
||
empNo: profile.empNo,
|
||
token: credentials.token,
|
||
lang: credentials.lang,
|
||
username: profile.username,
|
||
displayName: profile.username,
|
||
department: profile.department,
|
||
organization: profile.organization,
|
||
email: profile.email,
|
||
phone: profile.phone,
|
||
}, false)
|
||
applyProfile(userContext, profile, credentials)
|
||
userContext.authenticatedAt = new Date().toISOString()
|
||
userContext.sessionCreatedAt = new Date().toISOString()
|
||
|
||
await sessionStore.setex(
|
||
profile.empNo,
|
||
Math.floor(cookieMaxAge / 1000),
|
||
userContext,
|
||
)
|
||
|
||
const role = rolesStore.getRole(profile.empNo)
|
||
ctx.userContext = userContext
|
||
ctx.empNo = profile.empNo
|
||
ctx.role = role
|
||
ctx.permissions = computePermissions(role)
|
||
return next()
|
||
}
|
||
|
||
authMiddleware.udsClient = udsClient
|
||
authMiddleware.udsValidator = udsValidator
|
||
authMiddleware.verifyEmpNoAndToken = verifyEmpNoAndToken
|
||
return authMiddleware
|
||
}
|
||
|
||
export function requirePermission(ctx, permission) {
|
||
if (!ctx?.permissions) return false
|
||
if (permission === 'super_admin') {
|
||
return ctx.role === ROLES.SUPER_ADMIN || ctx.role === ROLES.FALLBACK_ADMIN
|
||
}
|
||
return !!ctx.permissions[permission]
|
||
}
|