Files
BanGUI/backend/app/routers/geo.py
Lukas 52a4d04d92 Task 8: Standardize modeling style (TypedDict vs Pydantic)
Convert inconsistent modeling style to standardized Pydantic models for all
external-facing data structures while maintaining TypedDict compatibility where
appropriate for internal layer-private structures.

Changes:
- Converted IpLookupResult TypedDict to use IpLookupResponse Pydantic model
  in jail_service.lookup_ip() for consistency with routers
- Added GeoCacheEntry Pydantic model for geo cache repository rows
- Converted GeoCacheRow TypedDict to use GeoCacheEntry alias
- Converted ImportLogRow TypedDict to use ImportLogEntry alias
- Updated routers and services to work with Pydantic models
- Updated all tests to use Pydantic model field access (attributes)
  instead of dict subscripting

Documentation:
- Added 'Model Type Usage by Layer' section to Backend-Development.md
- Defines when TypedDict is allowed (internal structures) vs Pydantic
  (external-facing, cross-boundary data)
- Provides clear guidance on modeling conventions per layer

Benefits:
- Consistent validation and serialization behavior
- Better IDE support and type checking
- Clearer separation of concerns by layer
- Reduced maintenance cost from mixed validation approaches

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-28 07:53:30 +02:00

122 lines
3.6 KiB
Python

"""Geo / IP lookup router.
Provides the IP enrichment endpoints:
* ``GET /api/geo/lookup/{ip}`` — ban status, ban history, and geo info for an IP
* ``POST /api/geo/re-resolve`` — retry all previously failed geo lookups
"""
from __future__ import annotations
from typing import Annotated
from fastapi import APIRouter, Path
from app.dependencies import (
AuthDep,
BanServiceContextDep,
Fail2BanSocketDep,
HttpSessionDep,
)
from app.models.geo import GeoCacheStatsResponse, GeoReResolveResponse, IpLookupResponse
from app.services import geo_service, jail_service
router: APIRouter = APIRouter(prefix="/api/geo", tags=["Geo"])
_IpPath = Annotated[str, Path(description="IPv4 or IPv6 address to look up.")]
@router.get(
"/lookup/{ip}",
response_model=IpLookupResponse,
summary="Look up ban status and geo information for an IP",
)
async def lookup_ip(
_auth: AuthDep,
ip: _IpPath,
socket_path: Fail2BanSocketDep,
http_session: HttpSessionDep,
) -> IpLookupResponse:
"""Return current ban status, geo data, and network information for an IP.
Checks every running fail2ban jail to determine whether the IP is
currently banned, and enriches the result with country, ASN, and
organisation data from ip-api.com.
Args:
_auth: Validated session — enforces authentication.
ip: The IP address to look up.
Returns:
:class:`~app.models.geo.IpLookupResponse` with ban status and geo data.
Raises:
HTTPException: 400 when *ip* is not a valid IP address.
HTTPException: 502 when fail2ban is unreachable.
"""
return await jail_service.lookup_ip(
socket_path,
ip,
http_session=http_session,
)
# ---------------------------------------------------------------------------
# POST /api/geo/re-resolve
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# GET /api/geo/stats
# ---------------------------------------------------------------------------
@router.get(
"/stats",
response_model=GeoCacheStatsResponse,
summary="Geo cache diagnostic counters",
)
async def geo_stats(
_auth: AuthDep,
ban_ctx: BanServiceContextDep,
) -> GeoCacheStatsResponse:
"""Return diagnostic counters for the geo cache subsystem.
Useful for operators and the UI to gauge geo-resolution health.
Args:
_auth: Validated session — enforces authentication.
ban_ctx: Ban service context containing db and repository.
Returns:
:class:`~app.models.geo.GeoCacheStatsResponse` with current counters.
"""
stats: dict[str, int] = await geo_service.cache_stats(ban_ctx.db)
return GeoCacheStatsResponse(**stats)
@router.post(
"/re-resolve",
summary="Re-resolve all IPs whose country could not be determined",
response_model=GeoReResolveResponse,
)
async def re_resolve_geo(
_auth: AuthDep,
ban_ctx: BanServiceContextDep,
http_session: HttpSessionDep,
) -> GeoReResolveResponse:
"""Retry geo resolution for every IP in ``geo_cache`` with a null country.
Clears the in-memory negative cache first so that previously failing IPs
are immediately eligible for a new API attempt.
Args:
_auth: Validated session — enforces authentication.
ban_ctx: Ban service context containing db and repository.
http_session: Shared HTTP session for geo lookups.
Returns:
A :class:`~app.models.geo.GeoReResolveResponse` with retry counts.
"""
return await geo_service.re_resolve_all(ban_ctx.db, http_session)