Files
BanGUI/backend/app/routers/geo.py
Lukas 507f153ab9 Enforce repository boundary: Remove DbDep from routers
This commit enforces the repository boundary by eliminating direct database connection
dependencies (DbDep) from all routers. Routers now depend on service context dependencies
that combine the database connection with the related repositories.

Changes:
- Add 5 service context dependencies in dependencies.py:
  * SessionServiceContext: db + session_repo
  * BlocklistServiceContext: db + blocklist_repo + import_log_repo + settings_repo
  * SettingsServiceContext: db + settings_repo
  * BanServiceContext: db + fail2ban_db_repo
  * HistoryServiceContext: db + fail2ban_db_repo + history_archive_repo

- Refactor all 9 routers (auth, bans, blocklist, config_misc, dashboard, geo,
  history, jails, setup) to use service contexts instead of DbDep.

- Update Backend-Development.md with clear examples of the new pattern and
  documentation of available service contexts.

Rationale:
- Enforces the repository boundary through the dependency system
- Makes database operations explicit and auditable
- Improves testability by allowing service contexts to be mocked
- Prevents accidental direct database access from routers

The deprecated DbDep remains available for backward compatibility with
services that have not yet been refactored, but routers can no longer import it.

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

127 lines
3.7 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 TYPE_CHECKING, Annotated
if TYPE_CHECKING:
from app.services.jail_service import IpLookupResult
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.
"""
result: IpLookupResult = await jail_service.lookup_ip(
socket_path,
ip,
http_session=http_session,
)
return IpLookupResponse(**result)
# ---------------------------------------------------------------------------
# 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)