oclaw/uds-auth/lib/middleware/auth-middleware.js
2026-09-08 00:28:58 +08:00

94 lines
3.5 KiB
JavaScript
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 { UdsClient } from '../uds/client.js'
import { UdsValidator } from '../uds/validator.js'
import { ROLES, computePermissions } from '../roles.js'
/**
* auth-middleware: 从 PORTALSSOUser (empNo) 取用户,注入 role/permissions
*
* 三条路径:
* 1. 有 empNo + 有 session → 续期,注入
* 2. 有 empNo + 无 session → 创建 session + bootstrap role
* 3. 无 empNo → 未登录,交给下游
*/
export function createAuthMiddleware(config, sessionStore, rolesStore) {
const udsClient = new UdsClient(config.udsAuth)
const validatorConfig = {
...config.udsAuth,
authMode: config.udsAuth?.authMode || 'trust',
}
const udsValidator = new UdsValidator(validatorConfig)
const { cookieMaxAge, slidingExpiration, slidingInterval } = config.session
function extractEmpNo(cookieHeader) {
// 优先取 UDS 的 empNo,其次取 fallback administrator
const udsMatch = cookieHeader?.match(/(?:PORTALSSOUser|ZTEDPGSSOUser)=([^;]+)/)
if (udsMatch) return decodeURIComponent(udsMatch[1].trim())
const fbMatch = cookieHeader?.match(/UDS_FALLBACK_USER=([^;]+)/)
if (fbMatch) return decodeURIComponent(fbMatch[1].trim())
return null
}
async function authMiddleware(ctx, next) {
const { req } = ctx
const cookieHeader = req.headers.cookie || ''
const empNo = extractEmpNo(cookieHeader)
if (!empNo) return next()
// 1. 有 session → 续期
let userContext = await sessionStore.get(empNo)
if (userContext) {
if (slidingExpiration && userContext.lastActiveAt) {
const lastActive = new Date(userContext.lastActiveAt).getTime()
if (Date.now() - lastActive >= slidingInterval) {
userContext.lastActiveAt = new Date().toISOString()
await sessionStore.setex(empNo, Math.floor(cookieMaxAge / 1000), userContext)
}
}
} else {
// 2. 无 session → 创建
// 信任模式下,empNo 来自我们自己的 QR 登录流程设置的 cookie,
// 即使没有完整的 header 凭据也应建立会话。
const credentials = udsValidator.extractCredentials(req)
if (udsValidator.authMode === 'trust') {
// trust 模式:只要有 empNo 就建立会话;token 可选
userContext = udsValidator.buildUserContext(
credentials || { empNo, token: '', lang: 'zh-CN' },
false,
)
} else {
// verify 模式:必须有有效凭据
if (!credentials || !udsValidator.validateCredentials(credentials)) {
return next()
}
userContext = udsValidator.buildUserContext(credentials, false)
}
userContext.authenticatedAt = new Date().toISOString()
userContext.lastActiveAt = new Date().toISOString()
userContext.isAuthenticated = true
await sessionStore.setex(empNo, Math.floor(cookieMaxAge / 1000), userContext)
}
// 注入 role + permissions
const role = rolesStore.getRole(empNo)
ctx.userContext = userContext
ctx.empNo = empNo
ctx.role = role
ctx.permissions = computePermissions(role)
return next()
}
authMiddleware.udsClient = udsClient
authMiddleware.udsValidator = udsValidator
return authMiddleware
}
/**
* 权限守卫 — 拦截非 super_admin 的管理 API
* 用法: 在路由里 call guard(req, res, ctx, 'canManageUsers') → true 表示通过
*/
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]
}