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>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
# =============================================================================
|
||||
# 企微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="重置是否成功")
|
||||
@@ -0,0 +1,127 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 扫码登录 Pydantic Schema
|
||||
# =============================================================================
|
||||
# 说明:定义扫码登录的请求/响应数据结构
|
||||
# 涵盖 4 个端点的入参/出参:
|
||||
# 1. POST /api/auth_qrcode/create — 创建扫码登录票据
|
||||
# 2. GET /api/auth_qrcode/poll/{ticket} — 前端轮询扫码状态
|
||||
# 3. POST /api/auth_qrcode/scan — 企微用户扫码后 OAuth code 回调
|
||||
# 4. POST /api/auth_qrcode/confirm — 当前已登录用户确认授权
|
||||
# =============================================================================
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# POST /api/auth_qrcode/create — 创建扫码登录票据
|
||||
# --------------------------------------------------------------------------
|
||||
class QrcodeCreateResponse(BaseModel):
|
||||
"""扫码登录票据创建响应。
|
||||
|
||||
Attributes:
|
||||
ticket: 票据 UUID,前端用此票据轮询状态
|
||||
qrcode_url: 企微 OAuth2 授权 URL(前端渲染二维码)
|
||||
expires_in: 票据有效期(秒),默认 120
|
||||
expires_at: 票据过期时间(ISO 8601 字符串)
|
||||
"""
|
||||
|
||||
ticket: str = Field(..., description="票据 UUID")
|
||||
qrcode_url: str = Field(..., description="企微 OAuth2 授权 URL")
|
||||
expires_in: int = Field(120, description="有效期(秒)")
|
||||
expires_at: datetime = Field(..., description="过期时间(ISO 8601)")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# GET /api/auth_qrcode/poll/{ticket} — 轮询扫码状态
|
||||
# --------------------------------------------------------------------------
|
||||
class QrcodePollResponse(BaseModel):
|
||||
"""扫码登录票据轮询响应。
|
||||
|
||||
status 取值:
|
||||
- waiting: 票据有效,等待扫码
|
||||
- scanned: 已扫码,等待确认
|
||||
- confirmed: 已确认登录成功,附带 token
|
||||
- expired: 票据过期/不存在
|
||||
|
||||
Attributes:
|
||||
status: 扫码状态
|
||||
employee_id: 企微用户 ID(scanned/confirmed 时返回)
|
||||
name: 企微用户姓名(scanned/confirmed 时返回)
|
||||
token: 登录 Token(confirmed 时返回,前端存 localStorage)
|
||||
"""
|
||||
|
||||
status: str = Field(..., description="等待/已扫码/已确认/已过期")
|
||||
employee_id: Optional[str] = Field(None, description="企微用户 ID")
|
||||
name: Optional[str] = Field(None, description="企微用户姓名")
|
||||
token: Optional[str] = Field(None, description="登录 Token")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# POST /api/auth_qrcode/scan — 企微 OAuth code 回调
|
||||
# --------------------------------------------------------------------------
|
||||
class QrcodeScanRequest(BaseModel):
|
||||
"""扫码登录扫码请求体。
|
||||
|
||||
Attributes:
|
||||
ticket: 扫码登录票据(UUID)
|
||||
code: 企微 OAuth2 授权回调 code
|
||||
"""
|
||||
|
||||
ticket: str = Field(..., min_length=1, description="扫码登录票据")
|
||||
code: str = Field(..., min_length=1, description="企微 OAuth2 授权 code")
|
||||
|
||||
|
||||
class QrcodeScanResponse(BaseModel):
|
||||
"""扫码登录扫码响应。
|
||||
|
||||
Attributes:
|
||||
success: 是否成功
|
||||
message: 提示消息
|
||||
"""
|
||||
|
||||
success: bool = Field(..., description="是否成功")
|
||||
message: str = Field(..., description="提示消息")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# POST /api/auth_qrcode/confirm — 当前已登录用户确认授权
|
||||
# --------------------------------------------------------------------------
|
||||
class QrcodeConfirmRequest(BaseModel):
|
||||
"""扫码登录确认请求体。
|
||||
|
||||
Attributes:
|
||||
ticket: 扫码登录票据(UUID)
|
||||
otp_code: OTP 动态码(管理员场景下可选,普通坐席可空)
|
||||
"""
|
||||
|
||||
ticket: str = Field(..., min_length=1, description="扫码登录票据")
|
||||
otp_code: Optional[str] = Field(
|
||||
None,
|
||||
min_length=6,
|
||||
max_length=6,
|
||||
description="OTP 动态码(管理员可选,普通坐席留空)",
|
||||
)
|
||||
|
||||
|
||||
class QrcodeConfirmResponse(BaseModel):
|
||||
"""扫码登录确认响应。
|
||||
|
||||
Attributes:
|
||||
token: 登录 Token(scanned 用户换发的新 token)
|
||||
employee_id: 企微用户 ID
|
||||
name: 用户姓名
|
||||
roles: 用户角色列表
|
||||
require_otp: 是否需要 OTP 二次验证(预留,本任务不强制)
|
||||
"""
|
||||
|
||||
token: str = Field(..., description="登录 Token")
|
||||
employee_id: str = Field(..., description="企微用户 ID")
|
||||
name: str = Field(..., description="用户姓名")
|
||||
roles: List[str] = Field(default_factory=list, description="用户角色列表")
|
||||
require_otp: Optional[bool] = Field(
|
||||
None,
|
||||
description="是否需要 OTP 二次验证(预留字段,Phase 2.1 实现)",
|
||||
)
|
||||
Reference in New Issue
Block a user