Files
BanGUI/backend/app/mappers/ban_mappers.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

121 lines
3.6 KiB
Python

"""Ban response mappers.
Convert domain models (from ban_service) to response models (for HTTP API).
This is the mapping layer at the router boundary, ensuring the service layer
remains independent of HTTP response shapes.
"""
from __future__ import annotations
from app.models.ban import (
ActiveBan,
ActiveBanListResponse,
BansByCountryResponse,
BansByJailResponse,
BanTrendBucket,
BanTrendResponse,
DashboardBanItem,
DashboardBanListResponse,
JailBanCount,
)
from app.models.ban_domain import (
DomainActiveBan,
DomainActiveBanList,
DomainBansByCountry,
DomainBansByJail,
DomainBanTrend,
DomainDashboardBanItem,
DomainDashboardBanList,
)
def map_domain_active_ban_to_response(domain_ban: DomainActiveBan) -> ActiveBan:
"""Convert a domain active ban to a response model."""
return ActiveBan(
ip=domain_ban.ip,
jail=domain_ban.jail,
banned_at=domain_ban.banned_at,
expires_at=domain_ban.expires_at,
ban_count=domain_ban.ban_count,
country=domain_ban.country,
)
def map_domain_active_ban_list_to_response(
domain_list: DomainActiveBanList,
) -> ActiveBanListResponse:
"""Convert a domain active ban list to a response model."""
return ActiveBanListResponse(
items=[map_domain_active_ban_to_response(ban) for ban in domain_list.bans],
total=domain_list.total,
)
def map_domain_dashboard_ban_item_to_response(
domain_item: DomainDashboardBanItem,
) -> DashboardBanItem:
"""Convert a domain dashboard ban item to a response model."""
return DashboardBanItem(
ip=domain_item.ip,
jail=domain_item.jail,
banned_at=domain_item.banned_at,
service=domain_item.service,
country_code=domain_item.country_code,
country_name=domain_item.country_name,
asn=domain_item.asn,
org=domain_item.org,
ban_count=domain_item.ban_count,
origin=domain_item.origin,
)
def map_domain_dashboard_ban_list_to_response(
domain_list: DomainDashboardBanList,
) -> DashboardBanListResponse:
"""Convert a domain dashboard ban list to a response model."""
return DashboardBanListResponse(
items=[
map_domain_dashboard_ban_item_to_response(item) for item in domain_list.items
],
total=domain_list.total,
page=domain_list.page,
page_size=domain_list.page_size,
)
def map_domain_bans_by_country_to_response(
domain_data: DomainBansByCountry,
) -> BansByCountryResponse:
"""Convert domain bans-by-country data to a response model."""
return BansByCountryResponse(
countries=domain_data.countries,
country_names=domain_data.country_names,
bans=[map_domain_dashboard_ban_item_to_response(item) for item in domain_data.items],
total=domain_data.total,
)
def map_domain_ban_trend_to_response(domain_trend: DomainBanTrend) -> BanTrendResponse:
"""Convert domain ban trend data to a response model."""
return BanTrendResponse(
buckets=[
BanTrendBucket(timestamp=bucket.timestamp, count=bucket.count)
for bucket in domain_trend.buckets
],
bucket_size=domain_trend.bucket_size,
)
def map_domain_bans_by_jail_to_response(
domain_data: DomainBansByJail,
) -> BansByJailResponse:
"""Convert domain bans-by-jail data to a response model."""
return BansByJailResponse(
jails=[
JailBanCount(jail=jail_count.jail, count=jail_count.count)
for jail_count in domain_data.jails
],
total=domain_data.total,
)