"""Server settings router. Provides endpoints to view and update fail2ban server-level settings and to flush log files. * ``GET /api/server/settings`` — current log level, target, and DB config * ``PUT /api/server/settings`` — update server-level settings * ``POST /api/server/flush-logs`` — flush and re-open log files """ from __future__ import annotations from fastapi import APIRouter, HTTPException, Request, status from app.dependencies import AuthDep from app.models.server import ServerSettingsResponse, ServerSettingsUpdate from app.services import server_service from app.services.server_service import ServerOperationError from app.utils.fail2ban_client import Fail2BanConnectionError router: APIRouter = APIRouter(prefix="/api/server", tags=["Server"]) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _bad_gateway(exc: Exception) -> HTTPException: return HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail=f"Cannot reach fail2ban: {exc}", ) def _bad_request(message: str) -> HTTPException: return HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=message, ) # --------------------------------------------------------------------------- # Endpoints # --------------------------------------------------------------------------- @router.get( "/settings", response_model=ServerSettingsResponse, summary="Return fail2ban server-level settings", ) async def get_server_settings( request: Request, _auth: AuthDep, ) -> ServerSettingsResponse: """Return the current fail2ban server-level settings. Includes log level, log target, syslog socket, database file path, database purge age, and maximum stored matches per record. Args: request: Incoming request (used to access ``app.state``). _auth: Validated session — enforces authentication. Returns: :class:`~app.models.server.ServerSettingsResponse`. Raises: HTTPException: 502 when fail2ban is unreachable. """ socket_path: str = request.app.state.settings.fail2ban_socket try: return await server_service.get_settings(socket_path) except Fail2BanConnectionError as exc: raise _bad_gateway(exc) from exc @router.put( "/settings", status_code=status.HTTP_204_NO_CONTENT, summary="Update fail2ban server-level settings", ) async def update_server_settings( request: Request, _auth: AuthDep, body: ServerSettingsUpdate, ) -> None: """Update fail2ban server-level settings. Only non-None fields in the request body are written. Changes take effect immediately without a daemon restart. Args: request: Incoming request. _auth: Validated session. body: Partial settings update. Raises: HTTPException: 400 when a set command is rejected by fail2ban. HTTPException: 502 when fail2ban is unreachable. """ socket_path: str = request.app.state.settings.fail2ban_socket try: await server_service.update_settings(socket_path, body) except ServerOperationError as exc: raise _bad_request(str(exc)) from exc except Fail2BanConnectionError as exc: raise _bad_gateway(exc) from exc @router.post( "/flush-logs", status_code=status.HTTP_200_OK, summary="Flush and re-open fail2ban log files", ) async def flush_logs( request: Request, _auth: AuthDep, ) -> dict[str, str]: """Flush and re-open fail2ban log files. Useful after log rotation so the daemon writes to the newly created log file rather than continuing to append to the rotated one. Args: request: Incoming request. _auth: Validated session. Returns: ``{"message": ""}`` Raises: HTTPException: 400 when the command is rejected. HTTPException: 502 when fail2ban is unreachable. """ socket_path: str = request.app.state.settings.fail2ban_socket try: result = await server_service.flush_logs(socket_path) return {"message": result} except ServerOperationError as exc: raise _bad_request(str(exc)) from exc except Fail2BanConnectionError as exc: raise _bad_gateway(exc) from exc