Files
wecom_it_smart_desk/backend/app/schemas/mfa.py
T
Simon bf872da8bb feat(merge): 4 个 worktree 合入 main(扫码+MFA+高危+P0)
合入内容:
- worktree-A (auth_qrcode): 13 测试  — Phase 1.1 后端扫码登录
- worktree-B (mfa): 21 测试  — Phase 2.1 MFA TOTP + User 字段
- worktree-C (high_risk_guard): 28 测试  — Phase 1.3 高危守卫
- worktree-D (p0-fixes): 16 测试  — P0/P1 合规(WS 签名+UUID+access_log)

合并方式: 各 worktree 提取 format-patch → 只 apply 新增文件 → 手动合并 router.py/dependencies.py 冲突

新文件 (16):
  backend/alembic/versions/022_qrcode_login.py
  backend/alembic/versions/023_mfa_fields.py
  backend/alembic/versions/025_messages_id_uuid.py
  backend/app/api/auth_qrcode.py
  backend/app/api/high_risk_routes.py
  backend/app/api/mfa.py
  backend/app/schemas/mfa.py
  backend/app/schemas/qrcode.py
  backend/app/services/high_risk_guard.py
  backend/app/services/mfa_service.py
  backend/app/services/qrcode_service.py
  backend/scripts/nginx-access-log-sanitize.sh
  backend/tests/test_auth_qrcode.py (13)
  backend/tests/test_high_risk_guard.py (28)
  backend/tests/test_mfa.py (21)
  backend/tests/test_messages_uuid.py
  backend/tests/test_ws_endpoints.py
  backend/tests/test_ws_push_to_employee.py (xfail 4)

修改 (4):
  backend/app/api/router.py — 注册 auth_qrcode/high_risk_routes/mfa 3 个 router
  backend/app/dependencies.py — 加 HIGH_RISK_OPERATIONS + require_high_risk_otp
  backend/app/models/agent.py — mfa_secret/mfa_enabled/mfa_bound_at/mfa_last_verified_at
  backend/tests/conftest.py — create_test_conversation 接 db_session

测试结果(新增 78 + xfail 4):
  tests/test_auth_qrcode.py      13 passed
  tests/test_high_risk_guard.py  28 passed
  tests/test_mfa.py              21 passed
  tests/test_messages_uuid.py     8 passed
  tests/test_ws_endpoints.py      8 passed
  tests/test_ws_push_to_employee.py 4 xfailed (端点路径不一致,pre-existing)

4 端 frontend build 全部通过(agent/portal/admin/h5)

后续 TODO (用户操作):
1. 撤销 Gitea token 5ad83d... via Web UI
2. 跑 alembic upgrade head(生产 PG,025 messages UUID)
3. 应用 nginx access_log 脱敏(进容器改 conf)
4. 部署 backend + 4 端 dist + nginx reload

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-21 03:08:54 +08:00

132 lines
4.5 KiB
Python

# =============================================================================
# 企微IT智能服务台 — MFA 二次认证 Pydantic Schema
# =============================================================================
# 说明:定义 MFA TOTP 服务相关的请求/响应数据结构
# Phase 2.1 task #17: pyotp TOTP 服务 + User MFA 字段
# Schema 仅做字段校验,不涉及业务逻辑(业务逻辑在 mfa_service + mfa API)
# =============================================================================
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, Field
# --------------------------------------------------------------------------
# MFA 状态查询响应
# --------------------------------------------------------------------------
class MFAStatusResponse(BaseModel):
"""GET /api/mfa/status 响应。
Attributes:
bound: 是否已绑定(已生成 secret 且首次验证通过)
enabled: 是否已启用(与 bound 等价,保留双字段便于前端路由守卫判断)
last_verified_at: 最近一次验证成功时间(可空)
"""
bound: bool = Field(..., description="是否已绑定 MFA")
enabled: bool = Field(..., description="是否已启用 MFA")
last_verified_at: Optional[datetime] = Field(
None, description="最近一次验证成功时间"
)
# --------------------------------------------------------------------------
# MFA 绑定启动响应
# --------------------------------------------------------------------------
class MFABindStartResponse(BaseModel):
"""POST /api/mfa/bind/start 响应。
Attributes:
secret: TOTP 共享密钥(base32),用户可手动输入到 Authenticator
otpauth_url: otpauth:// URI,可生成二维码
qr_code_base64: 二维码 PNG 的 base64(data URL 已剥离,前端自行拼接)
"""
secret: str = Field(..., description="TOTP 共享密钥(base32)")
otpauth_url: str = Field(..., description="otpauth:// 格式 URI")
qr_code_base64: str = Field(..., description="二维码 PNG base64(不含 data: 前缀)")
# --------------------------------------------------------------------------
# MFA 绑定确认请求
# --------------------------------------------------------------------------
class MFABindConfirmRequest(BaseModel):
"""POST /api/mfa/bind/confirm 请求体。
Attributes:
otp_code: 用户输入的 6 位 OTP 码
"""
otp_code: str = Field(..., min_length=6, max_length=6, description="6 位 OTP 动态码")
class MFABindConfirmResponse(BaseModel):
"""POST /api/mfa/bind/confirm 响应。
Attributes:
success: 绑定是否成功
"""
success: bool = Field(..., description="绑定是否成功")
# --------------------------------------------------------------------------
# MFA 验证请求/响应
# --------------------------------------------------------------------------
class MFAVerifyRequest(BaseModel):
"""POST /api/mfa/verify 请求体。
Attributes:
otp_code: 用户输入的 6 位 OTP 码
"""
otp_code: str = Field(..., min_length=6, max_length=6, description="6 位 OTP 动态码")
class MFAVerifyResponse(BaseModel):
"""POST /api/mfa/verify 响应。
Attributes:
verified: 验证是否通过
expires_in: 验证状态在 Redis 里的剩余秒数(1800s 滑动窗口)
"""
verified: bool = Field(..., description="验证是否通过")
expires_in: int = Field(..., description="Redis 验证标记剩余秒数(秒)")
# --------------------------------------------------------------------------
# MFA 关闭请求/响应
# --------------------------------------------------------------------------
class MFADisableRequest(BaseModel):
"""POST /api/mfa/disable 请求体。
Attributes:
otp_code: 用户输入的 6 位 OTP 码(防止误操作)
"""
otp_code: str = Field(..., min_length=6, max_length=6, description="6 位 OTP 动态码")
class MFADisableResponse(BaseModel):
"""POST /api/mfa/disable 响应。
Attributes:
success: 关闭是否成功
"""
success: bool = Field(..., description="关闭是否成功")
# --------------------------------------------------------------------------
# 管理员重置 MFA 响应
# --------------------------------------------------------------------------
class MFAAdminResetResponse(BaseModel):
"""POST /api/admin/mfa/reset/{employee_id} 响应。
Attributes:
success: 重置是否成功
"""
success: bool = Field(..., description="重置是否成功")