Files
BanGUI/backend/app/models/response.py
Lukas 1c673d600c Standardize API response envelope shapes across all endpoints
This commit standardizes how API responses are wrapped, solving issue #24.

Problem:
- Inconsistent response envelopes (jails vs items vs bans vs no wrapper)
- Frontend required multiple field name variants
- Integration bugs from branching logic
- No clear pattern for different response types

Solution:
- Created response.py with base classes: PaginatedListResponse,
  CollectionResponse, CommandResponse
- Standardized all list/collection responses to use 'items' field
- Domain-specific field names for detail and aggregation responses
- Updated all backends routers and mappers
- Updated frontend types and hooks to match

Changes:
Backend:
- backend/app/models/response.py (new): Base response models
- backend/app/models/ban.py: Updated responses to inherit from bases
- backend/app/models/jail.py: Updated JailListResponse, JailCommandResponse
- backend/app/models/config.py: Updated collection responses
- backend/app/services/jail_service.py: Updated return statements
- backend/app/mappers/ban_mappers.py: Updated 'bans' to 'items'
- backend/tests/test_mappers/test_ban_mappers.py: Updated tests

Frontend:
- frontend/src/types/jail.ts: Updated response interfaces
- frontend/src/types/config.ts: Updated response interfaces
- frontend/src/hooks/useActiveBans.ts: Updated selector
- frontend/src/hooks/useJailList.ts: Updated selector
- frontend/src/hooks/useJailConfigs.ts: Updated selector
- frontend/src/hooks/useConfigActiveStatus.ts: Updated field access
- frontend/src/hooks/useJailAdmin.ts: Updated field access

Documentation:
- Docs/Backend-Development.md: Added § 4.1 API Response Envelope Policy

The policy defines:
1. Paginated lists use PaginatedListResponse (items, total, page, page_size)
2. Non-paginated collections use CollectionResponse (items, total)
3. Detail responses use entity-specific field names (jail, status, settings)
4. Command responses use CommandResponse (message, success, optional target)
5. Aggregations use domain-specific fields (jails, countries, buckets, bans)

All responses now follow one of these patterns, reducing frontend complexity.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-28 10:12:55 +02:00

194 lines
5.3 KiB
Python

"""Base response wrapper models for standardized API envelopes.
All API endpoints should wrap their responses using the base classes defined here.
This ensures a consistent response shape across the entire API, reducing frontend
branching logic and integration bugs.
Response Patterns:
1. **Paginated List** — Use `PaginatedListResponse[T]` for endpoints returning paginated items.
Example: GET /api/jails, GET /api/dashboard/bans
```python
class MyListResponse(PaginatedListResponse[MyItem]):
pass
# Returns:
{
"items": [...],
"total": 100,
"page": 1,
"page_size": 20
}
```
2. **Simple Collection** — Use `CollectionResponse[T]` for non-paginated collections.
Example: GET /api/bans/active
```python
class MyCollectionResponse(CollectionResponse[MyItem]):
pass
# Returns:
{
"items": [...],
"total": 50
}
```
3. **Single Item Detail** — Use domain model directly wrapped in a named field.
Example: GET /api/jails/{name}, GET /api/dashboard/status
```python
class MyDetailResponse(BaseModel):
jail: Jail # or: status: ServerStatus, settings: ServerSettings
# Optional extra fields (ignore_list, warnings, etc.)
# Returns:
{
"jail": {...},
"ignore_list": [...]
}
```
4. **Command/Action Result** — Use `CommandResponse` for success/acknowledgement.
Example: POST /api/jails/{name}/start, POST /api/bans
```python
class MyCommandResponse(CommandResponse):
jail: str # Optional: target identifier
# Returns:
{
"message": "Jail 'sshd' started.",
"success": true,
"jail": "sshd"
}
```
5. **Aggregated Data** — Use domain-specific aggregation models with metadata.
Example: GET /api/dashboard/bans/by-jail
```python
class MyAggregationResponse(BaseModel):
jails: list[JailBanCount] # or: countries, buckets, etc.
total: int
# Optional: filters, time_range metadata
# Returns:
{
"jails": [...],
"total": 1234
}
```
Note on field naming:
- Paginated/collection responses always use "items" for the data array.
- Detail responses use domain-specific field names (jail, status, settings).
- Aggregation responses use domain-specific field names (jails, countries, buckets).
- All responses with multiple items include a "total" field.
"""
from typing import Generic, TypeVar
from pydantic import BaseModel, ConfigDict, Field
T = TypeVar("T")
class PaginatedListResponse(BaseModel, Generic[T]):
"""Standardized paginated list response.
Use this as a base for all endpoints that return paginated collections.
Automatically includes pagination metadata to support frontend paging UIs.
Fields:
items: The data items for the current page.
total: Total number of items matching the query (across all pages).
page: Current page number (1-based).
page_size: Number of items per page.
Example:
```python
class UserListResponse(PaginatedListResponse[User]):
pass
# Returns:
{
"items": [...],
"total": 150,
"page": 2,
"page_size": 50
}
```
"""
model_config = ConfigDict(strict=True)
items: list[T] = Field(default_factory=list, description="Data items for the current page.")
total: int = Field(..., ge=0, description="Total number of items matching the query.")
page: int = Field(..., ge=1, description="Current page number (1-based).")
page_size: int = Field(..., ge=1, description="Number of items per page.")
class CollectionResponse(BaseModel, Generic[T]):
"""Standardized non-paginated collection response.
Use this for endpoints that return a collection without pagination support.
Simpler than PaginatedListResponse, but still provides consistent wrapping.
Fields:
items: The data items in the collection.
total: Total number of items.
Example:
```python
class ActiveBansResponse(CollectionResponse[ActiveBan]):
pass
# Returns:
{
"items": [...],
"total": 42
}
```
"""
model_config = ConfigDict(strict=True)
items: list[T] = Field(default_factory=list, description="Collection items.")
total: int = Field(..., ge=0, description="Total number of items.")
class CommandResponse(BaseModel):
"""Standardized command/action result response.
Use this for endpoints that execute commands (start, stop, reload, ban, unban, etc.).
Always includes a success indicator and human-readable message.
Fields:
message: Human-readable result message or error description.
success: Whether the command succeeded (default True).
Example:
```python
class StartJailResponse(CommandResponse):
jail: str # Optional: target identifier
# Returns:
{
"message": "Jail 'sshd' started.",
"success": true,
"jail": "sshd"
}
```
"""
model_config = ConfigDict(strict=True)
message: str = Field(..., description="Human-readable result or error message.")
success: bool = Field(
default=True,
description="Whether the command succeeded (false for errors in non-exception handlers).",
)