feat: OTP首次绑定 + 三端登录修复 + 管理端权限修复 (2026-07-08)

OTP首次绑定:
- 新增统一 OTP 路由 /auth/otp-* (otp.py + router.py)
- 坐席端 OTP 绑定面板 (OtpBindPanel.vue)
- 管理端 OTP 管理列表 (MfaManage.vue)
- agent_login 签发半认证 token 支持首次绑定流程

三端登录修复:
- 坐席/管理端去掉'返回扫码登录'按钮
- 管理端改为二维码始终可见+轮询扫码状态
- 员工端 /itdesk/ 改为 alias 直接服务 H5 (不再301重定向)
- docker-compose 添加 h5 volume 挂载

管理端权限修复:
- 扫码登录改用 get_user_roles() 替代写死 roles=['agent']
- get_user_roles() 增加 agents.role 回退
- 新增 GET /admin/roles/user-roles 端点
- 角色管理页加载用户角色分配数据

文档更新:
- OTP PRD + 系统设计文档
- 故障排查手册 v1.1 (新增6案例)
- nginx 生产基准配置
This commit is contained in:
Simon
2026-07-08 21:54:57 +08:00
parent 6f0fbbb066
commit 400ce3ddcb
27 changed files with 2822 additions and 312 deletions
+34 -8
View File
@@ -267,13 +267,15 @@ async def agent_login(
await db.flush()
logger.info(f"坐席登录: user_id={body.user_id}, name={body.name}")
# 2. MFA 二次验证(已绑定 MFA 的坐席/管理员
# 决策3三端认证重构 AUTH-04):移除「企微已登录+角色→免密直接进入」分支,
# 2. MFA 二次验证(三端认证重构 AUTH-04/AUTH-05
# 决策3(AUTH-04):移除「企微已登录+角色→免密直接进入」分支,
# 所有登录方式(扫码/账密/企微验证)均需 OTP 验证,统一安全水位。
# 执行MFA验证
# 决策4AUTH-05):区分两种 OTP 状态——
# - mfa_enabled=True → 已绑定,需验证 OTP 动态码
# - mfa_enabled=False → 未绑定,引导首次绑定流程
if agent.mfa_enabled:
# 已绑定 OTP → 要求验证或校验码
if not body.otp_code:
# 需要 OTP 验证,返回 require_otp 标记(必须包含role字段,否则前端校验会失败)
return success_response(data={
"require_otp": True,
"message": "请输入OTP动态码",
@@ -281,10 +283,34 @@ async def agent_login(
"name": agent.name,
"role": agent.role, # 必须包含role字段,供前端校验权限
})
else:
# 验证 OTP 码(决策3:复用 MFAService 统一校验逻辑)
if not MFAService.verify_code(agent.mfa_secret, body.otp_code, valid_window=1):
raise AppException(1006, "OTP验证码错误,请重新输入")
# 验证 OTP 码(复用 MFAService 统一校验逻辑)
if not MFAService.verify_code(agent.mfa_secret, body.otp_code, valid_window=1):
raise AppException(1006, "OTP验证码错误,请重新输入")
else:
# 未绑定 OTP → 引导首次绑定(AUTH-05
# BUG-001 修复: 签发半认证 token,使前端可以调用 otp-bind / otp-verify
# 这些端点需要 Bearer tokenget_current_user 认证),否则流程完全阻断
from app.services.token_service import TokenService
from app.dependencies import get_redis
redis_client = await get_redis()
token_service = TokenService(redis_client)
bind_token = await token_service.create_token(
employee_id=agent.user_id,
name=agent.name,
roles=["agent"],
avatar=avatar,
login_source="agent_pending_otp",
)
return success_response(data={
"require_otp_bind": True,
"message": "首次登录请先绑定OTP二次验证",
"user_id": agent.user_id,
"name": agent.name,
"role": agent.role,
"token": bind_token,
})
# 3. 生成随机 token(使用统一格式)
from app.services.token_service import TokenService