From 3b581798458cbb9a441008247bdb590135b638cb Mon Sep 17 00:00:00 2001 From: Lukas Date: Mon, 6 Apr 2026 16:38:17 +0200 Subject: [PATCH] Refactor router dependencies to use explicit fail2ban socket and HTTP session injection --- backend/app/routers/dashboard.py | 36 +++++++------------ backend/app/routers/jails.py | 62 +++++++++++--------------------- 2 files changed, 34 insertions(+), 64 deletions(-) diff --git a/backend/app/routers/dashboard.py b/backend/app/routers/dashboard.py index 9d5b3f4..bdafe68 100644 --- a/backend/app/routers/dashboard.py +++ b/backend/app/routers/dashboard.py @@ -12,15 +12,17 @@ Also provides ``GET /api/dashboard/bans`` for the dashboard ban-list table, from __future__ import annotations -from typing import TYPE_CHECKING, Literal - -if TYPE_CHECKING: - import aiohttp +from typing import Literal from fastapi import APIRouter, Query, Request from app import __version__ -from app.dependencies import AuthDep, DbDep +from app.dependencies import ( + AuthDep, + DbDep, + Fail2BanSocketDep, + HttpSessionDep, +) from app.models.ban import ( BanOrigin, BansByCountryResponse, @@ -80,9 +82,10 @@ async def get_server_status( summary="Return a paginated list of recent bans", ) async def get_dashboard_bans( - request: Request, _auth: AuthDep, db: DbDep, + socket_path: Fail2BanSocketDep, + http_session: HttpSessionDep, range: TimeRange = Query(default=_DEFAULT_RANGE, description="Time-range preset."), source: Literal["fail2ban", "archive"] = Query( default="fail2ban", @@ -104,7 +107,6 @@ async def get_dashboard_bans( GET request. Args: - request: The incoming request (used to access ``app.state``). _auth: Validated session dependency. range: Time-range preset — ``"24h"``, ``"7d"``, ``"30d"``, or ``"365d"``. @@ -116,9 +118,6 @@ async def get_dashboard_bans( :class:`~app.models.ban.DashboardBanListResponse` with paginated ban items and the total count for the selected window. """ - socket_path: str = request.app.state.settings.fail2ban_socket - http_session: aiohttp.ClientSession = request.app.state.http_session - return await ban_service.list_bans( socket_path, range, @@ -138,9 +137,10 @@ async def get_dashboard_bans( summary="Return ban counts aggregated by country", ) async def get_bans_by_country( - request: Request, _auth: AuthDep, db: DbDep, + socket_path: Fail2BanSocketDep, + http_session: HttpSessionDep, range: TimeRange = Query(default=_DEFAULT_RANGE, description="Time-range preset."), source: Literal["fail2ban", "archive"] = Query( default="fail2ban", @@ -164,7 +164,6 @@ async def get_bans_by_country( during this GET request. Args: - request: The incoming request. _auth: Validated session dependency. range: Time-range preset. origin: Optional filter by ban origin. @@ -173,9 +172,6 @@ async def get_bans_by_country( :class:`~app.models.ban.BansByCountryResponse` with per-country aggregation and the companion ban list. """ - socket_path: str = request.app.state.settings.fail2ban_socket - http_session: aiohttp.ClientSession = request.app.state.http_session - return await ban_service.bans_by_country( socket_path, range, @@ -195,9 +191,9 @@ async def get_bans_by_country( summary="Return ban counts aggregated into time buckets", ) async def get_ban_trend( - request: Request, _auth: AuthDep, db: DbDep, + socket_path: Fail2BanSocketDep, range: TimeRange = Query(default=_DEFAULT_RANGE, description="Time-range preset."), source: Literal["fail2ban", "archive"] = Query( default="fail2ban", @@ -223,7 +219,6 @@ async def get_ban_trend( * ``365d`` → 7-day buckets (~53 total) Args: - request: The incoming request (used to access ``app.state``). _auth: Validated session dependency. range: Time-range preset. origin: Optional filter by ban origin. @@ -232,8 +227,6 @@ async def get_ban_trend( :class:`~app.models.ban.BanTrendResponse` with the ordered bucket list and the bucket-size label. """ - socket_path: str = request.app.state.settings.fail2ban_socket - return await ban_service.ban_trend( socket_path, range, @@ -249,9 +242,9 @@ async def get_ban_trend( summary="Return ban counts aggregated by jail", ) async def get_bans_by_jail( - request: Request, _auth: AuthDep, db: DbDep, + socket_path: Fail2BanSocketDep, range: TimeRange = Query(default=_DEFAULT_RANGE, description="Time-range preset."), source: Literal["fail2ban", "archive"] = Query( default="fail2ban", @@ -269,7 +262,6 @@ async def get_bans_by_jail( distribution bar chart. Args: - request: The incoming request (used to access ``app.state``). _auth: Validated session dependency. range: Time-range preset — ``"24h"``, ``"7d"``, ``"30d"``, or ``"365d"``. @@ -279,8 +271,6 @@ async def get_bans_by_jail( :class:`~app.models.ban.BansByJailResponse` with per-jail counts sorted descending and the total for the selected window. """ - socket_path: str = request.app.state.settings.fail2ban_socket - return await ban_service.bans_by_jail( socket_path, range, diff --git a/backend/app/routers/jails.py b/backend/app/routers/jails.py index c8cd27a..c59d4fb 100644 --- a/backend/app/routers/jails.py +++ b/backend/app/routers/jails.py @@ -21,9 +21,15 @@ from __future__ import annotations from typing import Annotated -from fastapi import APIRouter, Body, HTTPException, Path, Request, status +from fastapi import APIRouter, Body, HTTPException, Path, status -from app.dependencies import AuthDep, DbDep +from app.dependencies import ( + AuthDep, + DbDep, + Fail2BanSocketDep, + HttpSessionDep, +) +from app.exceptions import JailNotFoundError, JailOperationError from app.models.ban import JailBannedIpsResponse from app.models.jail import ( IgnoreIpRequest, @@ -32,7 +38,6 @@ from app.models.jail import ( JailListResponse, ) from app.services import geo_service, jail_service -from app.exceptions import JailNotFoundError, JailOperationError from app.utils.fail2ban_client import Fail2BanConnectionError router: APIRouter = APIRouter(prefix="/api/jails", tags=["Jails"]) @@ -100,8 +105,8 @@ def _conflict(message: str) -> HTTPException: summary="List all active fail2ban jails", ) async def get_jails( - request: Request, _auth: AuthDep, + socket_path: Fail2BanSocketDep, ) -> JailListResponse: """Return a summary of every active fail2ban jail. @@ -110,13 +115,11 @@ async def get_jails( for each jail. Args: - request: Incoming request (used to access ``app.state``). _auth: Validated session — enforces authentication. Returns: :class:`~app.models.jail.JailListResponse` with all active jails. """ - socket_path: str = request.app.state.settings.fail2ban_socket try: return await jail_service.list_jails(socket_path) except Fail2BanConnectionError as exc: @@ -129,9 +132,9 @@ async def get_jails( summary="Return full detail for a single jail", ) async def get_jail( - request: Request, _auth: AuthDep, name: _NamePath, + socket_path: Fail2BanSocketDep, ) -> JailDetailResponse: """Return the complete configuration and runtime state for one jail. @@ -140,7 +143,6 @@ async def get_jail( counters. Args: - request: Incoming request (used to access ``app.state``). _auth: Validated session — enforces authentication. name: Jail name. @@ -151,7 +153,6 @@ async def get_jail( HTTPException: 404 when the jail does not exist. HTTPException: 502 when fail2ban is unreachable. """ - socket_path: str = request.app.state.settings.fail2ban_socket try: return await jail_service.get_jail(socket_path, name) except JailNotFoundError: @@ -171,8 +172,8 @@ async def get_jail( summary="Reload all fail2ban jails", ) async def reload_all_jails( - request: Request, _auth: AuthDep, + socket_path: Fail2BanSocketDep, ) -> JailCommandResponse: """Reload every fail2ban jail to apply configuration changes. @@ -180,7 +181,6 @@ async def reload_all_jails( jails simultaneously. Args: - request: Incoming request (used to access ``app.state``). _auth: Validated session — enforces authentication. Returns: @@ -190,7 +190,6 @@ async def reload_all_jails( HTTPException: 502 when fail2ban is unreachable. HTTPException: 409 when fail2ban reports the operation failed. """ - socket_path: str = request.app.state.settings.fail2ban_socket try: await jail_service.reload_all(socket_path) return JailCommandResponse(message="All jails reloaded successfully.", jail="*") @@ -206,14 +205,13 @@ async def reload_all_jails( summary="Start a stopped jail", ) async def start_jail( - request: Request, _auth: AuthDep, name: _NamePath, + socket_path: Fail2BanSocketDep, ) -> JailCommandResponse: """Start a fail2ban jail that is currently stopped. Args: - request: Incoming request (used to access ``app.state``). _auth: Validated session — enforces authentication. name: Jail name. @@ -225,7 +223,6 @@ async def start_jail( HTTPException: 409 when fail2ban reports the operation failed. HTTPException: 502 when fail2ban is unreachable. """ - socket_path: str = request.app.state.settings.fail2ban_socket try: await jail_service.start_jail(socket_path, name) return JailCommandResponse(message=f"Jail {name!r} started.", jail=name) @@ -243,9 +240,9 @@ async def start_jail( summary="Stop a running jail", ) async def stop_jail( - request: Request, _auth: AuthDep, name: _NamePath, + socket_path: Fail2BanSocketDep, ) -> JailCommandResponse: """Stop a running fail2ban jail. @@ -254,7 +251,6 @@ async def stop_jail( jail is already stopped the request succeeds silently (idempotent). Args: - request: Incoming request (used to access ``app.state``). _auth: Validated session — enforces authentication. name: Jail name. @@ -265,7 +261,6 @@ async def stop_jail( HTTPException: 409 when fail2ban reports the operation failed. HTTPException: 502 when fail2ban is unreachable. """ - socket_path: str = request.app.state.settings.fail2ban_socket try: await jail_service.stop_jail(socket_path, name) return JailCommandResponse(message=f"Jail {name!r} stopped.", jail=name) @@ -281,9 +276,9 @@ async def stop_jail( summary="Toggle idle mode for a jail", ) async def toggle_idle( - request: Request, _auth: AuthDep, name: _NamePath, + socket_path: Fail2BanSocketDep, on: bool = Body(..., description="``true`` to enable idle, ``false`` to disable."), ) -> JailCommandResponse: """Enable or disable idle mode for a fail2ban jail. @@ -292,7 +287,6 @@ async def toggle_idle( preserving all existing bans. Args: - request: Incoming request (used to access ``app.state``). _auth: Validated session — enforces authentication. name: Jail name. on: ``true`` to enable idle, ``false`` to disable. @@ -305,7 +299,6 @@ async def toggle_idle( HTTPException: 409 when fail2ban reports the operation failed. HTTPException: 502 when fail2ban is unreachable. """ - socket_path: str = request.app.state.settings.fail2ban_socket state_str = "on" if on else "off" try: await jail_service.set_idle(socket_path, name, on=on) @@ -327,14 +320,13 @@ async def toggle_idle( summary="Reload a single jail", ) async def reload_jail( - request: Request, _auth: AuthDep, name: _NamePath, + socket_path: Fail2BanSocketDep, ) -> JailCommandResponse: """Reload a single fail2ban jail to pick up configuration changes. Args: - request: Incoming request (used to access ``app.state``). _auth: Validated session — enforces authentication. name: Jail name. @@ -346,7 +338,6 @@ async def reload_jail( HTTPException: 409 when fail2ban reports the operation failed. HTTPException: 502 when fail2ban is unreachable. """ - socket_path: str = request.app.state.settings.fail2ban_socket try: await jail_service.reload_jail(socket_path, name) return JailCommandResponse(message=f"Jail {name!r} reloaded.", jail=name) @@ -377,14 +368,13 @@ class _IgnoreSelfRequest(IgnoreIpRequest): summary="List the ignore IPs for a jail", ) async def get_ignore_list( - request: Request, _auth: AuthDep, name: _NamePath, + socket_path: Fail2BanSocketDep, ) -> list[str]: """Return the current ignore list (IP whitelist) for a fail2ban jail. Args: - request: Incoming request (used to access ``app.state``). _auth: Validated session — enforces authentication. name: Jail name. @@ -395,7 +385,6 @@ async def get_ignore_list( HTTPException: 404 when the jail does not exist. HTTPException: 502 when fail2ban is unreachable. """ - socket_path: str = request.app.state.settings.fail2ban_socket try: return await jail_service.get_ignore_list(socket_path, name) except JailNotFoundError: @@ -411,10 +400,10 @@ async def get_ignore_list( summary="Add an IP or network to the ignore list", ) async def add_ignore_ip( - request: Request, _auth: AuthDep, name: _NamePath, body: IgnoreIpRequest, + socket_path: Fail2BanSocketDep, ) -> JailCommandResponse: """Add an IP address or CIDR network to a jail's ignore list. @@ -422,7 +411,6 @@ async def add_ignore_ip( trigger the configured fail regex. Args: - request: Incoming request (used to access ``app.state``). _auth: Validated session — enforces authentication. name: Jail name. body: Payload containing the IP or CIDR to add. @@ -436,7 +424,6 @@ async def add_ignore_ip( HTTPException: 409 when fail2ban reports the operation failed. HTTPException: 502 when fail2ban is unreachable. """ - socket_path: str = request.app.state.settings.fail2ban_socket try: await jail_service.add_ignore_ip(socket_path, name, body.ip) return JailCommandResponse( @@ -462,15 +449,14 @@ async def add_ignore_ip( summary="Remove an IP or network from the ignore list", ) async def del_ignore_ip( - request: Request, _auth: AuthDep, name: _NamePath, body: IgnoreIpRequest, + socket_path: Fail2BanSocketDep, ) -> JailCommandResponse: """Remove an IP address or CIDR network from a jail's ignore list. Args: - request: Incoming request (used to access ``app.state``). _auth: Validated session — enforces authentication. name: Jail name. body: Payload containing the IP or CIDR to remove. @@ -483,7 +469,6 @@ async def del_ignore_ip( HTTPException: 409 when fail2ban reports the operation failed. HTTPException: 502 when fail2ban is unreachable. """ - socket_path: str = request.app.state.settings.fail2ban_socket try: await jail_service.del_ignore_ip(socket_path, name, body.ip) return JailCommandResponse( @@ -504,9 +489,9 @@ async def del_ignore_ip( summary="Toggle the ignoreself option for a jail", ) async def toggle_ignore_self( - request: Request, _auth: AuthDep, name: _NamePath, + socket_path: Fail2BanSocketDep, on: bool = Body(..., description="``true`` to enable ignoreself, ``false`` to disable."), ) -> JailCommandResponse: """Toggle the ``ignoreself`` flag for a fail2ban jail. @@ -515,7 +500,6 @@ async def toggle_ignore_self( own IP addresses to the ignore list so the host can never ban itself. Args: - request: Incoming request (used to access ``app.state``). _auth: Validated session — enforces authentication. name: Jail name. on: ``true`` to enable, ``false`` to disable. @@ -528,7 +512,6 @@ async def toggle_ignore_self( HTTPException: 409 when fail2ban reports the operation failed. HTTPException: 502 when fail2ban is unreachable. """ - socket_path: str = request.app.state.settings.fail2ban_socket state_str = "enabled" if on else "disabled" try: await jail_service.set_ignore_self(socket_path, name, on=on) @@ -555,10 +538,11 @@ async def toggle_ignore_self( summary="Return paginated currently-banned IPs for a single jail", ) async def get_jail_banned_ips( - request: Request, _auth: AuthDep, db: DbDep, name: _NamePath, + socket_path: Fail2BanSocketDep, + http_session: HttpSessionDep, page: int = 1, page_size: int = 25, search: str | None = None, @@ -570,7 +554,6 @@ async def get_jail_banned_ips( geo-enriched exclusively for that page slice. Args: - request: Incoming request (used to access ``app.state``). _auth: Validated session — enforces authentication. name: Jail name. page: 1-based page number (default 1, min 1). @@ -596,9 +579,6 @@ async def get_jail_banned_ips( detail="page_size must be between 1 and 100.", ) - socket_path: str = request.app.state.settings.fail2ban_socket - http_session = getattr(request.app.state, "http_session", None) - try: return await jail_service.get_jail_banned_ips( socket_path=socket_path,