2026-07-02 03:29:06 +08:00
|
|
|
|
"""白名单域 Router(ALW-01~ALW-06 + P1缺口)。
|
|
|
|
|
|
|
|
|
|
|
|
本 router 实现渠道账户白名单条目的全部 HTTP 端点,覆盖查询 / 新增 /
|
|
|
|
|
|
修改 / 删除 / 批量导入 / 导出 / 批量删除 / 清理过期共 8 个操作。所有端点
|
|
|
|
|
|
统一采用模板 A(控制面端口路由),通过 ``get_channel_use_cases`` 装配
|
|
|
|
|
|
``ChannelUseCases``,经 ``whitelist_management`` 端口调用类型化方法,由
|
|
|
|
|
|
``ChannelControlService._executeControl`` 内部走控制面管道
|
|
|
|
|
|
(auth → permission → rate_limit → dispatch → audit)。
|
|
|
|
|
|
|
|
|
|
|
|
鉴权策略:全部端点使用 ``get_admin_user`` 依赖,要求管理员或超级管理员角色。
|
|
|
|
|
|
角色校验由依赖函数完成,router 内不做角色判断(规范 §4)。
|
|
|
|
|
|
|
|
|
|
|
|
路径设计:``/batch`` / ``/export`` / ``/batch-delete`` / ``/clean-expired``
|
|
|
|
|
|
静态后缀先于 ``/{peer_id}`` 动态路径声明(规范 §6.5),避免静态路径被动态
|
|
|
|
|
|
参数捕获。子 router 不自行设置 prefix,根前缀 ``/channels`` 由
|
|
|
|
|
|
``channels_router`` 聚合 router 统一追加。
|
|
|
|
|
|
|
2026-07-04 00:16:00 +08:00
|
|
|
|
``peer_id`` 路径参数允许任意非空字符串(含 URL 编码字符),调用方需对
|
|
|
|
|
|
``/`` / ``?`` / ``#`` 等保留字符进行百分号编码(RFC 3986 §2.4)。
|
|
|
|
|
|
|
2026-07-02 03:29:06 +08:00
|
|
|
|
端点清单(对应本设计方案 §2.1 + P1缺口补全):
|
|
|
|
|
|
- GET /{channel_type}/{account_id}/allowlist/{policy_type} ALW-01 list_whitelist
|
|
|
|
|
|
- POST /{channel_type}/{account_id}/allowlist/{policy_type} ALW-02 add_whitelist_entry
|
|
|
|
|
|
- POST /{channel_type}/{account_id}/allowlist/{policy_type}/batch ALW-05 batch_add_whitelist
|
|
|
|
|
|
- GET /{channel_type}/{account_id}/allowlist/{policy_type}/export ALW-06 export_whitelist
|
|
|
|
|
|
- POST /{channel_type}/{account_id}/allowlist/{policy_type}/batch-delete ALW-BATCH-DEL 批量删除白名单
|
|
|
|
|
|
- POST /{channel_type}/{account_id}/allowlist/{policy_type}/clean-expired ALW-CLEAN-EXPIRED 清理过期白名单
|
2026-07-04 00:16:00 +08:00
|
|
|
|
- PATCH /{channel_type}/{account_id}/allowlist/{policy_type}/{peer_id} ALW-03 update_whitelist_entry
|
2026-07-02 03:29:06 +08:00
|
|
|
|
- DELETE /{channel_type}/{account_id}/allowlist/{policy_type}/{peer_id} ALW-04 remove_whitelist_entry
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-07-04 00:16:00 +08:00
|
|
|
|
from datetime import datetime
|
2026-07-02 03:29:06 +08:00
|
|
|
|
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.whitelist import (
|
2026-07-04 00:16:00 +08:00
|
|
|
|
UNSET,
|
2026-07-02 03:29:06 +08:00
|
|
|
|
BatchWhitelistDeleteCmd,
|
|
|
|
|
|
CleanExpiredWhitelistCmd,
|
2026-07-04 00:16:00 +08:00
|
|
|
|
UnsetType,
|
2026-07-02 03:29:06 +08:00
|
|
|
|
WhitelistEntry,
|
|
|
|
|
|
WhitelistPolicyType,
|
2026-07-04 00:16:00 +08:00
|
|
|
|
WhitelistUpdateCmd,
|
2026-07-02 03:29:06 +08:00
|
|
|
|
)
|
|
|
|
|
|
from yuxi.channels.contract.errors import ValidationError
|
|
|
|
|
|
from yuxi.storage.postgres.models_business import User
|
|
|
|
|
|
|
|
|
|
|
|
from server.routers.channels import (
|
|
|
|
|
|
build_operator,
|
|
|
|
|
|
get_channel_use_cases,
|
|
|
|
|
|
parse_datetime,
|
|
|
|
|
|
raiseOnControlFailure,
|
|
|
|
|
|
serialize_control_data,
|
|
|
|
|
|
)
|
|
|
|
|
|
from server.utils.auth_middleware import get_admin_user
|
|
|
|
|
|
|
|
|
|
|
|
allowlist_router = APIRouter(tags=["channels-allowlist"])
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-04 00:16:00 +08:00
|
|
|
|
_WHITELIST_POLICY_VALUES = " | ".join(e.value for e in WhitelistPolicyType)
|
|
|
|
|
|
_WHITELIST_EXPORT_FORMATS = ("json", "csv")
|
|
|
|
|
|
_WHITELIST_EXPORT_FORMAT_VALUES = " | ".join(_WHITELIST_EXPORT_FORMATS)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-02 03:29:06 +08:00
|
|
|
|
def _parse_policy_type(raw: str) -> WhitelistPolicyType:
|
|
|
|
|
|
"""string → WhitelistPolicyType,失败抛 ValidationError。"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
return WhitelistPolicyType(raw)
|
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
|
"policy_type",
|
2026-07-04 00:16:00 +08:00
|
|
|
|
f"unsupported policy_type: {raw} (expected: {_WHITELIST_POLICY_VALUES})",
|
2026-07-02 03:29:06 +08:00
|
|
|
|
) from exc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_export_format(raw: str) -> str:
|
|
|
|
|
|
"""校验导出格式参数,失败抛 ValidationError。
|
|
|
|
|
|
|
|
|
|
|
|
仅允许 ``json`` / ``csv`` 两种取值,原样返回(无需类型转换)。
|
|
|
|
|
|
"""
|
2026-07-04 00:16:00 +08:00
|
|
|
|
if raw not in _WHITELIST_EXPORT_FORMATS:
|
2026-07-02 03:29:06 +08:00
|
|
|
|
raise ValidationError(
|
|
|
|
|
|
"format",
|
2026-07-04 00:16:00 +08:00
|
|
|
|
f"unsupported format: {raw} (expected: {_WHITELIST_EXPORT_FORMAT_VALUES})",
|
2026-07-02 03:29:06 +08:00
|
|
|
|
)
|
|
|
|
|
|
return raw
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WhitelistAddRequest(BaseModel):
|
|
|
|
|
|
"""新增白名单条目请求。字段对齐 ``WhitelistManagementPort.addWhitelistEntry`` 入参。"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
2026-07-04 00:16:00 +08:00
|
|
|
|
peer_id: str = Field(..., min_length=1, description="对端 ID(非空)")
|
2026-07-02 03:29:06 +08:00
|
|
|
|
peer_name: str | None = Field(default=None, description="对端名称")
|
|
|
|
|
|
reason: str | None = Field(default=None, description="加白原因")
|
|
|
|
|
|
expires_at: str | None = Field(
|
|
|
|
|
|
default=None,
|
|
|
|
|
|
description="过期时间(ISO 8601),缺失表示永久",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WhitelistUpdateRequest(BaseModel):
|
2026-07-04 00:16:00 +08:00
|
|
|
|
"""修改白名单条目请求(PATCH 三态语义)。
|
2026-07-02 03:29:06 +08:00
|
|
|
|
|
|
|
|
|
|
支持修改 ``peer_name`` / ``reason`` / ``expires_at`` 三字段;
|
2026-07-04 00:16:00 +08:00
|
|
|
|
``peer_id`` 与 ``policy_type`` 为路径参数不可变更。
|
|
|
|
|
|
|
|
|
|
|
|
字段三态语义:
|
|
|
|
|
|
- 字段未提供(不在请求体中):不修改,保留原值
|
|
|
|
|
|
- 字段显式为 ``null``:清除原值(``expires_at=null`` 设为永久;
|
|
|
|
|
|
``peer_name`` / ``reason`` 清空为 null)
|
|
|
|
|
|
- 字段为具体值:更新为该值
|
2026-07-02 03:29:06 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
2026-07-04 00:16:00 +08:00
|
|
|
|
peer_name: str | None = Field(default=None, description="对端名称(未提供=不修改,null=清空)")
|
|
|
|
|
|
reason: str | None = Field(default=None, description="加白原因(未提供=不修改,null=清空)")
|
2026-07-02 03:29:06 +08:00
|
|
|
|
expires_at: str | None = Field(
|
|
|
|
|
|
default=None,
|
2026-07-04 00:16:00 +08:00
|
|
|
|
description="过期时间(ISO 8601,未提供=不修改,null=设为永久)",
|
2026-07-02 03:29:06 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WhitelistBatchItem(BaseModel):
|
|
|
|
|
|
"""批量导入白名单条目项。"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
2026-07-04 00:16:00 +08:00
|
|
|
|
peer_id: str = Field(..., min_length=1, description="对端 ID(非空)")
|
2026-07-02 03:29:06 +08:00
|
|
|
|
peer_name: str | None = Field(default=None, description="对端名称")
|
|
|
|
|
|
reason: str | None = Field(default=None, description="加白原因")
|
|
|
|
|
|
expires_at: str | None = Field(default=None, description="过期时间(ISO 8601)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WhitelistBatchRequest(BaseModel):
|
|
|
|
|
|
"""批量导入白名单请求。
|
|
|
|
|
|
|
|
|
|
|
|
``items`` 长度上限 500(规范 §2.3.5),``stop_on_error`` 默认 False
|
|
|
|
|
|
跳过失败项继续导入。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
|
|
items: list[WhitelistBatchItem] = Field(
|
|
|
|
|
|
...,
|
|
|
|
|
|
min_length=1,
|
|
|
|
|
|
max_length=500,
|
|
|
|
|
|
description="批量条目(上限 500)",
|
|
|
|
|
|
)
|
|
|
|
|
|
stop_on_error: bool = Field(
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
description="是否在首条失败时终止;默认 False 跳过失败项继续导入",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WhitelistBatchDeleteRequest(BaseModel):
|
|
|
|
|
|
"""批量删除白名单条目请求(ALW-BATCH-DEL)。"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
|
|
peer_ids: list[str] = Field(
|
|
|
|
|
|
...,
|
|
|
|
|
|
min_length=1,
|
|
|
|
|
|
max_length=500,
|
|
|
|
|
|
description="待删除对端 ID 列表(上限 500)",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WhitelistCleanExpiredRequest(BaseModel):
|
|
|
|
|
|
"""清理过期白名单条目请求(ALW-CLEAN-EXPIRED)。"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
|
|
older_than: str | None = Field(
|
|
|
|
|
|
default=None,
|
|
|
|
|
|
description="过期阈值(ISO 8601),缺失取当前时间",
|
|
|
|
|
|
)
|
|
|
|
|
|
max_count: int = Field(
|
|
|
|
|
|
default=500,
|
|
|
|
|
|
ge=1,
|
|
|
|
|
|
le=1000,
|
|
|
|
|
|
description="单次最大清理数(1-1000,默认 500)",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@allowlist_router.get(
|
|
|
|
|
|
"/{channel_type}/{account_id}/allowlist/{policy_type}",
|
|
|
|
|
|
response_model=dict,
|
|
|
|
|
|
)
|
|
|
|
|
|
async def list_whitelist(
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
account_id: str,
|
|
|
|
|
|
policy_type: str,
|
|
|
|
|
|
request: Request,
|
2026-07-04 00:16:00 +08:00
|
|
|
|
limit: int = Query(default=100, ge=1, le=1000, description="分页大小(1-1000,默认 100)"),
|
|
|
|
|
|
offset: int = Query(default=0, ge=0, description="偏移量(>=0,默认 0)"),
|
|
|
|
|
|
keyword: str | None = Query(default=None, description="关键词(模糊匹配 peer_id / peer_name)"),
|
2026-07-02 03:29:06 +08:00
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
2026-07-04 00:16:00 +08:00
|
|
|
|
"""查询白名单条目列表(FR-18 / AC-42),支持分页与关键词过滤。
|
2026-07-02 03:29:06 +08:00
|
|
|
|
|
|
|
|
|
|
``policy_type`` 非法时返回 400。对应控制面操作 ``whitelist/list``。
|
2026-07-04 00:16:00 +08:00
|
|
|
|
返回结构 ``{"whitelist": [...], "total": int, "limit": int, "offset": int}``。
|
2026-07-02 03:29:06 +08:00
|
|
|
|
"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
|
|
|
|
|
policy = _parse_policy_type(policy_type)
|
|
|
|
|
|
result = await use_cases.whitelist_management.listWhitelist(
|
|
|
|
|
|
channel_type=channel_type,
|
|
|
|
|
|
account_id=account_id,
|
|
|
|
|
|
policy_type=policy,
|
|
|
|
|
|
operator=operator,
|
2026-07-04 00:16:00 +08:00
|
|
|
|
limit=limit,
|
|
|
|
|
|
offset=offset,
|
|
|
|
|
|
keyword=keyword,
|
2026-07-02 03:29:06 +08:00
|
|
|
|
)
|
|
|
|
|
|
raiseOnControlFailure(result)
|
|
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@allowlist_router.post(
|
|
|
|
|
|
"/{channel_type}/{account_id}/allowlist/{policy_type}",
|
|
|
|
|
|
response_model=dict,
|
|
|
|
|
|
)
|
|
|
|
|
|
async def add_whitelist_entry(
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
account_id: str,
|
|
|
|
|
|
policy_type: str,
|
|
|
|
|
|
payload: WhitelistAddRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""新增白名单条目(FR-18 / AC-20 / AC-42 / AC-65)。"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
|
|
|
|
|
policy = _parse_policy_type(policy_type)
|
|
|
|
|
|
expires_at = parse_datetime("expires_at", payload.expires_at)
|
|
|
|
|
|
entry = WhitelistEntry(
|
|
|
|
|
|
peer_id=payload.peer_id,
|
|
|
|
|
|
peer_type=policy,
|
|
|
|
|
|
peer_name=payload.peer_name,
|
|
|
|
|
|
reason=payload.reason,
|
|
|
|
|
|
expires_at=expires_at,
|
|
|
|
|
|
)
|
|
|
|
|
|
result = await use_cases.whitelist_management.addWhitelistEntry(
|
|
|
|
|
|
channel_type=channel_type,
|
|
|
|
|
|
account_id=account_id,
|
|
|
|
|
|
entry=entry,
|
|
|
|
|
|
operator=operator,
|
|
|
|
|
|
)
|
|
|
|
|
|
raiseOnControlFailure(result)
|
|
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@allowlist_router.post(
|
|
|
|
|
|
"/{channel_type}/{account_id}/allowlist/{policy_type}/batch",
|
|
|
|
|
|
response_model=dict,
|
|
|
|
|
|
)
|
|
|
|
|
|
async def batch_add_whitelist(
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
account_id: str,
|
|
|
|
|
|
policy_type: str,
|
|
|
|
|
|
payload: WhitelistBatchRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""批量导入白名单条目(FR-18)。
|
|
|
|
|
|
|
|
|
|
|
|
支持两种语义:
|
|
|
|
|
|
- ``stop_on_error=True``:首条失败即终止,整批回滚(按异常抛出)
|
|
|
|
|
|
- ``stop_on_error=False``(默认):跳过失败项,响应中返回成功与失败列表
|
|
|
|
|
|
|
|
|
|
|
|
单批上限 500 条,超过返回 422 ``VALIDATION_ERROR``。
|
|
|
|
|
|
"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
|
|
|
|
|
policy = _parse_policy_type(policy_type)
|
|
|
|
|
|
entries = tuple(
|
|
|
|
|
|
WhitelistEntry(
|
|
|
|
|
|
peer_id=item.peer_id,
|
|
|
|
|
|
peer_type=policy,
|
|
|
|
|
|
peer_name=item.peer_name,
|
|
|
|
|
|
reason=item.reason,
|
2026-07-04 00:16:00 +08:00
|
|
|
|
expires_at=parse_datetime("expires_at", item.expires_at),
|
2026-07-02 03:29:06 +08:00
|
|
|
|
)
|
|
|
|
|
|
for item in payload.items
|
|
|
|
|
|
)
|
|
|
|
|
|
result = await use_cases.whitelist_management.batchAddWhitelistEntries(
|
|
|
|
|
|
channel_type=channel_type,
|
|
|
|
|
|
account_id=account_id,
|
|
|
|
|
|
policy_type=policy,
|
|
|
|
|
|
entries=entries,
|
|
|
|
|
|
stop_on_error=payload.stop_on_error,
|
|
|
|
|
|
operator=operator,
|
|
|
|
|
|
)
|
|
|
|
|
|
raiseOnControlFailure(result)
|
|
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@allowlist_router.get(
|
|
|
|
|
|
"/{channel_type}/{account_id}/allowlist/{policy_type}/export",
|
|
|
|
|
|
response_model=dict,
|
|
|
|
|
|
)
|
|
|
|
|
|
async def export_whitelist(
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
account_id: str,
|
|
|
|
|
|
policy_type: str,
|
|
|
|
|
|
request: Request,
|
2026-07-06 20:50:03 +08:00
|
|
|
|
export_format: str = Query(default="json", alias="format", description="导出格式:json / csv"),
|
2026-07-02 03:29:06 +08:00
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""导出白名单条目(FR-18)。
|
|
|
|
|
|
|
|
|
|
|
|
支持 ``json`` / ``csv`` 两种格式:
|
|
|
|
|
|
- ``json``:返回结构化 JSON 数组
|
|
|
|
|
|
- ``csv``:返回 ``data.content`` 为 CSV 字符串,``data.filename`` 为建议文件名
|
|
|
|
|
|
"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
|
|
|
|
|
policy = _parse_policy_type(policy_type)
|
2026-07-04 00:16:00 +08:00
|
|
|
|
fmt = _parse_export_format(export_format)
|
2026-07-02 03:29:06 +08:00
|
|
|
|
result = await use_cases.whitelist_management.exportWhitelist(
|
|
|
|
|
|
channel_type=channel_type,
|
|
|
|
|
|
account_id=account_id,
|
|
|
|
|
|
policy_type=policy,
|
2026-07-04 00:16:00 +08:00
|
|
|
|
output_format=fmt,
|
2026-07-02 03:29:06 +08:00
|
|
|
|
operator=operator,
|
|
|
|
|
|
)
|
|
|
|
|
|
raiseOnControlFailure(result)
|
|
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@allowlist_router.post(
|
|
|
|
|
|
"/{channel_type}/{account_id}/allowlist/{policy_type}/batch-delete",
|
|
|
|
|
|
response_model=dict,
|
|
|
|
|
|
)
|
|
|
|
|
|
async def batch_delete_whitelist(
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
account_id: str,
|
|
|
|
|
|
policy_type: str,
|
|
|
|
|
|
payload: WhitelistBatchDeleteRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""批量删除白名单条目(ALW-BATCH-DEL)。
|
|
|
|
|
|
|
|
|
|
|
|
逐条独立事务删除(模式 D 部分成功语义),复用单条 ``whitelist/remove``
|
|
|
|
|
|
逻辑。单批上限 500 条,单条失败不回滚已成功条目。对应控制面操作
|
|
|
|
|
|
``whitelist/batch_delete``。
|
|
|
|
|
|
|
|
|
|
|
|
响应:``{total, deleted: [peer_id, ...], failed: [{id, error_code, message}, ...]}``。
|
|
|
|
|
|
"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
|
|
|
|
|
policy = _parse_policy_type(policy_type)
|
|
|
|
|
|
cmd = BatchWhitelistDeleteCmd(
|
|
|
|
|
|
channel_type=channel_type,
|
|
|
|
|
|
account_id=account_id,
|
|
|
|
|
|
policy_type=policy,
|
|
|
|
|
|
operator=operator,
|
|
|
|
|
|
peer_ids=tuple(payload.peer_ids),
|
|
|
|
|
|
)
|
|
|
|
|
|
result = await use_cases.whitelist_management.batchDeleteWhitelist(cmd)
|
|
|
|
|
|
raiseOnControlFailure(result)
|
|
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@allowlist_router.post(
|
|
|
|
|
|
"/{channel_type}/{account_id}/allowlist/{policy_type}/clean-expired",
|
|
|
|
|
|
response_model=dict,
|
|
|
|
|
|
)
|
|
|
|
|
|
async def clean_expired_whitelist(
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
account_id: str,
|
|
|
|
|
|
policy_type: str,
|
|
|
|
|
|
payload: WhitelistCleanExpiredRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""清理过期白名单条目(ALW-CLEAN-EXPIRED)。
|
|
|
|
|
|
|
|
|
|
|
|
查询过期白名单条目(``expires_at < older_than``,缺省取当前时间)并
|
|
|
|
|
|
批量删除(单一事务 COOPERATIVE fail-closed)。``max_count`` 限制单次
|
|
|
|
|
|
最大清理数(1-1000,默认 500)。对应控制面操作 ``whitelist/clean_expired``。
|
|
|
|
|
|
|
|
|
|
|
|
响应:``{total, cleaned}``。
|
|
|
|
|
|
"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
|
|
|
|
|
policy = _parse_policy_type(policy_type)
|
|
|
|
|
|
cmd = CleanExpiredWhitelistCmd(
|
|
|
|
|
|
channel_type=channel_type,
|
|
|
|
|
|
account_id=account_id,
|
|
|
|
|
|
policy_type=policy,
|
|
|
|
|
|
operator=operator,
|
|
|
|
|
|
older_than=parse_datetime("older_than", payload.older_than),
|
|
|
|
|
|
max_count=payload.max_count,
|
|
|
|
|
|
)
|
|
|
|
|
|
result = await use_cases.whitelist_management.cleanExpiredWhitelist(cmd)
|
|
|
|
|
|
raiseOnControlFailure(result)
|
|
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-04 00:16:00 +08:00
|
|
|
|
@allowlist_router.patch(
|
2026-07-02 03:29:06 +08:00
|
|
|
|
"/{channel_type}/{account_id}/allowlist/{policy_type}/{peer_id}",
|
|
|
|
|
|
response_model=dict,
|
|
|
|
|
|
)
|
|
|
|
|
|
async def update_whitelist_entry(
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
account_id: str,
|
|
|
|
|
|
policy_type: str,
|
|
|
|
|
|
peer_id: str,
|
|
|
|
|
|
payload: WhitelistUpdateRequest,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
2026-07-04 00:16:00 +08:00
|
|
|
|
"""修改白名单条目(FR-18 / AC-41 / AC-42,PATCH 三态语义)。
|
2026-07-02 03:29:06 +08:00
|
|
|
|
|
|
|
|
|
|
支持修改 ``peer_name`` / ``reason`` / ``expires_at`` 三字段;
|
2026-07-04 00:16:00 +08:00
|
|
|
|
``peer_id`` 与 ``policy_type`` 为路径参数,不可变更。条目不存在时
|
|
|
|
|
|
返回 404(AC-41)。
|
|
|
|
|
|
|
|
|
|
|
|
字段三态语义:
|
|
|
|
|
|
- 字段未提供(不在请求体中):不修改,保留原值
|
|
|
|
|
|
- 字段显式为 ``null``:清除原值(``expires_at=null`` 设为永久)
|
|
|
|
|
|
- 字段为具体值:更新为该值
|
|
|
|
|
|
|
|
|
|
|
|
通过 ``model_dump(exclude_unset=True)`` 区分"未提供"与"显式 null",
|
|
|
|
|
|
映射为 ``WhitelistUpdateCmd`` 的 ``UNSET`` / ``None`` / 具体值三态。
|
2026-07-02 03:29:06 +08:00
|
|
|
|
"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
|
|
|
|
|
policy = _parse_policy_type(policy_type)
|
2026-07-04 00:16:00 +08:00
|
|
|
|
explicit = payload.model_dump(exclude_unset=True)
|
|
|
|
|
|
|
|
|
|
|
|
peer_name: str | None | UnsetType = UNSET
|
|
|
|
|
|
if "peer_name" in explicit:
|
|
|
|
|
|
peer_name = explicit["peer_name"]
|
|
|
|
|
|
|
|
|
|
|
|
reason: str | None | UnsetType = UNSET
|
|
|
|
|
|
if "reason" in explicit:
|
|
|
|
|
|
reason = explicit["reason"]
|
|
|
|
|
|
|
|
|
|
|
|
expires_at: datetime | None | UnsetType = UNSET
|
|
|
|
|
|
if "expires_at" in explicit:
|
|
|
|
|
|
# ``parse_datetime`` 接受 None 返回 None(清除语义),接受字符串解析为 UTC datetime
|
|
|
|
|
|
expires_at = parse_datetime("expires_at", explicit["expires_at"])
|
|
|
|
|
|
|
|
|
|
|
|
cmd = WhitelistUpdateCmd(
|
2026-07-02 03:29:06 +08:00
|
|
|
|
channel_type=channel_type,
|
|
|
|
|
|
account_id=account_id,
|
|
|
|
|
|
peer_id=peer_id,
|
|
|
|
|
|
policy_type=policy,
|
|
|
|
|
|
operator=operator,
|
2026-07-04 00:16:00 +08:00
|
|
|
|
peer_name=peer_name,
|
|
|
|
|
|
reason=reason,
|
|
|
|
|
|
expires_at=expires_at,
|
2026-07-02 03:29:06 +08:00
|
|
|
|
)
|
2026-07-04 00:16:00 +08:00
|
|
|
|
result = await use_cases.whitelist_management.updateWhitelistEntry(cmd)
|
2026-07-02 03:29:06 +08:00
|
|
|
|
raiseOnControlFailure(result)
|
|
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@allowlist_router.delete(
|
|
|
|
|
|
"/{channel_type}/{account_id}/allowlist/{policy_type}/{peer_id}",
|
|
|
|
|
|
response_model=dict,
|
|
|
|
|
|
)
|
|
|
|
|
|
async def remove_whitelist_entry(
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
account_id: str,
|
|
|
|
|
|
policy_type: str,
|
|
|
|
|
|
peer_id: str,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""删除白名单条目(FR-18 / AC-41 / AC-42)。
|
|
|
|
|
|
|
|
|
|
|
|
``peer_id`` 与 ``policy_type`` 为路径参数,联合定位唯一条目。
|
|
|
|
|
|
条目不存在时返回 404(AC-41);删除成功后立即触发 ``ConfigChanged``
|
|
|
|
|
|
事件,``WhitelistRegistry`` 同步移除(<1s,FR-37)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
|
|
|
|
|
policy = _parse_policy_type(policy_type)
|
|
|
|
|
|
result = await use_cases.whitelist_management.removeWhitelistEntry(
|
|
|
|
|
|
channel_type=channel_type,
|
|
|
|
|
|
account_id=account_id,
|
|
|
|
|
|
peer_id=peer_id,
|
|
|
|
|
|
policy_type=policy,
|
|
|
|
|
|
operator=operator,
|
|
|
|
|
|
)
|
|
|
|
|
|
raiseOnControlFailure(result)
|
2026-07-04 00:16:00 +08:00
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|