ForcePilot/backend/server/routers/channels/login_router.py

318 lines
13 KiB
Python
Raw Normal View History

"""登录流程域 RouterLGN-QR-01 / LGN-QR-02 / LGN-01 / LGN-02 / LGN-OUT-01 / LGN-STATUS / LGN-FORCE-LOGOUT
router 实现渠道账户登录流程域的全部 HTTP 端点覆盖扫码启动扫码轮询
取消扫码刷新二维码登出登录态查询强制下线共 7 个操作所有端点统一
采用模板 A控制面端口路由通过 ``get_channel_use_cases`` 装配
``ChannelUseCases`` ``login`` 端口调用类型化方法
``ChannelControlService`` 内部走控制面管道
auth permission rate_limit dispatch audit
鉴权策略全部端点使用 ``get_admin_user`` 依赖要求管理员或超级管理员角色
角色校验由依赖函数完成router 内不做角色判断规范 §4
模板选型模板 A控制面端口路由端点函数体仅做 ``operator = build_operator``
``use_cases.login.<method>`` ``raiseOnControlFailure``
``return {"success": True, "data": serialize_control_data(result.data)}``
不包含 try/except 吞异常直接 DB 访问ORM 模型返回或 ``executeControl``
直接调用
路径设计所有端点共享 ``/{channel_type}/accounts/{account_id}/login/`` 前缀
其下 ``qr/start````qr/wait````qr/cancel````qr/refresh````logout``
``status````force-logout`` 均为静态路径段无静态/动态路径冲突 router
不自行设置 prefix根前缀 ``/channels`` ``channels_router`` 聚合 router
统一追加
端点清单对应登录流程域设计方案§2.1
- POST /{channel_type}/accounts/{account_id}/login/qr/start LGN-QR-01 start_qr_login
- GET /{channel_type}/accounts/{account_id}/login/qr/wait LGN-QR-02 wait_qr_login
- POST /{channel_type}/accounts/{account_id}/login/qr/cancel LGN-01 cancel_qr_login
- POST /{channel_type}/accounts/{account_id}/login/qr/refresh LGN-02 refresh_qr_login
- POST /{channel_type}/accounts/{account_id}/login/logout LGN-OUT-01 logout
- GET /{channel_type}/accounts/{account_id}/login/status LGN-STATUS get_login_status
- POST /{channel_type}/accounts/{account_id}/login/force-logout LGN-FORCE-LOGOUT force_logout
"""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, Query, Request
from pydantic import BaseModel, ConfigDict, Field
from yuxi.channels.contract.dtos.channel import ChannelType
from yuxi.channels.contract.dtos.login import ForceLogoutCmd
from yuxi.storage.postgres.models_business import User
from server.routers.channels import (
build_operator,
get_channel_use_cases,
raiseOnControlFailure,
serialize_control_data,
)
from server.utils.auth_middleware import get_admin_user
login_router = APIRouter(tags=["channels-login"])
class ForceLogoutRequest(BaseModel):
"""强制下线请求体LGN-FORCE-LOGOUT"""
model_config = ConfigDict(frozen=True)
reason: str = Field(..., description="强制下线原因(安全审计用,必填)")
revoke_credentials: bool = Field(
default=False,
description="是否同时吊销凭据(默认 False",
)
@login_router.post(
"/{channel_type}/accounts/{account_id}/login/qr/start",
response_model=dict,
)
async def start_qr_login(
channel_type: ChannelType,
account_id: str,
request: Request,
force: bool = Query(
default=False,
description="是否强制启动新会话(取消既有活跃会话)",
),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""启动扫码登录LGN-QR-01
对应控制面操作 ``login/qr_start`` ``ChannelControlService.startQrLogin``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道
编排链路HTTP 入参 ``build_operator`` 构造操作人
``use_cases.login.startQrLogin`` 调用端口方法 控制面管道调度插件
``LoginAdapter.loginWithQrStart`` 生成会话与二维码 ``ControlResult``
``raiseOnControlFailure`` 转译失败 ``serialize_control_data`` 序列化
会话与二维码数据返回
"""
operator = build_operator(current_user, request)
result = await use_cases.login.startQrLogin(
channel_type=channel_type,
account_id=account_id,
operator=operator,
force=force,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@login_router.get(
"/{channel_type}/accounts/{account_id}/login/qr/wait",
response_model=dict,
)
async def wait_qr_login(
channel_type: ChannelType,
account_id: str,
request: Request,
session_id: str = Query(..., description="扫码会话 ID"),
current_qr_data_url: str | None = Query(
default=None,
description="客户端当前持有的二维码 URL用于变更检测",
),
timeout_ms: int | None = Query(
default=None,
ge=1000,
le=60000,
description="长轮询超时(毫秒)",
),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""轮询扫码登录状态LGN-QR-02
对应控制面操作 ``login/qr_wait`` ``ChannelControlService.waitQrLogin``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道
采用 long-polling 模式服务端在 ``timeout_ms`` 窗口内阻塞等待状态变化
已扫码 / 已确认 / 已过期 / 二维码刷新状态变化或超时立即返回
客户端再次发起轮询``current_qr_data_url`` 用于变更检测仅当二维码
已刷新时返回新的 URL编排链路HTTP 入参 ``build_operator``
``use_cases.login.waitQrLogin`` 控制面管道调度插件
``LoginAdapter.loginWithQrWait`` ``ControlResult`` ``raiseOnControlFailure``
转译失败 ``serialize_control_data`` 序列化会话状态返回
"""
operator = build_operator(current_user, request)
result = await use_cases.login.waitQrLogin(
channel_type=channel_type,
account_id=account_id,
operator=operator,
session_id=session_id,
current_qr_data_url=current_qr_data_url,
timeout_ms=timeout_ms,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@login_router.post(
"/{channel_type}/accounts/{account_id}/login/qr/cancel",
response_model=dict,
)
async def cancel_qr_login(
channel_type: ChannelType,
account_id: str,
request: Request,
session_id: str = Query(..., description="待取消的扫码会话 ID"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""取消扫码登录LGN-01
对应控制面操作 ``login/qr_cancel`` ``ChannelControlService.cancelQrLogin``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道
编排链路HTTP 入参 ``build_operator`` 构造操作人
``use_cases.login.cancelQrLogin`` 调用端口方法 控制面管道调度
``QrLoginService.cancelQrLogin`` 推进本地状态机至 CANCELLED 并清理
内存会话映射不调用插件 LoginAdapter ``ControlResult``
``raiseOnControlFailure`` 转译失败 ``serialize_control_data`` 序列化
会话终结结果返回
"""
operator = build_operator(current_user, request)
result = await use_cases.login.cancelQrLogin(
channel_type=channel_type,
account_id=account_id,
operator=operator,
session_id=session_id,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@login_router.post(
"/{channel_type}/accounts/{account_id}/login/qr/refresh",
response_model=dict,
)
async def refresh_qr_login(
channel_type: ChannelType,
account_id: str,
request: Request,
session_id: str = Query(..., description="待刷新的扫码会话 ID"),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""刷新扫码二维码LGN-02
对应控制面操作 ``login/qr_refresh`` ``ChannelControlService.refreshQrLogin``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道
编排链路HTTP 入参 ``build_operator`` 构造操作人
``use_cases.login.refreshQrLogin`` 调用端口方法 控制面管道调度
``QrLoginService.refreshQrLogin`` 复用插件 ``LoginAdapter.loginWithQrStart``
重新生成二维码并刷新过期时间 ``ControlResult``
``raiseOnControlFailure`` 转译失败 ``serialize_control_data`` 序列化
新二维码数据返回
"""
operator = build_operator(current_user, request)
result = await use_cases.login.refreshQrLogin(
channel_type=channel_type,
account_id=account_id,
operator=operator,
session_id=session_id,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@login_router.post(
"/{channel_type}/accounts/{account_id}/login/logout",
response_model=dict,
)
async def logout(
channel_type: ChannelType,
account_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""登出渠道账户LGN-OUT-01
对应控制面操作 ``login/logout`` ``ChannelControlService.logout``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道
编排链路HTTP 入参 ``build_operator`` 构造操作人
``use_cases.login.logout`` 调用端口方法 控制面管道调度插件
``LoginAdapter.logout`` 清理渠道侧会话与凭据 ``ControlResult``
``raiseOnControlFailure`` 转译失败 ``serialize_control_data`` 序列化
登出结果返回幂等性未登录或已登出的账户再次调用幂等返回成功
"""
operator = build_operator(current_user, request)
result = await use_cases.login.logout(
channel_type=channel_type,
account_id=account_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@login_router.get(
"/{channel_type}/accounts/{account_id}/login/status",
response_model=dict,
)
async def get_login_status(
channel_type: ChannelType,
account_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""查询账户登录态LGN-STATUS
对应控制面操作 ``login/status`` ``ChannelControlService.getLoginStatus``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道
供管理员排查"机器人无响应"时确认账户扫码会话状态仅返回内存态扫码
会话信息不查询渠道侧 API轻量查询编排链路HTTP 入参
``build_operator`` 构造操作人 ``use_cases.login.getLoginStatus`` 调用
端口方法 控制面管道调度 ``QrLoginService`` 查询内存态扫码会话
``ControlResult`` ``raiseOnControlFailure`` 转译失败
``serialize_control_data`` 序列化登录态结果返回
"""
operator = build_operator(current_user, request)
result = await use_cases.login.getLoginStatus(
channel_type=channel_type,
account_id=account_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@login_router.post(
"/{channel_type}/accounts/{account_id}/login/force-logout",
response_model=dict,
)
async def force_logout(
channel_type: ChannelType,
account_id: str,
request: Request,
body: ForceLogoutRequest,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""强制下线渠道账户LGN-FORCE-LOGOUT
对应控制面操作 ``login/force_logout`` ``ChannelControlService.forceLogout``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道
管理员发起的强制下线可选吊销凭据编排链路HTTP 入参
``build_operator`` 构造操作人 构造 ``ForceLogoutCmd``
``use_cases.login.forceLogout`` 调用端口方法 控制面管道校验账户
存在性与登录态 调度 ``QrLoginService.forceLogout``内部检查适配器
是否实现 ``forceLogout`` + 调用适配器 发布 ``ChannelAccountOffline``
事件 ``ControlResult`` ``raiseOnControlFailure`` 转译失败
``serialize_control_data`` 序列化强制下线结果返回
"""
operator = build_operator(current_user, request)
cmd = ForceLogoutCmd(
channel_type=channel_type,
account_id=account_id,
operator=operator,
reason=body.reason,
revoke_credentials=body.revoke_credentials,
)
result = await use_cases.login.forceLogout(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}