From aabde54f2e888b50736877f2528407eb47e6b524 Mon Sep 17 00:00:00 2001 From: Kris <2893855659@qq.com> Date: Sat, 4 Jul 2026 00:16:45 +0800 Subject: [PATCH] =?UTF-8?q?refactor(routers):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=89=80=E6=9C=89=E5=A4=96=E9=83=A8=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E6=8E=A5=E5=8F=A3=E7=9A=84=E5=8F=82=E6=95=B0=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E5=92=8C=E7=B1=BB=E5=9E=8B=E7=BA=A6=E6=9D=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 为所有查询参数添加max_length长度限制,规范参数输入范围 2. 使用Literal类型替换普通字符串参数,限定合法取值范围 3. 为路径参数添加Path校验,确保ID参数合法有效 4. 优化请求体参数的声明,补充缺失的Body注解和校验规则 5. 统一分页参数的offset/limit使用方式,替换旧的page/page_size模式 --- .../external_systems/access_rule_router.py | 30 ++++---- .../routers/external_systems/alert_router.py | 74 ++++++++++--------- .../routers/external_systems/asset_router.py | 24 +++--- .../external_systems/audit_log_router.py | 40 +++++----- .../external_systems/dashboard_router.py | 7 +- .../external_systems/execution_router.py | 48 ++++++------ .../external_systems/health_check_router.py | 27 +++---- .../external_systems/integration_router.py | 8 +- .../integration_tool_router.py | 6 +- .../routers/external_systems/metric_router.py | 22 +++--- .../notification_channel_router.py | 12 +-- .../routers/external_systems/quota_router.py | 38 +++++----- .../external_systems/test_case_router.py | 30 ++++---- .../external_systems/webhook_router.py | 46 ++++++------ 14 files changed, 214 insertions(+), 198 deletions(-) diff --git a/backend/server/routers/external_systems/access_rule_router.py b/backend/server/routers/external_systems/access_rule_router.py index 6cad4152..bb050080 100644 --- a/backend/server/routers/external_systems/access_rule_router.py +++ b/backend/server/routers/external_systems/access_rule_router.py @@ -13,7 +13,7 @@ from __future__ import annotations from datetime import datetime from typing import Any, Literal -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Depends, Path, Query from pydantic import BaseModel, ConfigDict, Field from sqlalchemy.ext.asyncio import AsyncSession from yuxi.external_systems.infrastructure.container import create_use_cases_from_db @@ -115,7 +115,7 @@ class BatchToggleAccessRulesRequest(BaseModel): model_config = ConfigDict(frozen=True) - rule_ids: list[int] = Field(min_length=1) + rule_ids: list[int] = Field(..., min_length=1, max_length=100) enabled: bool @@ -133,7 +133,7 @@ class ReorderAccessRulesRequest(BaseModel): model_config = ConfigDict(frozen=True) - orders: list[ReorderItem] = Field(min_length=1) + orders: list[ReorderItem] = Field(..., min_length=1, max_length=100) class UpdateConditionsRequest(BaseModel): @@ -149,7 +149,7 @@ class BatchCloneAccessRulesRequest(BaseModel): model_config = ConfigDict(frozen=True) - source_rule_ids: list[int] = Field(min_length=1) + source_rule_ids: list[int] = Field(..., min_length=1, max_length=100) override_config: dict[str, Any] = Field(default_factory=dict) @@ -160,12 +160,12 @@ class BatchCloneAccessRulesRequest(BaseModel): async def list_access_rules( limit: int = Query(100, ge=1, le=500), offset: int = Query(0, ge=0), - system_id: int | None = Query(None), - tool_slug: str | None = Query(None), - principal_type: str | None = Query(None), - principal_id: str | None = Query(None), - env_key: str | None = Query(None), - effect: str | None = Query(None), + system_id: int | None = Query(None, ge=1), + tool_slug: str | None = Query(None, max_length=128), + principal_type: str | None = Query(None, max_length=32), + principal_id: str | None = Query(None, max_length=64), + env_key: str | None = Query(None, max_length=32), + effect: Literal["allow", "deny"] | None = Query(None, description="规则效果:allow/deny"), enabled: bool | None = Query(None), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), @@ -296,7 +296,7 @@ async def batch_clone_access_rules( @access_rule_router.get("/{rule_id}", response_model=dict) async def get_access_rule( - rule_id: int, + rule_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -309,8 +309,8 @@ async def get_access_rule( @access_rule_router.put("/{rule_id}", response_model=dict) async def update_access_rule( - rule_id: int, payload: UpdateAccessRuleRequest, + rule_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -327,7 +327,7 @@ async def update_access_rule( @access_rule_router.delete("/{rule_id}", response_model=dict) async def delete_access_rule( - rule_id: int, + rule_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -343,7 +343,7 @@ async def delete_access_rule( @access_rule_router.get("/{rule_id}/conditions", response_model=dict) async def get_conditions( - rule_id: int, + rule_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -356,8 +356,8 @@ async def get_conditions( @access_rule_router.put("/{rule_id}/conditions", response_model=dict) async def update_conditions( - rule_id: int, payload: UpdateConditionsRequest, + rule_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: diff --git a/backend/server/routers/external_systems/alert_router.py b/backend/server/routers/external_systems/alert_router.py index 58939713..eb1f8998 100644 --- a/backend/server/routers/external_systems/alert_router.py +++ b/backend/server/routers/external_systems/alert_router.py @@ -14,9 +14,9 @@ dedup_key 过滤参数。Request Schema 与 Input DTO 不共享类,Router 内 from __future__ import annotations from datetime import datetime -from typing import Any +from typing import Any, Literal -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Body, Depends, Path, Query from pydantic import BaseModel, ConfigDict, Field from sqlalchemy.ext.asyncio import AsyncSession from yuxi.external_systems.infrastructure.container import create_use_cases_from_db @@ -48,20 +48,20 @@ class FireAlertRequest(BaseModel): model_config = ConfigDict(frozen=True) - system_id: int | None - alert_type: str - title: str + system_id: int | None = Field(default=None, ge=1) + alert_type: str = Field(..., min_length=1, max_length=64) + title: str = Field(..., min_length=1, max_length=256) triggered_at: datetime - env_key: str | None = None - severity: str = "warning" + env_key: str | None = Field(default=None, max_length=32) + severity: Literal["info", "warning", "critical", "fatal"] = "warning" description: str | None = None detail: dict[str, Any] = Field(default_factory=dict) - resource_type: str | None = None - resource_id: str | None = None - related_execution_id: str | None = None - related_trace_id: str | None = None + resource_type: str | None = Field(default=None, max_length=32) + resource_id: str | None = Field(default=None, max_length=128) + related_execution_id: str | None = Field(default=None, max_length=64) + related_trace_id: str | None = Field(default=None, max_length=64) metric_snapshot: dict[str, Any] | None = None - dedup_key: str | None = None + dedup_key: str | None = Field(default=None, max_length=256) class AcknowledgeAlertRequest(BaseModel): @@ -85,7 +85,7 @@ class BatchAcknowledgeAlertsRequest(BaseModel): model_config = ConfigDict(frozen=True) - alert_ids: list[int] + alert_ids: list[int] = Field(..., min_length=1, max_length=100) note: str | None = None @@ -94,7 +94,7 @@ class BatchResolveAlertsRequest(BaseModel): model_config = ConfigDict(frozen=True) - alert_ids: list[int] + alert_ids: list[int] = Field(..., min_length=1, max_length=100) resolution_note: str | None = None @@ -105,18 +105,22 @@ class BatchResolveAlertsRequest(BaseModel): async def list_alerts( limit: int = Query(100, ge=1, le=500), offset: int = Query(0, ge=0), - system_id: int | None = Query(None), - env_key: str | None = Query(None), - alert_type: str | None = Query(None), - severity: str | None = Query(None), - status: str | None = Query(None), - start_at: datetime | None = Query(None, description="ISO8601 开始时间"), - end_at: datetime | None = Query(None, description="ISO8601 结束时间"), + system_id: int | None = Query(None, ge=1), + env_key: str | None = Query(None, max_length=32), + alert_type: str | None = Query(None, max_length=64), + severity: Literal["info", "warning", "critical", "fatal"] | None = Query( + None, description="严重度:info/warning/critical/fatal" + ), + status: Literal["firing", "acknowledged", "resolved", "suppressed"] | None = Query( + None, description="告警状态:firing/acknowledged/resolved/suppressed" + ), + start_time: datetime | None = Query(None, description="ISO8601 开始时间"), + end_time: datetime | None = Query(None, description="ISO8601 结束时间"), # 新增查询参数 - resource_type: str | None = Query(None), - resource_id: str | None = Query(None), - related_trace_id: str | None = Query(None), - dedup_key: str | None = Query(None), + resource_type: str | None = Query(None, max_length=32), + resource_id: str | None = Query(None, max_length=128), + related_trace_id: str | None = Query(None, max_length=64), + dedup_key: str | None = Query(None, max_length=256), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -130,8 +134,8 @@ async def list_alerts( alert_type=alert_type, severity=severity, status=status, - start_at=start_at, - end_at=end_at, + start_at=start_time, + end_at=end_time, resource_type=resource_type, resource_id=resource_id, related_trace_id=related_trace_id, @@ -143,7 +147,7 @@ async def list_alerts( @alert_router.get("/stats", response_model=dict) async def get_alert_stats( - system_id: int | None = Query(None), + system_id: int | None = Query(None, ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -209,7 +213,7 @@ async def batch_resolve_alerts( @alert_router.get("/{alert_id}", response_model=dict) async def get_alert( - alert_id: int, + alert_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -235,8 +239,8 @@ async def fire_alert( @alert_router.post("/{alert_id}/acknowledge", response_model=dict) async def acknowledge_alert( - alert_id: int, - payload: AcknowledgeAlertRequest, + alert_id: int = Path(ge=1), + payload: AcknowledgeAlertRequest = Body(...), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -253,8 +257,8 @@ async def acknowledge_alert( @alert_router.post("/{alert_id}/resolve", response_model=dict) async def resolve_alert( - alert_id: int, - payload: ResolveAlertRequest, + alert_id: int = Path(ge=1), + payload: ResolveAlertRequest = Body(...), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -271,7 +275,7 @@ async def resolve_alert( @alert_router.get("/{alert_id}/context", response_model=dict) async def get_alert_context( - alert_id: int, + alert_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -284,7 +288,7 @@ async def get_alert_context( @alert_router.get("/{alert_id}/notifications", response_model=dict) async def get_alert_notifications( - alert_id: int, + alert_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: diff --git a/backend/server/routers/external_systems/asset_router.py b/backend/server/routers/external_systems/asset_router.py index 1abac5de..b8776bcb 100644 --- a/backend/server/routers/external_systems/asset_router.py +++ b/backend/server/routers/external_systems/asset_router.py @@ -161,13 +161,13 @@ class ProbeAssetRequest(BaseModel): async def list_assets( limit: int = Query(100, ge=1, le=500), offset: int = Query(0, ge=0), - adapter_type: str | None = Query(None), - asset_type: str | None = Query(None), - status: str | None = Query(None), - keyword: str | None = Query(None), + adapter_type: str | None = Query(None, max_length=32), + asset_type: str | None = Query(None, max_length=32), + status: str | None = Query(None, max_length=32), + keyword: str | None = Query(None, max_length=128), start_time: datetime | None = Query(None, description="创建时间起始(UTC ISO 8601)"), end_time: datetime | None = Query(None, description="创建时间结束(UTC ISO 8601)"), - created_by: str | None = Query(None, description="按创建人过滤"), + created_by: str | None = Query(None, max_length=64, description="按创建人过滤"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -334,7 +334,7 @@ async def probe_asset( @asset_router.get("/{asset_id}", response_model=dict) async def get_asset( - asset_id: int, + asset_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -347,8 +347,8 @@ async def get_asset( @asset_router.put("/{asset_id}", response_model=dict) async def update_asset( - asset_id: int, payload: UpdateAssetRequest, + asset_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -369,7 +369,7 @@ async def update_asset( @asset_router.delete("/{asset_id}", response_model=dict) async def delete_asset( - asset_id: int, + asset_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -382,7 +382,7 @@ async def delete_asset( @asset_router.post("/{asset_id}/validate", response_model=dict) async def validate_asset( - asset_id: int, + asset_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -395,7 +395,7 @@ async def validate_asset( @asset_router.get("/{asset_id}/preview", response_model=dict) async def preview_asset( - asset_id: int, + asset_id: int = Path(ge=1), max_lines: int = Query(200, ge=1, le=10000), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), @@ -414,7 +414,7 @@ async def preview_asset( @asset_router.get("/{asset_id}/generate-preview", response_model=dict) async def generate_preview( - asset_id: int, + asset_id: int = Path(ge=1), max_lines: int = Query(200, ge=1, le=10000), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), @@ -433,7 +433,7 @@ async def generate_preview( @asset_router.get("/{asset_id}/references", response_model=dict) async def get_asset_references( - asset_id: int, + asset_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: diff --git a/backend/server/routers/external_systems/audit_log_router.py b/backend/server/routers/external_systems/audit_log_router.py index b2419a71..9ef8c32a 100644 --- a/backend/server/routers/external_systems/audit_log_router.py +++ b/backend/server/routers/external_systems/audit_log_router.py @@ -13,7 +13,7 @@ import json from datetime import UTC, datetime from typing import Any -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Depends, Path, Query from fastapi.responses import StreamingResponse from sqlalchemy.ext.asyncio import AsyncSession from yuxi.external_systems.infrastructure.container import create_use_cases_from_db @@ -44,12 +44,12 @@ audit_log_router = APIRouter( @audit_log_router.get("/export") async def export_audit_logs( - system_id: int | None = Query(None), - env_key: str | None = Query(None), - event_type: str | None = Query(None), - action_type: str | None = Query(None), - resource: str | None = Query(None), - user: str | None = Query(None), + system_id: int | None = Query(None, ge=1), + env_key: str | None = Query(None, max_length=32), + event_type: str | None = Query(None, max_length=64), + action_type: str | None = Query(None, max_length=64), + resource: str | None = Query(None, max_length=256), + user: str | None = Query(None, max_length=64), start_time: datetime | None = Query(None), end_time: datetime | None = Query(None), db: AsyncSession = Depends(get_db), @@ -95,12 +95,12 @@ async def export_audit_logs( async def list_audit_logs( limit: int = Query(100, ge=1, le=500), offset: int = Query(0, ge=0), - system_id: int | None = Query(None), - env_key: str | None = Query(None), - event_type: str | None = Query(None), - action_type: str | None = Query(None), - resource: str | None = Query(None), - user: str | None = Query(None), + system_id: int | None = Query(None, ge=1), + env_key: str | None = Query(None, max_length=32), + event_type: str | None = Query(None, max_length=64), + action_type: str | None = Query(None, max_length=64), + resource: str | None = Query(None, max_length=256), + user: str | None = Query(None, max_length=64), start_time: datetime | None = Query(None), end_time: datetime | None = Query(None), db: AsyncSession = Depends(get_db), @@ -126,7 +126,7 @@ async def list_audit_logs( @audit_log_router.get("/stats", response_model=dict) async def get_audit_log_stats( - system_id: int | None = Query(None), + system_id: int | None = Query(None, ge=1), start_time: datetime | None = Query(None), end_time: datetime | None = Query(None), db: AsyncSession = Depends(get_db), @@ -147,11 +147,11 @@ async def get_audit_log_stats( async def list_config_changes( limit: int = Query(100, ge=1, le=500), offset: int = Query(0, ge=0), - system_id: int | None = Query(None), - env_key: str | None = Query(None), - action_type: str | None = Query(None), - resource: str | None = Query(None), - user: str | None = Query(None), + system_id: int | None = Query(None, ge=1), + env_key: str | None = Query(None, max_length=32), + action_type: str | None = Query(None, max_length=64), + resource: str | None = Query(None, max_length=256), + user: str | None = Query(None, max_length=64), start_time: datetime | None = Query(None), end_time: datetime | None = Query(None), db: AsyncSession = Depends(get_db), @@ -220,7 +220,7 @@ async def list_operators( @audit_log_router.get("/{log_id}", response_model=dict) async def get_audit_log_detail( - log_id: int, + log_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: diff --git a/backend/server/routers/external_systems/dashboard_router.py b/backend/server/routers/external_systems/dashboard_router.py index d4ceb4a6..0080ceae 100644 --- a/backend/server/routers/external_systems/dashboard_router.py +++ b/backend/server/routers/external_systems/dashboard_router.py @@ -16,7 +16,7 @@ from __future__ import annotations from datetime import datetime -from typing import Any +from typing import Any, Literal from fastapi import APIRouter, Depends, Query from sqlalchemy.ext.asyncio import AsyncSession @@ -87,10 +87,9 @@ async def get_executions_stats( @dashboard_router.get("/alerts", response_model=dict) async def get_alerts_stats( - severity_status: str = Query( + severity_status: Literal["firing", "acknowledged", "resolved", "suppressed"] = Query( "firing", - description="count_by_severity 的状态过滤", - pattern="^(firing|acknowledged|resolved|suppressed)$", + description="count_by_severity 的状态过滤,告警状态:firing/acknowledged/resolved/suppressed", ), top_limit: int = Query(10, ge=1, le=50, description="Top 告警去重键数量"), db: AsyncSession = Depends(get_db), diff --git a/backend/server/routers/external_systems/execution_router.py b/backend/server/routers/external_systems/execution_router.py index 4b6ab45e..cc0b60a4 100644 --- a/backend/server/routers/external_systems/execution_router.py +++ b/backend/server/routers/external_systems/execution_router.py @@ -53,7 +53,7 @@ class CleanupExecutionsRequest(BaseModel): system_id: int | None = None before_at: datetime | None = None - status: str | None = None + status: Literal["pending", "running", "success", "failed", "timeout", "throttled", "cancelled"] | None = None batch_size: int = Field(default=1000, ge=1, le=10000) @@ -84,17 +84,19 @@ async def cleanup_old_executions( async def paginate_executions( limit: int = Query(100, ge=1, le=500), offset: int = Query(0, ge=0), - system_id: int | None = Query(None), - env_key: str | None = Query(None), - tool_slug: str | None = Query(None), - status: str | None = Query(None, description="按状态过滤:success/failed/running/timeout"), - caller: str | None = Query(None), - caller_id: str | None = Query(None, description="按调用方实体过滤"), - operation: str | None = Query(None, description="按操作类型过滤"), - tag_key: str | None = Query(None, description="按业务标签键过滤"), - tag_value: str | None = Query(None, description="按业务标签值过滤"), - trace_id: str | None = Query(None), - correlation_id: str | None = Query(None), + system_id: int | None = Query(None, ge=1), + env_key: str | None = Query(None, max_length=32), + tool_slug: str | None = Query(None, max_length=128), + status: Literal["pending", "running", "success", "failed", "timeout", "throttled", "cancelled"] | None = Query( + None, description="执行状态:pending/running/success/failed/timeout/throttled/cancelled" + ), + caller: str | None = Query(None, max_length=32), + caller_id: str | None = Query(None, max_length=64, description="按调用方实体过滤"), + operation: str | None = Query(None, max_length=32, description="按操作类型过滤"), + tag_key: str | None = Query(None, max_length=64, description="按业务标签键过滤"), + tag_value: str | None = Query(None, max_length=256, description="按业务标签值过滤"), + trace_id: str | None = Query(None, max_length=64), + correlation_id: str | None = Query(None, max_length=64), start_time: datetime | None = Query(None, description="ISO8601 开始时间"), end_time: datetime | None = Query(None, description="ISO8601 结束时间"), db: AsyncSession = Depends(get_db), @@ -130,9 +132,9 @@ async def paginate_executions( @execution_router.get("/stats", response_model=dict) async def get_execution_stats( - system_id: int | None = Query(None, description="系统过滤"), - env_key: str | None = Query(None, description="环境过滤"), - adapter_type: str | None = Query(None, description="适配器类型过滤"), + system_id: int | None = Query(None, ge=1, description="系统过滤"), + env_key: str | None = Query(None, max_length=32, description="环境过滤"), + adapter_type: str | None = Query(None, max_length=32, description="适配器类型过滤"), start_time: datetime | None = Query(None, description="起始时间"), end_time: datetime | None = Query(None, description="结束时间"), tool_limit: int = Query(20, ge=1, le=100, description="Top 工具数量"), @@ -156,9 +158,9 @@ async def get_execution_stats( @execution_router.get("/stats/grouped", response_model=dict) async def get_grouped_stats( group_by: Literal["tool"] = Query(..., description="分组维度,仅支持 tool"), - system_id: int | None = Query(None, description="系统过滤"), - env_key: str | None = Query(None, description="环境过滤"), - adapter_type: str | None = Query(None, description="适配器类型过滤"), + system_id: int | None = Query(None, ge=1, description="系统过滤"), + env_key: str | None = Query(None, max_length=32, description="环境过滤"), + adapter_type: str | None = Query(None, max_length=32, description="适配器类型过滤"), start_time: datetime | None = Query(None, description="起始时间"), end_time: datetime | None = Query(None, description="结束时间"), limit: int = Query(20, ge=1, le=100, description="返回数量"), @@ -181,7 +183,7 @@ async def get_grouped_stats( @execution_router.get("/stats/error-analysis", response_model=dict) async def get_error_analysis( - system_id: int = Query(..., description="系统 ID(必填)"), + system_id: int = Query(..., ge=1, description="系统 ID(必填)"), start_time: datetime | None = Query(None, description="起始时间"), end_time: datetime | None = Query(None, description="结束时间"), limit: int = Query(5, ge=1, le=50, description="返回数量"), @@ -202,9 +204,9 @@ async def get_error_analysis( @execution_router.get("/stats/trend", response_model=dict) async def get_execution_trend( - tool_slug: str = Query(..., description="工具标识(必填)"), - system_id: int | None = Query(None, description="系统过滤"), - env_key: str | None = Query(None, description="环境过滤"), + tool_slug: str = Query(..., min_length=1, max_length=128, description="工具标识(必填)"), + system_id: int | None = Query(None, ge=1, description="系统过滤"), + env_key: str | None = Query(None, max_length=32, description="环境过滤"), start_time: datetime | None = Query(None, description="起始时间"), end_time: datetime | None = Query(None, description="结束时间"), interval: Literal["hour", "day"] = Query("day", description="时间桶粒度:hour / day"), @@ -227,7 +229,7 @@ async def get_execution_trend( @execution_router.get("/slow", response_model=dict) async def list_slow_executions( - system_id: int = Query(..., description="系统 ID(必填)"), + system_id: int = Query(..., ge=1, description="系统 ID(必填)"), min_duration_ms: int = Query(..., ge=0, description="耗时阈值(毫秒,必填)"), limit: int = Query(50, ge=1, le=200, description="返回数量"), db: AsyncSession = Depends(get_db), diff --git a/backend/server/routers/external_systems/health_check_router.py b/backend/server/routers/external_systems/health_check_router.py index c6bf1e7a..161e39ec 100644 --- a/backend/server/routers/external_systems/health_check_router.py +++ b/backend/server/routers/external_systems/health_check_router.py @@ -17,9 +17,9 @@ from __future__ import annotations from datetime import datetime -from typing import Any +from typing import Any, Literal -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Depends, Path, Query from sqlalchemy.ext.asyncio import AsyncSession from yuxi.external_systems.infrastructure.container import create_use_cases_from_db from yuxi.external_systems.use_cases.dto.health_check import ( @@ -47,9 +47,11 @@ health_check_router = APIRouter(prefix="/health-checks", tags=["external-systems @health_check_router.get("/latest", response_model=dict) async def list_latest_health( system_ids: list[int] | None = Query(None, description="按系统 ID 过滤(逗号分隔)"), - health_status: str | None = Query(None, description="按健康状态过滤"), - page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), + health_status: Literal["healthy", "auth_failure", "connectivity_issue", "not_checked"] | None = Query( + None, description="健康状态:healthy/auth_failure/connectivity_issue/not_checked" + ), + limit: int = Query(20, ge=1, le=100), + offset: int = Query(0, ge=0), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -58,8 +60,8 @@ async def list_latest_health( input_dto = ListLatestHealthInput( system_ids=system_ids, health_status=health_status, - page=page, - page_size=page_size, + page=offset // limit + 1, + page_size=limit, ) output = await use_cases.health_check_service.list_latest(input_dto) return {"success": True, "data": output.model_dump()} @@ -80,10 +82,9 @@ async def count_by_status( @health_check_router.get("/failing", response_model=dict) async def list_failing( - health_status: str = Query( + health_status: Literal["auth_failure", "connectivity_issue"] = Query( "auth_failure", description="故障状态过滤:auth_failure / connectivity_issue", - pattern="^(auth_failure|connectivity_issue)$", ), system_ids: list[int] | None = Query(None, description="按系统 ID 过滤(逗号分隔)"), limit: int = Query(100, ge=1, le=500), @@ -132,8 +133,8 @@ async def delete_old_records( @health_check_router.get("/by-system/{system_id}", response_model=dict) async def list_by_system( - system_id: int, - env_key: str | None = Query(None, description="按环境过滤"), + system_id: int = Path(ge=1), + env_key: str | None = Query(None, max_length=32, description="按环境过滤"), limit: int = Query(20, ge=1, le=100), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), @@ -151,8 +152,8 @@ async def list_by_system( @health_check_router.get("/by-system/{system_id}/latest", response_model=dict) async def get_latest_by_system( - system_id: int, - env_key: str | None = Query(None, description="环境标识(不传则跨 env 取最近一条)"), + system_id: int = Path(ge=1), + env_key: str | None = Query(None, max_length=32, description="环境标识(不传则跨 env 取最近一条)"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: diff --git a/backend/server/routers/external_systems/integration_router.py b/backend/server/routers/external_systems/integration_router.py index 031bf0c9..355713da 100644 --- a/backend/server/routers/external_systems/integration_router.py +++ b/backend/server/routers/external_systems/integration_router.py @@ -38,9 +38,11 @@ _SUMMARY_EXCLUDED_FIELDS = ("connection_extra_schema", "example_config", "depend @integration_router.get("", response_model=dict) async def list_integrations( - adapter_type: str | None = Query(None, description="按适配器类型精确过滤"), - tags: str | None = Query(None, description="按标签过滤,多个标签以逗号分隔(OR 关系)"), - keyword: str | None = Query(None, description="模糊匹配 display_name / description(大小写不敏感)"), + adapter_type: str | None = Query(None, max_length=32, description="按适配器类型精确过滤"), + tags: str | None = Query(None, max_length=256, description="按标签过滤,多个标签以逗号分隔(OR 关系)"), + keyword: str | None = Query( + None, max_length=128, description="模糊匹配 display_name / description(大小写不敏感)" + ), limit: int = Query(50, ge=1, le=200, description="分页大小,最大 200"), offset: int = Query(0, ge=0, description="分页偏移"), current_user: User = Depends(get_required_user), diff --git a/backend/server/routers/external_systems/integration_tool_router.py b/backend/server/routers/external_systems/integration_tool_router.py index 8f9d5cae..da747121 100644 --- a/backend/server/routers/external_systems/integration_tool_router.py +++ b/backend/server/routers/external_systems/integration_tool_router.py @@ -101,10 +101,10 @@ async def list_generated_tools( limit: int = Query(100, ge=1, le=500), offset: int = Query(0, ge=0), system_id: int | None = Query(None), - category: str | None = Query(None), - adapter_type: str | None = Query(None), + category: str | None = Query(None, max_length=64), + adapter_type: str | None = Query(None, max_length=32), enabled: bool | None = Query(None), - keyword: str | None = Query(None), + keyword: str | None = Query(None, max_length=128), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: diff --git a/backend/server/routers/external_systems/metric_router.py b/backend/server/routers/external_systems/metric_router.py index a7f212f7..c49e4060 100644 --- a/backend/server/routers/external_systems/metric_router.py +++ b/backend/server/routers/external_systems/metric_router.py @@ -17,7 +17,7 @@ from __future__ import annotations from datetime import datetime -from typing import Any +from typing import Any, Literal from fastapi import APIRouter, Depends, Query, Response from sqlalchemy.ext.asyncio import AsyncSession @@ -48,8 +48,8 @@ metric_router = APIRouter(prefix="/metrics", tags=["external-systems-metric"]) @metric_router.get("/aggregate", response_model=dict) async def aggregate_metrics( system_id: int | None = Query(None), - env_key: str | None = Query(None), - adapter_type: str | None = Query(None), + env_key: str | None = Query(None, max_length=32), + adapter_type: str | None = Query(None, max_length=32), start_time: datetime | None = Query(None), end_time: datetime | None = Query(None), db: AsyncSession = Depends(get_db), @@ -71,11 +71,11 @@ async def aggregate_metrics( @metric_router.get("/timeseries", response_model=dict) async def get_metrics_timeseries( system_id: int | None = Query(None), - env_key: str | None = Query(None), - adapter_type: str | None = Query(None), + env_key: str | None = Query(None, max_length=32), + adapter_type: str | None = Query(None, max_length=32), start_time: datetime = Query(..., description="起始时间(必填)"), end_time: datetime = Query(..., description="结束时间(必填)"), - interval: str = Query("hour", pattern="^(hour|day)$", description="聚合间隔:hour / day"), + interval: Literal["hour", "day"] = Query("hour", description="聚合间隔:hour / day"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -95,8 +95,8 @@ async def get_metrics_timeseries( @metric_router.get("/by-system", response_model=dict) async def get_metrics_by_system( - env_key: str | None = Query(None), - adapter_type: str | None = Query(None), + env_key: str | None = Query(None, max_length=32), + adapter_type: str | None = Query(None, max_length=32), start_time: datetime = Query(..., description="起始时间(必填)"), end_time: datetime = Query(..., description="结束时间(必填)"), limit: int = Query(20, ge=1, le=100), @@ -138,7 +138,7 @@ async def get_metrics_by_env( @metric_router.get("/latest", response_model=dict) async def get_latest_metric_bucket( system_id: int = Query(..., description="系统 ID(必填)"), - env_key: str = Query("default", description="环境标识"), + env_key: str = Query("default", max_length=32, description="环境标识"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -153,7 +153,7 @@ async def get_latest_metric_bucket( async def export_metrics_prometheus( start_time: datetime | None = Query(None), end_time: datetime | None = Query(None), - env_key: str | None = Query(None), + env_key: str | None = Query(None, max_length=32), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> Response: @@ -183,7 +183,7 @@ async def delete_old_metric_buckets( @metric_router.get("", response_model=dict) async def list_metric_buckets( system_id: int | None = Query(None), - env_key: str | None = Query(None), + env_key: str | None = Query(None, max_length=32), start_time: datetime | None = Query(None), end_time: datetime | None = Query(None), limit: int = Query(20, ge=1, le=100), diff --git a/backend/server/routers/external_systems/notification_channel_router.py b/backend/server/routers/external_systems/notification_channel_router.py index 412b1d60..e2fdec60 100644 --- a/backend/server/routers/external_systems/notification_channel_router.py +++ b/backend/server/routers/external_systems/notification_channel_router.py @@ -17,7 +17,7 @@ from __future__ import annotations from typing import Any -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Depends, Path, Query from pydantic import BaseModel, ConfigDict, Field from sqlalchemy.ext.asyncio import AsyncSession from yuxi.external_systems.infrastructure.container import create_use_cases_from_db @@ -79,9 +79,9 @@ class UpdateNotificationChannelRequest(BaseModel): async def list_notification_channels( limit: int = Query(100, ge=1, le=500), offset: int = Query(0, ge=0), - channel_type: str | None = Query(None), + channel_type: str | None = Query(None, max_length=32), enabled: bool | None = Query(None), - keyword: str | None = Query(None), + keyword: str | None = Query(None, max_length=128), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -112,7 +112,7 @@ async def get_notification_channel_stats( @notification_channel_router.get("/{channel_id}", response_model=dict) async def get_notification_channel( - channel_id: int, + channel_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -145,8 +145,8 @@ async def create_notification_channel( @notification_channel_router.put("/{channel_id}", response_model=dict) async def update_notification_channel( - channel_id: int, payload: UpdateNotificationChannelRequest, + channel_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -163,7 +163,7 @@ async def update_notification_channel( @notification_channel_router.delete("/{channel_id}", response_model=dict) async def delete_notification_channel( - channel_id: int, + channel_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: diff --git a/backend/server/routers/external_systems/quota_router.py b/backend/server/routers/external_systems/quota_router.py index 05f412fe..265e0cab 100644 --- a/backend/server/routers/external_systems/quota_router.py +++ b/backend/server/routers/external_systems/quota_router.py @@ -17,9 +17,9 @@ Request Schema 与 Input DTO 不共享类,Router 内显式构造 DTO。 from __future__ import annotations from datetime import datetime -from typing import Any +from typing import Any, Literal -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Depends, Path, Query from pydantic import BaseModel, ConfigDict, Field from sqlalchemy.ext.asyncio import AsyncSession from yuxi.external_systems.infrastructure.container import create_use_cases_from_db @@ -170,8 +170,8 @@ class UpsertQuotaRequest(BaseModel): @quota_router.get("/thresholds", response_model=dict) async def get_quota_threshold( system_id: int = Query(...), - env_key: str | None = Query(None), - quota_key: str | None = Query(None), + env_key: str | None = Query(None, max_length=32), + quota_key: str | None = Query(None, max_length=128), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -192,8 +192,8 @@ async def get_quota_threshold( @quota_router.get("/by-key", response_model=dict) async def get_quota_by_key( system_id: int = Query(...), - env_key: str = Query("default"), - quota_key: str = Query(...), + env_key: str = Query("default", max_length=32), + quota_key: str = Query(..., max_length=128), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -318,7 +318,7 @@ async def upsert_quota( @quota_router.delete("/by-system/{system_id}", response_model=dict) async def delete_quotas_by_system( - system_id: int, + system_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -337,14 +337,18 @@ async def list_quotas( limit: int = Query(100, ge=1, le=500, description="返回记录数上限"), offset: int = Query(0, ge=0, description="偏移量(与 limit 配合分页)"), system_id: int | None = Query(None, description="按系统过滤"), - env_key: str | None = Query(None, description="按环境键过滤"), - quota_key: str | None = Query(None, description="按配额键过滤"), - quota_window: str | None = Query(None, description="按窗口类型过滤(day/month/total 等)"), - source: str | None = Query(None, description="按来源过滤(config/response_header 等)"), + env_key: str | None = Query(None, max_length=32, description="按环境键过滤"), + quota_key: str | None = Query(None, max_length=128, description="按配额键过滤"), + quota_window: Literal["daily", "hourly", "rolling_24h", "minute", "custom"] | None = Query( + None, description="窗口类型:daily/hourly/rolling_24h/minute/custom" + ), + source: Literal["response_header", "manual", "config"] | None = Query( + None, description="配额来源:response_header/manual/config" + ), near_warning: bool = Query(False, description="仅返回接近告警阈值的配额"), near_critical: bool = Query(False, description="仅返回接近临界阈值的配额"), expiring_before: datetime | None = Query(None, description="仅返回 window_end <= 该时间的配额"), - keyword: str | None = Query(None, description="按 quota_name / quota_key 模糊匹配"), + keyword: str | None = Query(None, max_length=128, description="按 quota_name / quota_key 模糊匹配"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -385,7 +389,7 @@ async def create_quota( @quota_router.get("/{quota_id}", response_model=dict) async def get_quota( - quota_id: int, + quota_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -398,8 +402,8 @@ async def get_quota( @quota_router.put("/{quota_id}", response_model=dict) async def update_quota( - quota_id: int, payload: UpdateQuotaRequest, + quota_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -416,8 +420,8 @@ async def update_quota( @quota_router.patch("/{quota_id}/threshold", response_model=dict) async def set_quota_threshold( - quota_id: int, payload: SetQuotaThresholdRequest, + quota_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -434,8 +438,8 @@ async def set_quota_threshold( @quota_router.post("/{quota_id}/reset", response_model=dict) async def reset_quota( - quota_id: int, payload: ResetQuotaRequest, + quota_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -452,7 +456,7 @@ async def reset_quota( @quota_router.delete("/{quota_id}", response_model=dict) async def delete_quota( - quota_id: int, + quota_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: diff --git a/backend/server/routers/external_systems/test_case_router.py b/backend/server/routers/external_systems/test_case_router.py index 3863f88e..8bd83e7f 100644 --- a/backend/server/routers/external_systems/test_case_router.py +++ b/backend/server/routers/external_systems/test_case_router.py @@ -14,7 +14,7 @@ import json from datetime import UTC, datetime from typing import Any -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Body, Depends, Path, Query from fastapi.responses import StreamingResponse from pydantic import BaseModel, ConfigDict, Field from sqlalchemy.ext.asyncio import AsyncSession @@ -123,7 +123,7 @@ class ImportTestCasesRequest(BaseModel): model_config = ConfigDict(frozen=True) - items: list[CreateTestCaseRequest] = Field(..., min_length=1) + items: list[CreateTestCaseRequest] = Field(..., min_length=1, max_length=100) class UpdateAlertConfigRequest(BaseModel): @@ -142,10 +142,10 @@ class UpdateAlertConfigRequest(BaseModel): async def list_test_cases( limit: int = Query(100, ge=1, le=500), offset: int = Query(0, ge=0), - tool_slug: str | None = Query(None), - env_key: str | None = Query(None), + tool_slug: str | None = Query(None, max_length=128), + env_key: str | None = Query(None, max_length=32), enabled: bool | None = Query(None), - schedule_cron: str | None = Query(None), + schedule_cron: str | None = Query(None, max_length=64), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -269,7 +269,7 @@ async def export_test_cases( @test_case_router.get("/{case_id}", response_model=dict) async def get_test_case( - case_id: int, + case_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -282,8 +282,8 @@ async def get_test_case( @test_case_router.put("/{case_id}", response_model=dict) async def update_test_case( - case_id: int, - payload: UpdateTestCaseRequest, + case_id: int = Path(ge=1), + payload: UpdateTestCaseRequest = Body(...), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -300,7 +300,7 @@ async def update_test_case( @test_case_router.delete("/{case_id}", response_model=dict) async def delete_test_case( - case_id: int, + case_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -316,7 +316,7 @@ async def delete_test_case( @test_case_router.post("/{case_id}/run", response_model=dict) async def run_test_case( - case_id: int, + case_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -332,8 +332,8 @@ async def run_test_case( @test_case_router.post("/{case_id}/clone", response_model=dict) async def clone_test_case( - case_id: int, - payload: CloneTestCaseRequest, + case_id: int = Path(ge=1), + payload: CloneTestCaseRequest = Body(...), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -350,7 +350,7 @@ async def clone_test_case( @test_case_router.get("/{case_id}/alert-config", response_model=dict) async def get_alert_config( - case_id: int, + case_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -363,8 +363,8 @@ async def get_alert_config( @test_case_router.put("/{case_id}/alert-config", response_model=dict) async def update_alert_config( - case_id: int, - payload: UpdateAlertConfigRequest, + case_id: int = Path(ge=1), + payload: UpdateAlertConfigRequest = Body(...), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: diff --git a/backend/server/routers/external_systems/webhook_router.py b/backend/server/routers/external_systems/webhook_router.py index d71d0712..d0756941 100644 --- a/backend/server/routers/external_systems/webhook_router.py +++ b/backend/server/routers/external_systems/webhook_router.py @@ -18,9 +18,9 @@ from __future__ import annotations from datetime import datetime -from typing import Any +from typing import Any, Literal -from fastapi import APIRouter, Depends, Header, Query, Request +from fastapi import APIRouter, Body, Depends, Header, Path, Query, Request from pydantic import BaseModel, ConfigDict, Field from sqlalchemy.ext.asyncio import AsyncSession from yuxi.external_systems.infrastructure.container import create_use_cases_from_db @@ -194,7 +194,9 @@ async def list_subscriptions( system_id: int | None = Query(None, ge=1), env_key: str | None = Query(None, max_length=32), source_event_type: str | None = Query(None, max_length=128), - status: str | None = Query(None, max_length=32), + status: Literal["pending", "active", "paused", "expired", "disabled"] | None = Query( + None, description="订阅状态:pending/active/paused/expired/disabled" + ), enabled: bool | None = Query(None), target_handler_type: str | None = Query(None, max_length=32), target_handler_id: str | None = Query(None, max_length=64), @@ -276,7 +278,9 @@ async def list_events( subscription_id: int | None = Query(None, ge=1), subscription_slug: str | None = Query(None, max_length=128), event_type: str | None = Query(None, max_length=128), - processing_status: str | None = Query(None, max_length=32), + processing_status: Literal["pending", "processing", "processed", "failed", "ignored", "duplicate"] | None = Query( + None, description="处理状态:pending/processing/processed/failed/ignored/duplicate" + ), correlation_id: str | None = Query(None, max_length=128), triggered_execution_id: str | None = Query(None, max_length=128), signature_verified: bool | None = Query(None), @@ -369,7 +373,7 @@ async def list_renewal_failures( @webhook_router.get("/subscriptions/{subscription_id}", response_model=dict) async def get_subscription( - subscription_id: int, + subscription_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -382,8 +386,8 @@ async def get_subscription( @webhook_router.put("/subscriptions/{subscription_id}", response_model=dict) async def update_subscription( - subscription_id: int, - payload: UpdateWebhookSubscriptionRequest, + subscription_id: int = Path(ge=1), + payload: UpdateWebhookSubscriptionRequest = Body(...), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -400,7 +404,7 @@ async def update_subscription( @webhook_router.delete("/subscriptions/{subscription_id}", response_model=dict) async def delete_subscription( - subscription_id: int, + subscription_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -413,7 +417,7 @@ async def delete_subscription( @webhook_router.post("/subscriptions/{subscription_id}/renew", response_model=dict) async def renew_subscription( - subscription_id: int, + subscription_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -426,7 +430,7 @@ async def renew_subscription( @webhook_router.get("/events/{event_id}/deliveries", response_model=dict) async def get_event_deliveries( - event_id: int, + event_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -439,7 +443,7 @@ async def get_event_deliveries( @webhook_router.post("/events/{event_id}/replay", response_model=dict) async def replay_event( - event_id: int, + event_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -452,7 +456,7 @@ async def replay_event( @webhook_router.get("/subscriptions/{subscription_id}/filter-rules", response_model=dict) async def get_filter_rules( - subscription_id: int, + subscription_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -465,8 +469,8 @@ async def get_filter_rules( @webhook_router.put("/subscriptions/{subscription_id}/filter-rules", response_model=dict) async def update_filter_rules( - subscription_id: int, - payload: UpdateFilterRulesRequest, + subscription_id: int = Path(ge=1), + payload: UpdateFilterRulesRequest = Body(...), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -483,7 +487,7 @@ async def update_filter_rules( @webhook_router.get("/subscriptions/{subscription_id}/transform-rules", response_model=dict) async def get_transform_rules( - subscription_id: int, + subscription_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: @@ -496,8 +500,8 @@ async def get_transform_rules( @webhook_router.put("/subscriptions/{subscription_id}/transform-rules", response_model=dict) async def update_transform_rules( - subscription_id: int, - payload: UpdateTransformRulesRequest, + subscription_id: int = Path(ge=1), + payload: UpdateTransformRulesRequest = Body(...), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -514,8 +518,8 @@ async def update_transform_rules( @webhook_router.post("/subscriptions/{subscription_id}/test", response_model=dict) async def test_subscription( - subscription_id: int, - payload: TestWebhookSubscriptionRequest, + subscription_id: int = Path(ge=1), + payload: TestWebhookSubscriptionRequest = Body(...), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: @@ -533,8 +537,8 @@ async def test_subscription( @webhook_router.post("/subscriptions/{subscription_id}/verify-signature", response_model=dict) async def verify_signature( - subscription_id: int, - payload: VerifySignatureRequest, + subscription_id: int = Path(ge=1), + payload: VerifySignatureRequest = Body(...), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: