400ce3ddcb
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 生产基准配置
513 lines
24 KiB
Markdown
513 lines
24 KiB
Markdown
# 系统架构设计 — OTP 首次绑定与重置
|
||
|
||
> **文档版本**: v1.0
|
||
> **创建日期**: 2026-07-06
|
||
> **架构师**: Bob(高见远)
|
||
> **关联 PRD**: `docs/02-产品需求/05-增量PRD-OTP首次绑定与重置.md`
|
||
> **关联母文档**: `docs/02-产品需求/04-增量PRD-三端认证重构.md`
|
||
|
||
---
|
||
|
||
## Part A: 系统设计
|
||
|
||
### 1. 实现方案与框架选型
|
||
|
||
#### 1.1 核心技术挑战
|
||
|
||
| 挑战 | 描述 | 策略 |
|
||
|------|------|------|
|
||
| **登录态区分** | 后端需区分"需要 OTP 验证"(已绑定)和"需要 OTP 绑定"(未绑定)两种状态 | `agents.py` 新增 `else` 分支,返回 `require_otp_bind: true` |
|
||
| **绑定即登录** | OTP 首次绑定成功后,用户不应重新输入账密 | `otp-verify` 绑定场景下直接签发 token 返回 |
|
||
| **前端 API 迁移** | 现有坐席/管理端 API 适配层使用旧端点 `/mfa/*`、`/admin/mfa/*`,需迁移到统一 `/auth/otp-*` | 逐步迁移,保留旧端点兼容过渡期 |
|
||
| **绑定面板嵌入** | PRD 要求绑定面板嵌入登录卡片内(非独立页面) | 新建 `OtpBindPanel.vue` 组件,在 `Login.vue` 中 `v-if="requireOtpBind"` 条件渲染 |
|
||
|
||
#### 1.2 框架与库(沿用现有栈,无新增)
|
||
|
||
| 层级 | 技术选型 | 说明 |
|
||
|------|----------|------|
|
||
| 后端框架 | FastAPI + SQLAlchemy Async | 已有,不新增 |
|
||
| OTP 服务 | `pyotp` + `qrcode` | 已有 `MFAService`,不新增 |
|
||
| 前端坐席端 | Vite + Vue 3 + Element Plus + Pinia | 已有,不新增 |
|
||
| 前端管理端 | Vite + Vue 3 + Element Plus + Pinia | 已有,不新增 |
|
||
| Redis | `redis.asyncio` | 已有,用于 MFA 验证状态缓存 |
|
||
|
||
#### 1.3 架构模式
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ nginx (反向代理) │
|
||
│ /api/auth/otp-* → backend │
|
||
└────────────┬────────────────────────────┬────────────────┘
|
||
│ │
|
||
┌────────▼────────┐ ┌────────▼────────┐
|
||
│ frontend-agent │ │ frontend-admin │
|
||
│ (坐席端 SPA) │ │ (管理端 SPA) │
|
||
│ │ │ │
|
||
│ Login.vue │ │ MfaManage.vue │
|
||
│ ├─ 账密表单 │ │ ├─ 列表+搜索 │
|
||
│ ├─ OTP 输入框 │ │ ├─ 清除绑定按钮 │
|
||
│ └─ OtpBindPanel │ │ └─ 确认弹窗 │
|
||
│ (新增组件) │ │ │
|
||
└────────┬─────────┘ └────────┬─────────┘
|
||
│ │
|
||
└──────────┬──────────────────┘
|
||
│
|
||
┌─────────▼──────────┐
|
||
│ backend (FastAPI) │
|
||
│ │
|
||
│ agents.py │
|
||
│ └─ agent_login: │
|
||
│ mfa_enabled? │
|
||
│ ├─ True+无码 │
|
||
│ │ → require_otp│
|
||
│ ├─ True+有码 │
|
||
│ │ → 验证→token │
|
||
│ └─ False │
|
||
│ → require_ │
|
||
│ otp_bind ★ │
|
||
│ │
|
||
│ otp.py │
|
||
│ ├─ otp-bind │
|
||
│ ├─ otp-verify ★ │
|
||
│ │ (绑定场景返token)│
|
||
│ ├─ otp-unbind │
|
||
│ ├─ otp-status │
|
||
│ ├─ otp-admin-users│
|
||
│ └─ otp-admin-reset│
|
||
└────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
### 2. 文件列表
|
||
|
||
#### 2.1 后端
|
||
|
||
| 相对路径 | 操作 | 说明 |
|
||
|----------|------|------|
|
||
| `backend/app/api/agents.py` | **【修改】** | `agent_login` 增加 `else` 分支:`mfa_enabled=False` → `require_otp_bind: true` |
|
||
| `backend/app/api/otp.py` | **【修改】** | `verify_otp` 绑定场景(`mfa_enabled=False`)校验成功后设置 `mfa_enabled=True`、`mfa_bound_at=now`,并签发返回 token |
|
||
| `backend/app/schemas/mfa.py` | **【修改】** | 新增 `MFAVerifyBindResponse`(含 `token` 字段) |
|
||
|
||
#### 2.2 前端 — 坐席端
|
||
|
||
| 相对路径 | 操作 | 说明 |
|
||
|----------|------|------|
|
||
| `frontend-agent/src/api/otp.ts` | **【新增】** | 统一 OTP API 适配层(bind / verify / unbind / status),使用新端点 `/auth/otp-*` |
|
||
| `frontend-agent/src/stores/agent.ts` | **【修改】** | `login()` 方法处理 `require_otp_bind` 响应(不再 throw Error,返回标记供 UI 消费) |
|
||
| `frontend-agent/src/views/Login.vue` | **【修改】** | 新增 `requireOtpBind` 状态 + `OtpBindPanel` 组件渲染 |
|
||
| `frontend-agent/src/components/OtpBindPanel.vue` | **【新增】** | 可复用 OTP 绑定面板(二维码 + secret + 验证码输入 + 确认按钮 + "暂不绑定"按钮) |
|
||
| `frontend-agent/src/views/Settings.vue` | **【新增】** | 坐席设置页,含 OTP 管理面板(状态显示 + 解绑 + 重新绑定)— P1 |
|
||
| `frontend-agent/src/router/index.ts` | **【修改】** | 新增 `/settings` 路由 — P1 |
|
||
|
||
#### 2.3 前端 — 管理端
|
||
|
||
| 相对路径 | 操作 | 说明 |
|
||
|----------|------|------|
|
||
| `frontend-admin/src/api/mfa.ts` | **【修改】** | 切换 API 路径到 `/auth/otp-admin-users` 和 `/auth/otp-admin-reset`,更新 TS 类型字段名 |
|
||
| `frontend-admin/src/views/MfaManage.vue` | **【修改】** | 更新字段映射(`mfa_enabled`/`mfa_bound_at`/`mfa_last_verified_at`),"重置 MFA"→"清除绑定",更新确认弹窗文案 |
|
||
| `frontend-admin/src/components/Sidebar.vue` | **【修改】** | 在"运营管理"分区下新增「OTP 管理」菜单项 |
|
||
| `frontend-admin/src/router/index.ts` | **【修改】** | 路由标题更新为「OTP 管理」 |
|
||
|
||
---
|
||
|
||
### 3. 数据结构与接口
|
||
|
||
#### 3.1 后端 Schema 变更
|
||
|
||
```mermaid
|
||
classDiagram
|
||
class MFAVerifyRequest {
|
||
+str otp_code
|
||
}
|
||
|
||
class MFAVerifyResponse {
|
||
+bool verified
|
||
+int expires_in
|
||
}
|
||
|
||
class MFAVerifyBindResponse {
|
||
+bool verified
|
||
+int expires_in
|
||
+str token
|
||
+str user_id
|
||
+str name
|
||
+str role
|
||
}
|
||
|
||
class MFABindStartResponse {
|
||
+str secret
|
||
+str otpauth_url
|
||
+str qr_code_base64
|
||
}
|
||
|
||
class MFAStatusResponse {
|
||
+bool bound
|
||
+bool enabled
|
||
+datetime last_verified_at
|
||
}
|
||
|
||
MFAVerifyRequest --> MFAVerifyResponse : "mfa_enabled=True → 仅验证"
|
||
MFAVerifyRequest --> MFAVerifyBindResponse : "mfa_enabled=False → 绑定+签发token"
|
||
```
|
||
|
||
#### 3.2 核心 API 契约
|
||
|
||
| 端点 | 方法 | 鉴权 | 说明 | 变更 |
|
||
|------|------|------|------|------|
|
||
| `/api/auth/otp-status` | GET | `get_current_user` | 查询绑定状态 | 无变更 |
|
||
| `/api/auth/otp-bind` | POST | `get_current_user` | 生成 secret + QR | 无变更 |
|
||
| `/api/auth/otp-verify` | POST | `get_current_user` | 校验 OTP → **绑定场景额外返回 token** | **★ 行为变更** |
|
||
| `/api/auth/otp-unbind` | POST | `get_current_user` | 用户主动解绑 | 无变更 |
|
||
| `/api/auth/otp-admin-users` | GET | `require_role("admin")` | 管理员查看全量坐席 OTP 状态 | 无变更 |
|
||
| `/api/auth/otp-admin-reset/{employee_id}` | POST | `require_role("admin")` | 管理员清除指定坐席 OTP | 无变更 |
|
||
| `/api/agents/login` | POST | 无(登录接口) | **mfa_enabled=False → require_otp_bind** | **★ 行为变更** |
|
||
|
||
#### 3.3 `POST /api/agents/login` 响应契约变更
|
||
|
||
**变更前**(当前 `agents.py:274`):
|
||
```json
|
||
// mfa_enabled=True 且未传 otp_code
|
||
{ "code": 0, "data": { "require_otp": true, "message": "请输入OTP动态码", "user_id": "...", "name": "...", "role": "..." } }
|
||
|
||
// mfa_enabled=False → 直接签发 token(问题)
|
||
{ "code": 0, "data": { "token": "...", "user_id": "...", ... } }
|
||
```
|
||
|
||
**变更后**:
|
||
```json
|
||
// mfa_enabled=True 且未传 otp_code — 不变
|
||
{ "code": 0, "data": { "require_otp": true, "message": "请输入OTP动态码", "user_id": "...", "name": "...", "role": "..." } }
|
||
|
||
// mfa_enabled=False — 新增 ★
|
||
{ "code": 0, "data": { "require_otp_bind": true, "message": "首次登录请先绑定OTP二次验证", "user_id": "...", "name": "...", "role": "..." } }
|
||
```
|
||
|
||
#### 3.4 `POST /api/auth/otp-verify` 响应契约变更
|
||
|
||
**变更前**(当前 `otp.py:verify_otp`):
|
||
```json
|
||
// mfa_enabled=False → 直接返回 verified=false
|
||
{ "code": 0, "data": { "verified": false, "expires_in": 0 } }
|
||
```
|
||
|
||
**变更后**:
|
||
```json
|
||
// mfa_enabled=False + 校验通过 → 绑定并签发 token ★
|
||
{ "code": 0, "data": { "verified": true, "expires_in": 1800, "token": "...", "user_id": "...", "name": "...", "role": "..." } }
|
||
|
||
// mfa_enabled=True + 校验通过 → 不变
|
||
{ "code": 0, "data": { "verified": true, "expires_in": 1800 } }
|
||
```
|
||
|
||
---
|
||
|
||
### 4. 程序调用流程
|
||
|
||
#### 4.1 首次登录 OTP 绑定流程(P0 核心)
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant User as 坐席
|
||
participant LoginVue as Login.vue
|
||
participant AgentStore as agentStore
|
||
participant OtpBind as OtpBindPanel.vue
|
||
participant API as apiClient
|
||
participant Backend as FastAPI Backend
|
||
participant Redis as Redis
|
||
|
||
User->>LoginVue: 输入账号密码 → 点击登录
|
||
LoginVue->>AgentStore: login(userId, password, undefined)
|
||
AgentStore->>API: POST /api/agents/login {user_id, password}
|
||
API->>Backend: agent_login()
|
||
|
||
Note over Backend: agent.mfa_enabled == False
|
||
Backend-->>API: { require_otp_bind: true, user_id, name, role }
|
||
API-->>AgentStore: { require_otp_bind: true, ... }
|
||
AgentStore-->>LoginVue: return { require_otp_bind: true }
|
||
|
||
Note over LoginVue: 隐藏账密表单<br/>显示 OtpBindPanel
|
||
LoginVue->>OtpBind: requireOtpBind = true
|
||
|
||
OtpBind->>API: POST /api/auth/otp-bind
|
||
API->>Backend: bind_otp()
|
||
Note over Backend: 生成 secret + QR
|
||
Backend-->>API: { secret, otpauth_url, qr_code_base64 }
|
||
API-->>OtpBind: { secret, otpauth_url, qr_code_base64 }
|
||
|
||
Note over OtpBind: 渲染二维码 + secret 明文
|
||
User->>User: 用 Authenticator 扫码(或手动输入 secret)
|
||
User->>OtpBind: 输入 6 位验证码 → 点击"验证并完成绑定"
|
||
|
||
OtpBind->>API: POST /api/auth/otp-verify { otp_code }
|
||
API->>Backend: verify_otp()
|
||
|
||
Note over Backend: mfa_enabled=False (绑定场景)<br/>校验 TOTP → 通过<br/>设置 mfa_enabled=True<br/>设置 mfa_bound_at=now
|
||
|
||
Backend->>Redis: mark_verified(employee_id, TTL=1800)
|
||
Backend->>Backend: 签发 JWT token
|
||
Backend-->>API: { verified: true, token, user_id, name, role }
|
||
API-->>OtpBind: { verified: true, token, ... }
|
||
|
||
OtpBind-->>LoginVue: emit('bind-success', { token, user_id, name })
|
||
LoginVue->>AgentStore: 保存 token → 设置 agentInfo
|
||
LoginVue->>LoginVue: router.push('/workspace')
|
||
```
|
||
|
||
#### 4.2 管理后台清除 OTP 绑定流程
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant Admin as 管理员
|
||
participant MfaVue as MfaManage.vue
|
||
participant API as apiClient
|
||
participant Backend as FastAPI Backend
|
||
participant DB as PostgreSQL
|
||
participant Redis as Redis
|
||
|
||
Admin->>MfaVue: 访问 OTP 管理页面
|
||
MfaVue->>API: GET /api/auth/otp-admin-users
|
||
API->>Backend: admin_list_otp_users()
|
||
Backend->>DB: SELECT * FROM agents
|
||
DB-->>Backend: agent rows
|
||
Backend-->>API: [{employee_id, name, mfa_enabled, mfa_bound_at, ...}]
|
||
API-->>MfaVue: 渲染表格
|
||
|
||
Admin->>MfaVue: 点击某坐席的「清除绑定」
|
||
MfaVue->>MfaVue: ElMessageBox 二次确认弹窗
|
||
Admin->>MfaVue: 确认清除
|
||
|
||
MfaVue->>API: POST /api/auth/otp-admin-reset/{employee_id}
|
||
API->>Backend: admin_reset_otp(employee_id)
|
||
Backend->>DB: UPDATE agents SET mfa_secret=NULL, mfa_enabled=False, mfa_bound_at=NULL
|
||
Backend->>Redis: clear_verified(employee_id)
|
||
Backend-->>API: { success: true }
|
||
API-->>MfaVue: ElMessage.success('已清除')
|
||
|
||
MfaVue->>API: 重新加载列表
|
||
```
|
||
|
||
#### 4.3 坐席自助解绑流程(P1)
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant User as 坐席
|
||
participant Settings as Settings.vue
|
||
participant API as apiClient
|
||
participant Backend as FastAPI Backend
|
||
participant DB as PostgreSQL
|
||
participant Redis as Redis
|
||
|
||
User->>Settings: 打开设置页 → OTP 管理面板
|
||
Settings->>API: GET /api/auth/otp-status
|
||
API->>Backend: get_otp_status()
|
||
Backend-->>API: { bound: true, enabled: true, last_verified_at }
|
||
API-->>Settings: 显示「✅ 已绑定」
|
||
|
||
User->>Settings: 点击「解绑 OTP」
|
||
Settings->>Settings: 弹窗:输入当前 OTP 验证码
|
||
User->>Settings: 输入 6 位码 → 确认
|
||
|
||
Settings->>API: POST /api/auth/otp-unbind { otp_code }
|
||
API->>Backend: unbind_otp()
|
||
Backend->>Backend: MFAService.verify_code() → 通过
|
||
Backend->>DB: UPDATE SET mfa_secret=NULL, mfa_enabled=False, mfa_bound_at=NULL
|
||
Backend->>Redis: clear_verified(employee_id)
|
||
Backend-->>API: { success: true }
|
||
API-->>Settings: ElMessage.success('OTP 已解绑')
|
||
Settings->>Settings: 刷新状态 → 显示「⚠️ 未绑定 — 立即绑定」
|
||
```
|
||
|
||
---
|
||
|
||
### 5. 待确认问题与假设
|
||
|
||
| # | 问题 | 假设 | 影响 |
|
||
|---|------|------|------|
|
||
| Q1 | 首次绑定能否"跳过"? | **P0 阶段不允许跳过**。简化首版逻辑,堵死无 OTP 直通漏洞。P1 阶段再引入"暂不绑定"按钮。 | 首次绑定向导无跳过按钮 |
|
||
| Q2 | 扫码登录的坐席是否需要 OTP? | **P0 阶段暂不处理**。扫码登录已有企微身份强验证,且当前 `agent_login` 中对已绑定 OTP 的坐席仍要求 OTP。本增量聚焦账密登录的绑定引导。 | 扫码登录不做额外改动 |
|
||
| Q3 | OTP verify 绑定场景是否直接返回 token? | **是**。修改 `otp.py:verify_otp`,绑定场景额外返回 token。避免前端二次 login。 | 后端改动量小,前端只需一次请求 |
|
||
| Q4 | 管理后台旧端点 `/admin/mfa/*` 是否需要保留? | **保留兼容过渡期**。前端切换到新端点,旧端点暂时保留不删,待下个迭代清理。 | 无风险 |
|
||
| Q5 | `OtpBindPanel.vue` 与已有 `MfaBind.vue` 的关系? | `OtpBindPanel` 是轻量嵌入组件(嵌入登录卡片内),`MfaBind.vue` 是独立全页组件(`/mfa-bind` 路由)。绑定流程 UI 逻辑可复用,但布局不同。**建议新建 `OtpBindPanel` 组件,从 `MfaBind.vue` 提取共享的二维码渲染 + 验证逻辑**。 | 两个组件共存,按场景使用 |
|
||
|
||
---
|
||
|
||
## Part B: 任务分解
|
||
|
||
### 6. 所需依赖包
|
||
|
||
**无新增依赖**。所有功能基于已有技术栈:
|
||
|
||
```
|
||
- Vue 3 + Element Plus(前端已有)
|
||
- Pinia(状态管理已有)
|
||
- pyotp + qrcode(后端已有 MFAService)
|
||
- FastAPI + SQLAlchemy Async(后端已有)
|
||
- redis.asyncio(Redis 客户端已有)
|
||
```
|
||
|
||
### 7. 任务列表(按依赖关系排序)
|
||
|
||
| Task ID | 任务名称 | 源文件 | 依赖 | 优先级 |
|
||
|---------|----------|--------|------|--------|
|
||
| **T01** | 后端 OTP 绑定流程改造 | `backend/app/api/agents.py`【修改】<br/>`backend/app/api/otp.py`【修改】<br/>`backend/app/schemas/mfa.py`【修改】 | 无 | **P0** |
|
||
| **T02** | 管理后台 OTP 管理页完整更新 | `frontend-admin/src/api/mfa.ts`【修改】<br/>`frontend-admin/src/views/MfaManage.vue`【修改】<br/>`frontend-admin/src/components/Sidebar.vue`【修改】<br/>`frontend-admin/src/router/index.ts`【修改】 | T01(需后端端点就绪) | **P0** |
|
||
| **T03** | 坐席端登录页 OTP 绑定面板 | `frontend-agent/src/api/otp.ts`【新增】<br/>`frontend-agent/src/stores/agent.ts`【修改】<br/>`frontend-agent/src/views/Login.vue`【修改】<br/>`frontend-agent/src/components/OtpBindPanel.vue`【新增】 | T01(需后端端点就绪) | **P0** |
|
||
| **T04** | 坐席端设置页 OTP 管理 | `frontend-agent/src/views/Settings.vue`【新增】<br/>`frontend-agent/src/router/index.ts`【修改】<br/>`frontend-agent/src/api/otp.ts`【修改】 | T03(依赖 otp.ts API 适配层) | **P1** |
|
||
|
||
#### T01 详细说明 — 后端 OTP 绑定流程改造
|
||
|
||
**修改点 1** — `backend/app/api/agents.py:274`(`agent_login` 函数):
|
||
```python
|
||
# 当前:只有 if agent.mfa_enabled: ...
|
||
# 改造后:添加 else 分支
|
||
if agent.mfa_enabled:
|
||
if not body.otp_code:
|
||
return success_response(data={
|
||
"require_otp": True,
|
||
"message": "请输入OTP动态码",
|
||
"user_id": agent.user_id,
|
||
"name": agent.name,
|
||
"role": agent.role,
|
||
})
|
||
else:
|
||
if not MFAService.verify_code(agent.mfa_secret, body.otp_code, valid_window=1):
|
||
raise AppException(1006, "OTP验证码错误,请重新输入")
|
||
else:
|
||
# ★ 新增:未绑定 OTP → 引导绑定
|
||
return success_response(data={
|
||
"require_otp_bind": True,
|
||
"message": "首次登录请先绑定OTP二次验证",
|
||
"user_id": agent.user_id,
|
||
"name": agent.name,
|
||
"role": agent.role,
|
||
})
|
||
```
|
||
|
||
**修改点 2** — `backend/app/api/otp.py:verify_otp`(第 213-218 行):
|
||
```python
|
||
# 当前:mfa_enabled=False → 直接返回 verified=false
|
||
# 改造后:mfa_enabled=False 且 mfa_secret 存在(已调用过 otp-bind)→ 走绑定校验逻辑
|
||
if not agent.mfa_enabled and agent.mfa_secret:
|
||
# ★ 绑定场景:校验 OTP → 设置 enabled + bound_at → 签发 token
|
||
if not MFAService.verify_code(agent.mfa_secret, body.otp_code):
|
||
logger.warning(f"OTP bind verify 验证码错误: agent={agent.user_id}")
|
||
return success_response(data=MFAVerifyResponse(verified=False, expires_in=0).model_dump())
|
||
|
||
now = datetime.now()
|
||
agent.mfa_enabled = True
|
||
agent.mfa_bound_at = now
|
||
agent.mfa_last_verified_at = now
|
||
db.add(agent)
|
||
await db.flush()
|
||
|
||
await MFAService.mark_verified(redis, agent.user_id, MFA_VERIFIED_TTL_SECONDS)
|
||
|
||
# 签发 token
|
||
from app.services.token_service import TokenService
|
||
from app.dependencies import get_redis as _get_redis_dep
|
||
redis_client = await _get_redis_dep()
|
||
token_service = TokenService(redis_client)
|
||
token = await token_service.create_token(
|
||
employee_id=agent.user_id,
|
||
name=getattr(agent, 'name', '') or '',
|
||
roles=[agent.role] if agent.role else [],
|
||
login_source="agent",
|
||
)
|
||
|
||
logger.info(f"OTP bind+verify 成功: agent={agent.user_id}")
|
||
return success_response(data={
|
||
"verified": True,
|
||
"expires_in": MFA_VERIFIED_TTL_SECONDS,
|
||
"token": token,
|
||
"user_id": agent.user_id,
|
||
"name": getattr(agent, 'name', '') or '',
|
||
"role": agent.role or '',
|
||
})
|
||
|
||
# 原有逻辑:已绑定场景保持不变
|
||
if not agent.mfa_enabled or not agent.mfa_secret:
|
||
return success_response(data=MFAVerifyResponse(verified=False, expires_in=0).model_dump())
|
||
# ... 后续不变
|
||
```
|
||
|
||
**修改点 3** — `backend/app/schemas/mfa.py`:新增 Schema(可选,也可用 dict 直接返回)。
|
||
|
||
#### T02 详细说明 — 管理后台 OTP 管理页完整更新
|
||
|
||
1. **`mfa.ts`**:API 路径从 `/admin/mfa/users` → `/auth/otp-admin-users`,`/admin/mfa/reset/{id}` → `/auth/otp-admin-reset/{id}`。TS 类型字段名从 `bound`/`bound_at` 改为 `mfa_enabled`/`mfa_bound_at`。
|
||
2. **`MfaManage.vue`**:字段映射 `row.bound` → `row.mfa_enabled`,`row.bound_at` → `row.mfa_bound_at`;按钮文案"重置 MFA"→"清除绑定";确认弹窗文案按 PRD §4.3 更新。
|
||
3. **`Sidebar.vue`**:在"📋 运营管理"分组添加 `<el-menu-item index="/mfa-manage">` 菜单项。
|
||
4. **`router/index.ts`**:`meta.title` 更新为 `'OTP 管理'`。
|
||
|
||
#### T03 详细说明 — 坐席端登录页 OTP 绑定面板
|
||
|
||
1. **`otp.ts`**:新建 API 适配层,封装 `bindOtp()` / `verifyOtp()` / `getOtpStatus()` / `unbindOtp()`,使用新端点 `/auth/otp-*`。
|
||
2. **`agent.ts:login()`**:处理 `require_otp_bind` 响应——不再 throw Error,返回 `{ require_otp_bind: true, user_id, name, role }` 供 Login.vue 消费。
|
||
3. **`Login.vue`**:新增 `requireOtpBind` ref;当 `result.require_otp_bind` 为 true 时,隐藏账密表单 + OTP 输入框,显示 `<OtpBindPanel>`;监听 `bind-success` 事件保存 token 并跳转。
|
||
4. **`OtpBindPanel.vue`**:新建可复用组件,包含:二维码渲染(`<img :src="data:image/png;base64,...">`)、secret 明文展示 + 复制按钮、6 位验证码输入框、"验证并完成绑定"主按钮、"暂不绑定,稍后设置"次要按钮(P0 阶段隐藏)。
|
||
|
||
#### T04 详细说明 — 坐席端设置页 OTP 管理(P1)
|
||
|
||
1. **`Settings.vue`**:新建设置页面,含「OTP 二次验证」面板:已绑定时显示状态+解绑按钮;未绑定时显示警告+绑定按钮。解绑流程:弹窗输入 OTP → `POST /auth/otp-unbind`。
|
||
2. **`router/index.ts`**:新增路由 `{ path: '/settings', component: () => import('@/views/Settings.vue'), meta: { title: '个人设置', requiresAuth: true } }`。
|
||
3. **`otp.ts`**:补充 `unbindOtp(otpCode)` 和 `getOtpStatus()` 的完整调用。
|
||
|
||
### 8. 共享知识
|
||
|
||
以下约定适用于所有任务、所有文件:
|
||
|
||
```
|
||
## 响应契约
|
||
- 所有 API 响应统一格式:{ code: 0, data: {...}, message: "success" }
|
||
- apiClient 拦截器已自动解包 data 层(Scheme A),前端直接消费 inner data
|
||
- 错误:{ code: 非0, message: "错误描述" } → 前端 catch 中取 error.message
|
||
|
||
## OTP 状态常量
|
||
- require_otp: true → 已绑定 OTP,需要输入 6 位码验证
|
||
- require_otp_bind: true → 未绑定 OTP,需要展示绑定面板(二维码 + 输入验证码)
|
||
- mfa_enabled: true/false → 数据库字段,标记 OTP 是否已启用
|
||
- mfa_bound_at: datetime → 首次绑定成功时间(NULL 表示从未绑定)
|
||
- mfa_secret: string/NULL → TOTP 密钥(仅绑定过程中临时存储,解绑后清空)
|
||
|
||
## 端点路径规范
|
||
- 坐席端/管理端统一使用 /auth/otp-* 前缀(新端点)
|
||
- 旧端点 /mfa/* 、/admin/mfa/* 保留兼容但前端不再调用
|
||
- nginx 剥离 /api 前缀后,对外即 /api/auth/otp-*
|
||
|
||
## 前端命名约定
|
||
- OtpBindPanel.vue: 嵌入登录卡片内的轻量绑定面板(P0)
|
||
- MfaBind.vue: 独立全页绑定向导(已有,保留用于 /mfa-bind 路由)
|
||
- 按钮文案:后端用"清除绑定",坐席端用"解绑 OTP"
|
||
|
||
## 错误处理
|
||
- OTP 验证码错误 → 返回 verified=false(不抛异常),前端显示红色提示,允许重试
|
||
- 解绑时 OTP 错误 → 后端抛 AppException(INVALID_PARAMETER),前端显示错误消息
|
||
- 已绑定用户重复调用 otp-bind → 后端拒绝,前端需先判断状态
|
||
|
||
## Redis Key
|
||
- mfa:verified:{employee_id} = "1",TTL = 1800s(30 分钟验证窗口)
|
||
- 绑定成功后写该 key(等同于已验证)
|
||
- 解绑/管理员清除时删除该 key
|
||
```
|
||
|
||
### 9. 任务依赖图
|
||
|
||
```mermaid
|
||
graph TD
|
||
T01["T01: 后端 OTP 绑定流程改造<br/>(agents.py + otp.py + schemas/mfa.py)<br/>P0"]
|
||
T02["T02: 管理后台 OTP 管理页<br/>(mfa.ts + MfaManage.vue + Sidebar.vue + router)<br/>P0"]
|
||
T03["T03: 坐席端登录 OTP 绑定面板<br/>(otp.ts + agent.ts + Login.vue + OtpBindPanel.vue)<br/>P0"]
|
||
T04["T04: 坐席端设置页 OTP 管理<br/>(Settings.vue + router + otp.ts)<br/>P1"]
|
||
|
||
T01 --> T02
|
||
T01 --> T03
|
||
T03 --> T04
|
||
```
|
||
|
||
> **说明**:T02 和 T03 可并行开发(均依赖 T01,互不依赖)。T04 依赖 T03(需要 `otp.ts` API 适配层)。
|
||
|
||
---
|
||
|
||
> **文档结束** — 请工程师按 T01 → T02/T03(并行) → T04 顺序实施。
|