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:
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,9 @@ import { usePortalStore } from '@/stores/portal'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { Loading, CircleCloseFilled, User, Headset, Setting, InfoFilled } from '@element-plus/icons-vue'
|
||||
import apiClient from '@/api/index'
|
||||
import { useWeChatWorkSSO } from '@/composables/useWeChatWorkSSO'
|
||||
|
||||
const sso = useWeChatWorkSSO()
|
||||
|
||||
// 获取 Portal Store
|
||||
const portalStore = usePortalStore()
|
||||
@@ -142,12 +145,57 @@ const selectedRole = ref<string | null>(null)
|
||||
|
||||
/**
|
||||
* 初始化门户会话(可重入)
|
||||
* 流程:OAuth2 回调 → 缓存 token → 没登录就尝试 Mock(OAuth2 失败时)→ 加载用户信息
|
||||
* 流程:
|
||||
* 1. SSO 回调(企微浏览器走 SSO 才有 sso_token 参数)→ verifyToken → 走原流程
|
||||
* 2. 企微浏览器但没拿到 sso_token → 主动 init SSO(走企微 OAuth2)
|
||||
* 3. OAuth2 回调(普通浏览器走老 QR 流程,URL 中有 code 参数)
|
||||
* 4. Token 跳转(从其他端跳过来,URL 中有 token 参数)
|
||||
* 5. 本地缓存 → 没登录 → 触发 OAuth 或 dev Mock
|
||||
*/
|
||||
async function initPortalSession(): Promise<boolean> {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const token = urlParams.get('token')
|
||||
const code = urlParams.get('code')
|
||||
const ssoToken = urlParams.get('sso_token')
|
||||
|
||||
// 0a. SSO 回调:URL 中有 sso_token 参数
|
||||
// 流程: 企微浏览器 → init SSO → 企微授权 → callback 写入 token → 跳回 ?sso_token=xxx
|
||||
// → 前端 verifyToken(一次性)→ 写 portal store → 重走原加载流程
|
||||
if (ssoToken) {
|
||||
loading.value = true
|
||||
try {
|
||||
const verifyResult = await sso.verifyToken(ssoToken)
|
||||
if (!verifyResult) {
|
||||
error.value = 'SSO token 已过期,请重新进入'
|
||||
return
|
||||
}
|
||||
// verifyToken 返回的 role 用于判断下一步 next
|
||||
// 写 portal store(后续 fetchUserInfo 会覆盖,这里只用于决定跳哪儿)
|
||||
// 清除 URL 中的 sso_token 参数,避免刷新时重复消费
|
||||
window.history.replaceState({}, '', window.location.pathname)
|
||||
// 继续往下走: 走 isAuthenticated 检测
|
||||
console.log('[SSO] 验证成功:', verifyResult)
|
||||
} catch (err: any) {
|
||||
console.error('SSO verify 失败:', err)
|
||||
error.value = 'SSO 验证失败: ' + (err?.message || '未知错误')
|
||||
return
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 0b. 企微浏览器(还没触发过 SSO)→ 主动 init
|
||||
// 优先级高于老 QR 流程,因为企微浏览器走 QR 会被嫌麻烦
|
||||
// 例外: dev 模式 + 普通 Chrome 不走 SSO(开发用 Mock)
|
||||
if (!ssoToken && !token && !code && sso.isWeChatWork()) {
|
||||
const isDev = import.meta.env.DEV || import.meta.env.VITE_DEV_MODE === 'true'
|
||||
if (!isDev) {
|
||||
console.log('[SSO] 企微浏览器, 跳 SSO init')
|
||||
const url = sso.buildInitUrl('/itdesk/')
|
||||
window.location.href = url
|
||||
return // 跳走,代码不执行
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 企微 OAuth2 回调:URL 中有 code 参数
|
||||
if (code && !token) {
|
||||
|
||||
Reference in New Issue
Block a user