3735dc0367
【workbuddy 推送 2026-06-14,任务 #10】 修复: - P0-#4 WS token 泄露:服务端 ws.py 优先从 Authorization: Bearer header 取, query param 仅作向后兼容降级路径(h5_websocket_endpoint 同) - P0-#5 坐席本地密码:Agent 模型加 password_hash 字段(bcrypt), 坐席登录增加 password 字段(企微验证失败时备用), 新增 POST /agents/password 端点修改密码, alembic 008 迁移脚本 新增/变更: M backend/app/api/agents.py (+67 行,登录 password 验证 + 改密端点) M backend/app/api/ws.py (~+30 行,header 优先 + query 降级) M backend/app/models/agent.py (+10 行,password_hash 字段) M backend/app/schemas/agent.py (+7 行,password 字段) M frontend-agent/.../useWebSocket.ts (+5 行,Authorization header) A backend/alembic/versions/008_add_agent_password.py A docs/安全/secret-管理.md (P0-#1 长期方案规划) 【评审遗留 5 项,详见 docs/评审报告/workbuddy-2026-06-14-P0安全.md】 - [P0-#4-ws.ts] 浏览器 WebSocket API 不支持自定义 header,需改 Sec-WebSocket-Protocol - [P0-#4-nginx] nginx access_log 没关闭,token 仍可能经 access_log 泄露 - [P0-#5-type] model Mapped[str] 严格模式下为 None 会报错,应改 Optional - [P0-#5-fall] 企微降级放行路径不强制 password 验证,反削弱 P0-#5 - [P0-#5-dep] requirements.txt 缺 passlib 依赖,部署会 ImportError 【推 Gitea】 卡 #8: MariaDB 套件未装,Gitea 未启动。本次 commit 暂存本地, Gitea 起来后一次 git push -u origin main 推送供 workbuddy 二次评审。
119 lines
4.0 KiB
Python
119 lines
4.0 KiB
Python
# =============================================================================
|
|
# 企微IT智能服务台 — 坐席 Pydantic Schema
|
|
# =============================================================================
|
|
# 说明:定义坐席相关的请求/响应数据结构
|
|
# 包含:登录、状态更新、响应三种 Schema
|
|
# =============================================================================
|
|
|
|
from datetime import datetime
|
|
from typing import List, Optional
|
|
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 坐席状态合法值
|
|
# --------------------------------------------------------------------------
|
|
VALID_AGENT_STATUSES = {"online", "offline", "busy"}
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 坐席登录 Schema
|
|
# --------------------------------------------------------------------------
|
|
class AgentLogin(BaseModel):
|
|
"""坐席登录请求 Schema。
|
|
|
|
第一步使用简单的用户名密码登录。
|
|
user_id 对应企微通讯录中的 UserID。
|
|
admin 角色需要 OTP 二次验证。
|
|
可选本地密码认证(企微验证失败时的备用认证)。
|
|
|
|
P0-#5 改动:
|
|
- 新增 password 字段:本地密码(可选)
|
|
- 企微主路径优先 → 本地 password 双因子(新增)
|
|
|
|
Attributes:
|
|
user_id: 企微用户ID
|
|
name: 坐席姓名
|
|
otp_code: OTP 动态码(admin 角色必填)
|
|
password: 本地密码(企微验证失败时的备用认证)
|
|
"""
|
|
|
|
user_id: str = Field(..., min_length=1, max_length=64, description="企微用户ID")
|
|
name: str = Field(..., min_length=1, max_length=128, description="坐席姓名")
|
|
otp_code: Optional[str] = Field(None, min_length=6, max_length=6, description="OTP动态码(6位数字)")
|
|
password: Optional[str] = Field(None, description="本地密码(可选)")
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 坐席状态更新 Schema
|
|
# --------------------------------------------------------------------------
|
|
class AgentStatusUpdate(BaseModel):
|
|
"""坐席状态更新请求 Schema。
|
|
|
|
坐席上线、离线、设为忙碌时使用。
|
|
|
|
Attributes:
|
|
status: 新的坐席状态
|
|
"""
|
|
|
|
status: str = Field(..., description="坐席状态: online/offline/busy")
|
|
|
|
@field_validator("status")
|
|
@classmethod
|
|
def validate_status(cls, v: str) -> str:
|
|
"""校验坐席状态值是否合法。"""
|
|
if v not in VALID_AGENT_STATUSES:
|
|
raise ValueError(f"无效的坐席状态: {v},合法值为: {VALID_AGENT_STATUSES}")
|
|
return v
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 坐席响应 Schema(返回给前端的数据结构)
|
|
# --------------------------------------------------------------------------
|
|
class AgentResponse(BaseModel):
|
|
"""坐席响应 Schema。
|
|
|
|
返回给前端的坐席数据结构。
|
|
使用 from_attributes=True 支持从 SQLAlchemy 模型直接转换。
|
|
|
|
Attributes:
|
|
id: 坐席ID
|
|
user_id: 企微用户ID
|
|
name: 坐席姓名
|
|
status: 坐席状态
|
|
role: 角色(admin=组长, agent=坐席)
|
|
skill_tags: 技能标签列表
|
|
current_load: 当前服务会话数
|
|
max_load: 最大同时服务数
|
|
created_at: 创建时间
|
|
updated_at: 更新时间
|
|
"""
|
|
|
|
id: str
|
|
user_id: str
|
|
name: str
|
|
status: str
|
|
role: str = "agent"
|
|
skill_tags: List[str] = []
|
|
current_load: int
|
|
max_load: int
|
|
otp_enabled: bool = False # OTP 是否已启用
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 坐席列表响应 Schema
|
|
# --------------------------------------------------------------------------
|
|
class AgentListResponse(BaseModel):
|
|
"""坐席列表响应 Schema。
|
|
|
|
Attributes:
|
|
items: 坐席列表
|
|
"""
|
|
|
|
items: List[AgentResponse]
|