Standardize API response envelopes: use items for collection responses and update tests

This commit is contained in:
2026-04-28 20:48:00 +02:00
parent 1c673d600c
commit b27765928a
23 changed files with 186 additions and 104 deletions

View File

@@ -72,12 +72,33 @@ class JailListResponse(CollectionResponse[JailSummary]):
pass
class IgnoreListResponse(CollectionResponse[str]):
"""Response for ``GET /api/jails/{name}/ignoreip``.
Returns the jailed ignore list as a standard collection response.
"""
pass
class JailDetailResponse(BaseModel):
"""Response for ``GET /api/jails/{name}``."""
"""Response for ``GET /api/jails/{name}``.
Includes the primary jail object together with supplemental metadata
required by the UI.
"""
model_config = ConfigDict(strict=True)
jail: Jail
ignore_list: list[str] = Field(
default_factory=list,
description="List of IP addresses and networks currently ignored by the jail.",
)
ignore_self: bool = Field(
default=False,
description="Whether the jail ignores the server's own IP addresses.",
)
class JailCommandResponse(CommandResponse):

View File

@@ -19,6 +19,7 @@ Provides CRUD and control operations for fail2ban jails:
from __future__ import annotations
import asyncio
from typing import Annotated
from fastapi import APIRouter, Body, HTTPException, Path, status
@@ -34,6 +35,7 @@ from app.dependencies import (
from app.models.ban import JailBannedIpsResponse
from app.models.jail import (
IgnoreIpRequest,
IgnoreListResponse,
JailCommandResponse,
JailDetailResponse,
JailListResponse,
@@ -103,7 +105,16 @@ async def get_jail(
HTTPException: 404 when the jail does not exist.
HTTPException: 502 when fail2ban is unreachable.
"""
return await jail_service.get_jail(socket_path, name)
jail, ignore_list, ignore_self = await asyncio.gather(
jail_service.get_jail(socket_path, name),
jail_service.get_ignore_list(socket_path, name),
jail_service.get_ignore_self(socket_path, name),
)
return JailDetailResponse(
jail=jail,
ignore_list=ignore_list,
ignore_self=ignore_self,
)
# ---------------------------------------------------------------------------
@@ -278,14 +289,14 @@ class _IgnoreSelfRequest(IgnoreIpRequest):
@router.get(
"/{name}/ignoreip",
response_model=list[str],
response_model=IgnoreListResponse,
summary="List the ignore IPs for a jail",
)
async def get_ignore_list(
_auth: AuthDep,
name: _NamePath,
socket_path: Fail2BanSocketDep,
) -> list[str]:
) -> IgnoreListResponse:
"""Return the current ignore list (IP whitelist) for a fail2ban jail.
Args:
@@ -299,7 +310,8 @@ async def get_ignore_list(
HTTPException: 404 when the jail does not exist.
HTTPException: 502 when fail2ban is unreachable.
"""
return await jail_service.get_ignore_list(socket_path, name)
ignore_list = await jail_service.get_ignore_list(socket_path, name)
return IgnoreListResponse(items=ignore_list, total=len(ignore_list))
@router.post(

View File

@@ -218,7 +218,7 @@ async def list_jail_configs(socket_path: str) -> JailConfigListResponse:
)
if not jail_names:
return JailConfigListResponse(jails=[], total=0)
return JailConfigListResponse(items=[], total=0)
responses: list[JailConfigResponse] = await asyncio.gather(
*[get_jail_config(socket_path, name) for name in jail_names],
@@ -227,7 +227,7 @@ async def list_jail_configs(socket_path: str) -> JailConfigListResponse:
jails = [r.jail for r in responses]
log.info("jail_configs_listed", count=len(jails))
return JailConfigListResponse(jails=jails, total=len(jails))
return JailConfigListResponse(items=jails, total=len(jails))
# ---------------------------------------------------------------------------

View File

@@ -311,7 +311,7 @@ async def list_inactive_jails(
active=len(active_names),
inactive=len(inactive),
)
return InactiveJailListResponse(jails=inactive, total=len(inactive))
return InactiveJailListResponse(items=inactive, total=len(inactive))
async def activate_jail(