diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py index df1e6ba..f4a1dda 100644 --- a/backend/app/dependencies.py +++ b/backend/app/dependencies.py @@ -16,6 +16,8 @@ from fastapi import Depends, HTTPException, Request, status from app.config import Settings from app.models.auth import Session +from app.models.config import PendingRecovery +from app.models.server import ServerStatus from app.utils.time_utils import utc_now import aiohttp @@ -171,6 +173,15 @@ async def get_fail2ban_start_command(settings: Settings = Depends(get_settings)) """Provide the configured fail2ban start command.""" return settings.fail2ban_start_command +async def get_server_status(request: Request) -> ServerStatus: + """Return the cached fail2ban server status snapshot from app state.""" + state = cast("AppState", request.app.state) + return getattr(state, "server_status", ServerStatus(online=False)) + +async def get_pending_recovery(request: Request) -> PendingRecovery | None: + """Return the current pending recovery record from app state.""" + state = cast("AppState", request.app.state) + return getattr(state, "pending_recovery", None) async def require_auth( request: Request, @@ -242,4 +253,6 @@ SchedulerDep = Annotated[AsyncIOScheduler, Depends(get_scheduler)] Fail2BanSocketDep = Annotated[str, Depends(get_fail2ban_socket)] Fail2BanConfigDirDep = Annotated[str, Depends(get_fail2ban_config_dir)] Fail2BanStartCommandDep = Annotated[str, Depends(get_fail2ban_start_command)] +ServerStatusDep = Annotated[ServerStatus, Depends(get_server_status)] +PendingRecoveryDep = Annotated[PendingRecovery | None, Depends(get_pending_recovery)] AuthDep = Annotated[Session, Depends(require_auth)] diff --git a/backend/app/routers/blocklist.py b/backend/app/routers/blocklist.py index a1a9590..245b2a4 100644 --- a/backend/app/routers/blocklist.py +++ b/backend/app/routers/blocklist.py @@ -121,16 +121,15 @@ async def create_blocklist( summary="Trigger a manual blocklist import", ) async def run_import_now( - request: Request, + http_session: HttpSessionDep, db: DbDep, _auth: AuthDep, - http_session: HttpSessionDep, socket_path: Fail2BanSocketDep, ) -> ImportRunResult: """Download and apply all enabled blocklist sources immediately. Args: - request: Incoming request (used to access shared HTTP session). + http_session: Shared HTTP session (injected). db: Application database connection (injected). _auth: Validated session — enforces authentication. @@ -155,7 +154,6 @@ async def run_import_now( summary="Get the current import schedule", ) async def get_schedule( - request: Request, db: DbDep, _auth: AuthDep, scheduler: SchedulerDep, @@ -188,10 +186,10 @@ async def get_schedule( ) async def update_schedule( payload: ScheduleConfig, - request: Request, db: DbDep, _auth: AuthDep, scheduler: SchedulerDep, + request: Request, ) -> ScheduleInfo: """Persist a new schedule configuration and reschedule the import job. @@ -342,7 +340,7 @@ async def delete_blocklist( ) async def preview_blocklist( source_id: int, - request: Request, + http_session: HttpSessionDep, db: DbDep, _auth: AuthDep, ) -> PreviewResponse: @@ -365,7 +363,6 @@ async def preview_blocklist( if source is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Blocklist source not found.") - http_session: aiohttp.ClientSession = request.app.state.http_session try: return await blocklist_service.preview_source(source.url, http_session) except ValueError as exc: diff --git a/backend/app/routers/dashboard.py b/backend/app/routers/dashboard.py index bdafe68..38f2802 100644 --- a/backend/app/routers/dashboard.py +++ b/backend/app/routers/dashboard.py @@ -14,7 +14,7 @@ from __future__ import annotations from typing import Literal -from fastapi import APIRouter, Query, Request +from fastapi import APIRouter, Query from app import __version__ from app.dependencies import ( @@ -22,6 +22,7 @@ from app.dependencies import ( DbDep, Fail2BanSocketDep, HttpSessionDep, + ServerStatusDep, ) from app.models.ban import ( BanOrigin, @@ -50,7 +51,7 @@ _DEFAULT_RANGE: TimeRange = "24h" summary="Return the cached fail2ban server status", ) async def get_server_status( - request: Request, + server_status: ServerStatusDep, _auth: AuthDep, ) -> ServerStatusResponse: """Return the most recent fail2ban health snapshot. @@ -60,18 +61,14 @@ async def get_server_status( returned so the response is always well-formed. Args: - request: The incoming request (used to access ``app.state``). + server_status: Cached fail2ban server health snapshot (injected). _auth: Validated session — enforces authentication on this endpoint. Returns: :class:`~app.models.server.ServerStatusResponse` containing the current health snapshot. """ - cached: ServerStatus = getattr( - request.app.state, - "server_status", - ServerStatus(online=False), - ) + cached: ServerStatus = server_status cached.version = __version__ return ServerStatusResponse(status=cached) diff --git a/backend/app/routers/file_config.py b/backend/app/routers/file_config.py index e29e853..d538dd3 100644 --- a/backend/app/routers/file_config.py +++ b/backend/app/routers/file_config.py @@ -31,9 +31,9 @@ from __future__ import annotations from typing import Annotated -from fastapi import APIRouter, HTTPException, Path, Request, status +from fastapi import APIRouter, HTTPException, Path, status -from app.dependencies import AuthDep +from app.dependencies import AuthDep, Fail2BanConfigDirDep from app.models.config import ( ActionConfig, ActionConfigUpdate, @@ -117,7 +117,7 @@ def _service_unavailable(message: str) -> HTTPException: summary="List all jail config files", ) async def list_jail_config_files( - request: Request, + config_dir: Fail2BanConfigDirDep, _auth: AuthDep, ) -> JailConfigFilesResponse: """Return metadata for every ``.conf`` and ``.local`` file in ``jail.d/``. @@ -126,13 +126,12 @@ async def list_jail_config_files( file (defaulting to ``true`` when the key is absent). Args: - request: Incoming request (used for ``app.state.settings``). + config_dir: Config directory path injected from application settings. _auth: Validated session — enforces authentication. Returns: :class:`~app.models.file_config.JailConfigFilesResponse`. """ - config_dir: str = request.app.state.settings.fail2ban_config_dir try: return await raw_config_io_service.list_jail_config_files(config_dir) except ConfigDirError as exc: @@ -145,7 +144,7 @@ async def list_jail_config_files( summary="Return a single jail config file with its content", ) async def get_jail_config_file( - request: Request, + config_dir: Fail2BanConfigDirDep, _auth: AuthDep, filename: _FilenamePath, ) -> JailConfigFileContent: @@ -164,7 +163,6 @@ async def get_jail_config_file( HTTPException: 404 if the file does not exist. HTTPException: 503 if the config directory is unavailable. """ - config_dir: str = request.app.state.settings.fail2ban_config_dir try: return await raw_config_io_service.get_jail_config_file(config_dir, filename) except ConfigFileNameError as exc: @@ -181,7 +179,7 @@ async def get_jail_config_file( summary="Overwrite a jail.d config file with new raw content", ) async def write_jail_config_file( - request: Request, + config_dir: Fail2BanConfigDirDep, _auth: AuthDep, filename: _FilenamePath, body: ConfFileUpdateRequest, @@ -202,7 +200,6 @@ async def write_jail_config_file( HTTPException: 404 if the file does not exist. HTTPException: 503 if the config directory is unavailable. """ - config_dir: str = request.app.state.settings.fail2ban_config_dir try: await raw_config_io_service.write_jail_config_file(config_dir, filename, body) except ConfigFileNameError as exc: @@ -221,7 +218,7 @@ async def write_jail_config_file( summary="Enable or disable a jail configuration file", ) async def set_jail_config_file_enabled( - request: Request, + config_dir: Fail2BanConfigDirDep, _auth: AuthDep, filename: _FilenamePath, body: JailConfigFileEnabledUpdate, @@ -242,7 +239,6 @@ async def set_jail_config_file_enabled( HTTPException: 404 if the file does not exist. HTTPException: 503 if the config directory is unavailable. """ - config_dir: str = request.app.state.settings.fail2ban_config_dir try: await raw_config_io_service.set_jail_config_enabled( config_dir, filename, body.enabled @@ -264,7 +260,7 @@ async def set_jail_config_file_enabled( summary="Create a new jail.d config file", ) async def create_jail_config_file( - request: Request, + config_dir: Fail2BanConfigDirDep, _auth: AuthDep, body: ConfFileCreateRequest, ) -> ConfFileContent: @@ -283,7 +279,6 @@ async def create_jail_config_file( HTTPException: 409 if a file with that name already exists. HTTPException: 503 if the config directory is unavailable. """ - config_dir: str = request.app.state.settings.fail2ban_config_dir try: filename = await raw_config_io_service.create_jail_config_file(config_dir, body) except ConfigFileNameError as exc: @@ -313,7 +308,7 @@ async def create_jail_config_file( summary="Return a filter definition file's raw content", ) async def get_filter_file_raw( - request: Request, + config_dir: Fail2BanConfigDirDep, _auth: AuthDep, name: _NamePath, ) -> ConfFileContent: @@ -336,7 +331,6 @@ async def get_filter_file_raw( HTTPException: 404 if the file does not exist. HTTPException: 503 if the config directory is unavailable. """ - config_dir: str = request.app.state.settings.fail2ban_config_dir try: return await raw_config_io_service.get_filter_file(config_dir, name) except ConfigFileNameError as exc: @@ -353,7 +347,7 @@ async def get_filter_file_raw( summary="Update a filter definition file (raw content)", ) async def write_filter_file( - request: Request, + config_dir: Fail2BanConfigDirDep, _auth: AuthDep, name: _NamePath, body: ConfFileUpdateRequest, @@ -371,7 +365,6 @@ async def write_filter_file( HTTPException: 404 if the file does not exist. HTTPException: 503 if the config directory is unavailable. """ - config_dir: str = request.app.state.settings.fail2ban_config_dir try: await raw_config_io_service.write_filter_file(config_dir, name, body) except ConfigFileNameError as exc: @@ -391,7 +384,7 @@ async def write_filter_file( summary="Create a new filter definition file (raw content)", ) async def create_filter_file( - request: Request, + config_dir: Fail2BanConfigDirDep, _auth: AuthDep, body: ConfFileCreateRequest, ) -> ConfFileContent: @@ -410,7 +403,6 @@ async def create_filter_file( HTTPException: 409 if a file with that name already exists. HTTPException: 503 if the config directory is unavailable. """ - config_dir: str = request.app.state.settings.fail2ban_config_dir try: filename = await raw_config_io_service.create_filter_file(config_dir, body) except ConfigFileNameError as exc: @@ -440,7 +432,7 @@ async def create_filter_file( summary="List all action definition files", ) async def list_action_files( - request: Request, + config_dir: Fail2BanConfigDirDep, _auth: AuthDep, ) -> ConfFilesResponse: """Return a list of every ``.conf`` and ``.local`` file in ``action.d/``. @@ -452,7 +444,6 @@ async def list_action_files( Returns: :class:`~app.models.file_config.ConfFilesResponse`. """ - config_dir: str = request.app.state.settings.fail2ban_config_dir try: return await raw_config_io_service.list_action_files(config_dir) except ConfigDirError as exc: @@ -465,7 +456,7 @@ async def list_action_files( summary="Return an action definition file with its content", ) async def get_action_file( - request: Request, + config_dir: Fail2BanConfigDirDep, _auth: AuthDep, name: _NamePath, ) -> ConfFileContent: @@ -484,7 +475,6 @@ async def get_action_file( HTTPException: 404 if the file does not exist. HTTPException: 503 if the config directory is unavailable. """ - config_dir: str = request.app.state.settings.fail2ban_config_dir try: return await raw_config_io_service.get_action_file(config_dir, name) except ConfigFileNameError as exc: @@ -501,7 +491,7 @@ async def get_action_file( summary="Update an action definition file", ) async def write_action_file( - request: Request, + config_dir: Fail2BanConfigDirDep, _auth: AuthDep, name: _NamePath, body: ConfFileUpdateRequest, @@ -519,7 +509,6 @@ async def write_action_file( HTTPException: 404 if the file does not exist. HTTPException: 503 if the config directory is unavailable. """ - config_dir: str = request.app.state.settings.fail2ban_config_dir try: await raw_config_io_service.write_action_file(config_dir, name, body) except ConfigFileNameError as exc: @@ -539,7 +528,7 @@ async def write_action_file( summary="Create a new action definition file", ) async def create_action_file( - request: Request, + config_dir: Fail2BanConfigDirDep, _auth: AuthDep, body: ConfFileCreateRequest, ) -> ConfFileContent: @@ -558,7 +547,6 @@ async def create_action_file( HTTPException: 409 if a file with that name already exists. HTTPException: 503 if the config directory is unavailable. """ - config_dir: str = request.app.state.settings.fail2ban_config_dir try: filename = await raw_config_io_service.create_action_file(config_dir, body) except ConfigFileNameError as exc: @@ -588,7 +576,7 @@ async def create_action_file( summary="Return a filter file parsed into a structured model", ) async def get_parsed_filter( - request: Request, + config_dir: Fail2BanConfigDirDep, _auth: AuthDep, name: _NamePath, ) -> FilterConfig: @@ -611,7 +599,6 @@ async def get_parsed_filter( HTTPException: 404 if the file does not exist. HTTPException: 503 if the config directory is unavailable. """ - config_dir: str = request.app.state.settings.fail2ban_config_dir try: return await raw_config_io_service.get_parsed_filter_file(config_dir, name) except ConfigFileNameError as exc: @@ -628,7 +615,7 @@ async def get_parsed_filter( summary="Update a filter file from a structured model", ) async def update_parsed_filter( - request: Request, + config_dir: Fail2BanConfigDirDep, _auth: AuthDep, name: _NamePath, body: FilterConfigUpdate, @@ -649,7 +636,6 @@ async def update_parsed_filter( HTTPException: 404 if the file does not exist. HTTPException: 503 if the config directory is unavailable. """ - config_dir: str = request.app.state.settings.fail2ban_config_dir try: await raw_config_io_service.update_parsed_filter_file(config_dir, name, body) except ConfigFileNameError as exc: @@ -673,7 +659,7 @@ async def update_parsed_filter( summary="Return an action file parsed into a structured model", ) async def get_parsed_action( - request: Request, + config_dir: Fail2BanConfigDirDep, _auth: AuthDep, name: _NamePath, ) -> ActionConfig: @@ -696,7 +682,6 @@ async def get_parsed_action( HTTPException: 404 if the file does not exist. HTTPException: 503 if the config directory is unavailable. """ - config_dir: str = request.app.state.settings.fail2ban_config_dir try: return await raw_config_io_service.get_parsed_action_file(config_dir, name) except ConfigFileNameError as exc: @@ -713,7 +698,7 @@ async def get_parsed_action( summary="Update an action file from a structured model", ) async def update_parsed_action( - request: Request, + config_dir: Fail2BanConfigDirDep, _auth: AuthDep, name: _NamePath, body: ActionConfigUpdate, @@ -734,7 +719,6 @@ async def update_parsed_action( HTTPException: 404 if the file does not exist. HTTPException: 503 if the config directory is unavailable. """ - config_dir: str = request.app.state.settings.fail2ban_config_dir try: await raw_config_io_service.update_parsed_action_file(config_dir, name, body) except ConfigFileNameError as exc: @@ -758,7 +742,7 @@ async def update_parsed_action( summary="Return a jail.d file parsed into a structured model", ) async def get_parsed_jail_file( - request: Request, + config_dir: Fail2BanConfigDirDep, _auth: AuthDep, filename: _NamePath, ) -> JailFileConfig: @@ -781,7 +765,6 @@ async def get_parsed_jail_file( HTTPException: 404 if the file does not exist. HTTPException: 503 if the config directory is unavailable. """ - config_dir: str = request.app.state.settings.fail2ban_config_dir try: return await raw_config_io_service.get_parsed_jail_file(config_dir, filename) except ConfigFileNameError as exc: @@ -798,7 +781,7 @@ async def get_parsed_jail_file( summary="Update a jail.d file from a structured model", ) async def update_parsed_jail_file( - request: Request, + config_dir: Fail2BanConfigDirDep, _auth: AuthDep, filename: _NamePath, body: JailFileConfigUpdate, @@ -819,7 +802,6 @@ async def update_parsed_jail_file( HTTPException: 404 if the file does not exist. HTTPException: 503 if the config directory is unavailable. """ - config_dir: str = request.app.state.settings.fail2ban_config_dir try: await raw_config_io_service.update_parsed_jail_file(config_dir, filename, body) except ConfigFileNameError as exc: diff --git a/backend/app/routers/geo.py b/backend/app/routers/geo.py index 3f9e4f1..4f38e81 100644 --- a/backend/app/routers/geo.py +++ b/backend/app/routers/geo.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: from app.services.jail_service import IpLookupResult import aiosqlite -from fastapi import APIRouter, Depends, HTTPException, Path, Request, status +from fastapi import APIRouter, Depends, HTTPException, Path, status from app.dependencies import ( AuthDep, @@ -40,7 +40,6 @@ _IpPath = Annotated[str, Path(description="IPv4 or IPv6 address to look up.")] summary="Look up ban status and geo information for an IP", ) async def lookup_ip( - request: Request, _auth: AuthDep, ip: _IpPath, socket_path: Fail2BanSocketDep, @@ -53,7 +52,6 @@ async def lookup_ip( organisation data from ip-api.com. Args: - request: Incoming request (used to access ``app.state``). _auth: Validated session — enforces authentication. ip: The IP address to look up. @@ -140,7 +138,6 @@ async def geo_stats( summary="Re-resolve all IPs whose country could not be determined", ) async def re_resolve_geo( - request: Request, _auth: AuthDep, db: Annotated[aiosqlite.Connection, Depends(get_db)], http_session: HttpSessionDep, @@ -151,9 +148,9 @@ async def re_resolve_geo( are immediately eligible for a new API attempt. Args: - request: Incoming request (used to access ``app.state.http_session``). _auth: Validated session — enforces authentication. db: BanGUI application database (for reading/writing ``geo_cache``). + http_session: Shared HTTP session for geo lookups. Returns: JSON object ``{"resolved": N, "total": M}`` where *N* is the number