feat(v0.7.1): P0 修复 + 企微 SSO + RBAC 细粒度 + audit_log

P0 修复:
- /api/ready import 错误 (_get_engine + settings.create_redis_client)
- 删 agent.otp_secret/otp_enabled 双字段 (migration 026)
- 重建 021_rbac migration (IF NOT EXISTS 兼容)

P1 新增:
- 企微 SSO (auth_wecom_sso.py, useWeChatWorkSSO composable, PortalSelect UA 检测)
- RBAC 5 角色 × 4 资源 × 4 操作 × 3 范围 (rbac_service + seed_rbac + require_permission)
- audit_log 模型 + migration 027 + 服务 + API
- 管理后台 RBAC 权限矩阵 UI (PermissionsMatrix.vue)

质量:
- pytest 405 passed / 33 pre-existing failed / 4 xfailed (v0.7.1 引入失败 = 0)
- conftest GBK patch 强制 UTF-8 读 .env
- .gitignore 排除 *.b64 (含 admin token 凭据)
- DEPLOY-v0.7.1.md 7 步 runbook + 4 坑 + 回滚预案
This commit is contained in:
Simon
2026-06-22 17:38:47 +08:00
parent 2e6ac0f0ab
commit 78f60c6857
30 changed files with 2928 additions and 49 deletions
@@ -0,0 +1,111 @@
// =============================================================================
// 企微IT智能服务台 — 企微 SSO UA 检测 composable (v0.7.1 task #85)
// =============================================================================
// 解决问题: v0.7.0 hotfix1 用户反馈"企微工作台进入应用也要扫码"。
//
// 用法:
// const sso = useWeChatWorkSSO()
// if (sso.isWeChatWork()) {
// window.location.href = sso.buildInitUrl('/itdesk/')
// }
//
// 设计原则:
// 1. UA 检测: MicroMessenger / wxwork / wxwork.* 都算企微浏览器
// 2. 静默授权: scope=snsapi_base (用户无感,直接拿到 userid)
// 3. 降级策略: 非企微浏览器保留原 QR 扫码流程
// =============================================================================
export type SSOInitOptions = {
/** 登录成功后跳转路径,默认 /itdesk/ */
next?: string
}
/**
* 企微 UA 检测 + SSO URL 生成 composable
*/
export function useWeChatWorkSSO() {
/**
* 检测当前是否在企微浏览器中
*
* 匹配的 UA 关键字:
* - MicroMessenger: 微信内置浏览器(用户侧)
* - wxwork: 企业微信内置浏览器(企业侧,最常见)
* - wxwork/.*: 企微版本号
* - DingTalk: 钉钉(预留,暂不实现)
*
* 参考文档: https://developer.work.weixin.qq.com/document/path/91484
*/
function isWeChatWork(): boolean {
const ua = navigator.userAgent || ''
return /MicroMessenger/i.test(ua) || /wxwork/i.test(ua) || /\bDingTalk\b/i.test(ua)
}
/**
* 构建 SSO 初始化 URL
*
* 后端 /api/auth_wecom/sso/init 会:
* 1. 生成 state 存 Redis (5 分钟 TTL, 防 CSRF)
* 2. 拼企微 OAuth2 授权 URL
* 3. 302 跳转到企微授权页
*
* 企微授权页会:
* 1. 用户授权(静默, snsapi_base)
* 2. 回调 /api/auth_wecom/sso/callback?code=...&state=...
* 3. 后端用 code 换 userid, 查角色, 生成 SSO token
* 4. 302 跳转到 next + ?sso_token=xxx
*
* @example
* window.location.href = buildInitUrl('/itdesk/')
*/
function buildInitUrl(next: string = '/itdesk/'): string {
const params = new URLSearchParams({ next })
// 用相对路径走 nginx 反代(开发环境走 vite proxy)
return `/api/auth_wecom/sso/init?${params.toString()}`
}
/**
* 用 SSO token 换取用户身份(一次性 token, 用完即删)
*
* 后端会返回 { user_id, name, role }
* 前端用此身份调 portal.getRoles() 或写入 store
*/
async function verifyToken(ssoToken: string): Promise<{
user_id: string
name: string
role: string
} | null> {
try {
const apiBase = import.meta.env.VITE_API_BASE || '/api'
const resp = await fetch(`${apiBase}/auth_wecom/sso/verify?sso_token=${encodeURIComponent(ssoToken)}`)
const data = await resp.json()
if (data?.code === 0 && data.data) {
return data.data
}
return null
} catch (e) {
console.error('SSO verify 失败:', e)
return null
}
}
/**
* 便捷函数: 如果是企微浏览器则跳转 SSO, 否则返回 false
*
* 用法:
* if (tryAutoSSO({ next: '/itdesk/' })) return
* // 降级到 QR 扫码
*/
function tryAutoSSO(options: SSOInitOptions = {}): boolean {
if (!isWeChatWork()) return false
const url = buildInitUrl(options.next)
window.location.href = url
return true
}
return {
isWeChatWork,
buildInitUrl,
verifyToken,
tryAutoSSO,
}
}