diff --git a/Docs/Backend-Development.md b/Docs/Backend-Development.md index 55fb8ec..5436893 100644 --- a/Docs/Backend-Development.md +++ b/Docs/Backend-Development.md @@ -253,44 +253,60 @@ The **repository boundary** separates database-aware code from application logic | Layer | Responsibilities | Dependencies | |---|---|---| -| **Routers** | Receive requests, validate input, return responses. | Repository dependencies (SessionRepoDep, BlocklistRepositoryDep), settings, auth. Never raw database connections. | +| **Routers** | Receive requests, validate input, return responses. | Service context dependencies (SessionServiceContextDep, BlocklistServiceContextDep), settings, auth. Never raw database connections. | | **Services** | Contain business logic, orchestrate operations. | Other services, repositories. May receive `aiosqlite.Connection` for repository operations. | | **Repositories** | Execute all SQL queries. All database knowledge lives here. | `aiosqlite.Connection` (from callers). | **Rule: Routers must NOT depend on `DbDep` (raw database connections).** Instead, routers should: -1. Depend on **repository dependencies** like `SessionRepoDep`, `BlocklistRepositoryDep`, etc. -2. Pass repositories to services, not raw database connections. -3. Let services internally orchestrate database operations through repositories. +1. Depend on **service context dependencies** like `SessionServiceContextDep`, `BlocklistServiceContextDep`, etc. +2. These context dependencies combine the database connection and related repositories. +3. Pass the context to services, which internally orchestrate database operations. + +**Service Context Dependencies Available:** +- `SessionServiceContextDep` — Contains `db` and `session_repo` for session operations. +- `BlocklistServiceContextDep` — Contains `db`, `blocklist_repo`, `import_log_repo`, `settings_repo`. +- `SettingsServiceContextDep` — Contains `db` and `settings_repo`. +- `BanServiceContextDep` — Contains `db` and `fail2ban_db_repo`. +- `HistoryServiceContextDep` — Contains `db`, `fail2ban_db_repo`, `history_archive_repo`. **Why:** - **Enforcement**: Not exporting `DbDep` from the dependencies module makes it impossible for routers to accidentally bypass repositories. -- **Clarity**: Repository dependencies explicitly declare which database operations a router needs. +- **Clarity**: Service context dependencies explicitly declare which database operations a router needs. - **Testability**: Services and routers are easier to test when they depend on repositories (which can be mocked) rather than raw connections. **Example:** ```python -# ✅ GOOD — router depends on repository -@router.get("/blocklists") -async def list_blocklists( - blocklist_repo: BlocklistRepositoryDep, +# ✅ GOOD — router depends on service context +@router.post("/login") +async def login( + body: LoginRequest, + response: Response, + session_ctx: SessionServiceContextDep, # Contains db + session_repo _auth: AuthDep, -) -> BlocklistListResponse: - sources = await blocklist_repo.list_sources(???) # db comes from where? - return BlocklistListResponse(sources=sources) +) -> LoginResponse: + return await auth_service.login( + session_ctx.db, + password=body.password, + session_repo=session_ctx.session_repo, + ... + ) # ❌ BAD — router depends on raw db (DbDep is not exported for this reason) -@router.get("/blocklists") -async def list_blocklists( +@router.post("/login") +async def login( + body: LoginRequest, db: DbDep, # ← Cannot import DbDep in routers _auth: AuthDep, -) -> BlocklistListResponse: - sources = await blocklist_service.list_sources(db) - return BlocklistListResponse(sources=sources) +) -> LoginResponse: + return await auth_service.login(db, password=body.password, ...) ``` -**Migration Path**: Services are gradually being refactored to accept repositories instead of raw database connections. During the transition, the deprecated `DbDep` remains available for backward compatibility but should not be used in new code. +**DEPRECATED: DbDep** +- The `DbDep` type alias is provided for backward compatibility only. +- DO NOT use in new code. Use service context dependencies instead. +- See `backend/app/dependencies.py` for available service contexts. --- diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py index 9920f9a..55b7d2a 100644 --- a/backend/app/dependencies.py +++ b/backend/app/dependencies.py @@ -374,6 +374,169 @@ async def get_fail2ban_metadata_service() -> object: return default_fail2ban_metadata_service +# ----------------------------------------------------------------------- +# Service facade dependencies (db + repositories combined) +# These are for routers that need database access through services. +# Routers should depend on these instead of raw database connections. +# ----------------------------------------------------------------------- + + +@dataclass +class SessionServiceContext: + """Context for session-related database operations. + + Combines the database connection and session repository so that + routers don't need to import DbDep directly. + """ + + db: aiosqlite.Connection + session_repo: SessionRepository + + +async def get_session_service_context( + db: Annotated[aiosqlite.Connection, Depends(get_db)], + session_repo: Annotated[SessionRepository, Depends(get_session_repo)], +) -> SessionServiceContext: + """Provide combined session database context for routers. + + Args: + db: Request-scoped database connection. + session_repo: Session repository implementation. + + Returns: + SessionServiceContext with both db and repository. + """ + return SessionServiceContext(db=db, session_repo=session_repo) + + +@dataclass +class BlocklistServiceContext: + """Context for blocklist-related database operations. + + Combines the database connection and blocklist-related repositories + so that routers don't need to import DbDep directly. + """ + + db: aiosqlite.Connection + blocklist_repo: BlocklistRepository + import_log_repo: ImportLogRepository + settings_repo: SettingsRepository + + +async def get_blocklist_service_context( + db: Annotated[aiosqlite.Connection, Depends(get_db)], + blocklist_repo: Annotated[BlocklistRepository, Depends(get_blocklist_repo)], + import_log_repo: Annotated[ImportLogRepository, Depends(get_import_log_repo)], + settings_repo: Annotated[SettingsRepository, Depends(get_settings_repo)], +) -> BlocklistServiceContext: + """Provide combined blocklist database context for routers. + + Args: + db: Request-scoped database connection. + blocklist_repo: Blocklist repository implementation. + import_log_repo: Import log repository implementation. + settings_repo: Settings repository implementation. + + Returns: + BlocklistServiceContext with db and all blocklist repositories. + """ + return BlocklistServiceContext( + db=db, + blocklist_repo=blocklist_repo, + import_log_repo=import_log_repo, + settings_repo=settings_repo, + ) + + +@dataclass +class SettingsServiceContext: + """Context for settings-related database operations. + + Combines the database connection and settings repository so that + routers don't need to import DbDep directly. + """ + + db: aiosqlite.Connection + settings_repo: SettingsRepository + + +async def get_settings_service_context( + db: Annotated[aiosqlite.Connection, Depends(get_db)], + settings_repo: Annotated[SettingsRepository, Depends(get_settings_repo)], +) -> SettingsServiceContext: + """Provide combined settings database context for routers. + + Args: + db: Request-scoped database connection. + settings_repo: Settings repository implementation. + + Returns: + SettingsServiceContext with both db and repository. + """ + return SettingsServiceContext(db=db, settings_repo=settings_repo) + + +@dataclass +class BanServiceContext: + """Context for ban-related database operations. + + Combines the database connection and fail2ban DB repository. + """ + + db: aiosqlite.Connection + fail2ban_db_repo: Fail2BanDbRepository + + +async def get_ban_service_context( + db: Annotated[aiosqlite.Connection, Depends(get_db)], + fail2ban_db_repo: Annotated[Fail2BanDbRepository, Depends(get_fail2ban_db_repo)], +) -> BanServiceContext: + """Provide combined ban database context for routers. + + Args: + db: Request-scoped database connection. + fail2ban_db_repo: Fail2Ban DB repository implementation. + + Returns: + BanServiceContext with both db and repository. + """ + return BanServiceContext(db=db, fail2ban_db_repo=fail2ban_db_repo) + + +@dataclass +class HistoryServiceContext: + """Context for history-related database operations. + + Combines database connection and history-related repositories. + """ + + db: aiosqlite.Connection + fail2ban_db_repo: Fail2BanDbRepository + history_archive_repo: HistoryArchiveRepository + + +async def get_history_service_context( + db: Annotated[aiosqlite.Connection, Depends(get_db)], + fail2ban_db_repo: Annotated[Fail2BanDbRepository, Depends(get_fail2ban_db_repo)], + history_archive_repo: Annotated[HistoryArchiveRepository, Depends(get_history_archive_repo)], +) -> HistoryServiceContext: + """Provide combined history database context for routers. + + Args: + db: Request-scoped database connection. + fail2ban_db_repo: Fail2Ban DB repository implementation. + history_archive_repo: History archive repository implementation. + + Returns: + HistoryServiceContext with db and all history repositories. + """ + return HistoryServiceContext( + db=db, + fail2ban_db_repo=fail2ban_db_repo, + history_archive_repo=history_archive_repo, + ) + + # Internal database dependency for use by other dependencies only # Routers should NOT import this - they should use repository dependencies instead _DbDep = Annotated[aiosqlite.Connection, Depends(get_db)] @@ -482,6 +645,14 @@ AuthDep = Annotated[Session, Depends(require_auth)] LoginRateLimiterDep = Annotated[RateLimiter, Depends(get_login_rate_limiter)] Fail2BanMetadataServiceDep = Annotated[Fail2BanMetadataService, Depends(get_fail2ban_metadata_service)] +# Service context dependencies (db + repositories combined for routers) +# Routers should use these instead of importing DbDep directly. +SessionServiceContextDep = Annotated[SessionServiceContext, Depends(get_session_service_context)] +BlocklistServiceContextDep = Annotated[BlocklistServiceContext, Depends(get_blocklist_service_context)] +SettingsServiceContextDep = Annotated[SettingsServiceContext, Depends(get_settings_service_context)] +BanServiceContextDep = Annotated[BanServiceContext, Depends(get_ban_service_context)] +HistoryServiceContextDep = Annotated[HistoryServiceContext, Depends(get_history_service_context)] + # DEPRECATED: DbDep is provided for backward compatibility only. # DO NOT use in new code. Use repository dependencies instead (SessionRepoDep, BlocklistRepositoryDep, etc.) # See Backend-Development.md § 6 for dependency layering rules. diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index b9f4c72..27ab22e 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -26,10 +26,9 @@ from fastapi import APIRouter, HTTPException, Request, Response, status from app.dependencies import ( AuthDep, - DbDep, LoginRateLimiterDep, SessionCacheDep, - SessionRepoDep, + SessionServiceContextDep, SettingsDep, ) from app.models.auth import LoginRequest, LoginResponse, LogoutResponse @@ -55,9 +54,8 @@ async def login( body: LoginRequest, response: Response, request: Request, - db: DbDep, + session_ctx: SessionServiceContextDep, settings: SettingsDep, - session_repo: SessionRepoDep, rate_limiter: LoginRateLimiterDep, ) -> LoginResponse: """Verify the master password and return a session token. @@ -73,9 +71,8 @@ async def login( body: Login request validated by Pydantic. response: FastAPI response object used to set the cookie. request: The incoming HTTP request (used to extract client IP). - db: Injected aiosqlite connection. + session_ctx: Session service context containing db and repository. settings: Application settings (used for session duration). - session_repo: The session repository. rate_limiter: The login rate limiter (per IP). Returns: @@ -97,11 +94,11 @@ async def login( try: signed_token, expires_at = await auth_service.login( - db, + session_ctx.db, password=body.password, session_duration_minutes=settings.session_duration_minutes, session_secret=settings.session_secret, - session_repo=session_repo, + session_repo=session_ctx.session_repo, ) except ValueError as exc: # Add delay on wrong password to slow down brute-force attacks. @@ -159,10 +156,9 @@ async def validate_session( async def logout( request: Request, response: Response, - db: DbDep, + session_ctx: SessionServiceContextDep, settings: SettingsDep, session_cache: SessionCacheDep, - session_repo: SessionRepoDep, ) -> LogoutResponse: """Invalidate the active session. @@ -173,8 +169,9 @@ async def logout( Args: request: FastAPI request (used to extract the token). response: FastAPI response (used to clear the cookie). - db: Injected aiosqlite connection. + session_ctx: Session service context containing db and repository. settings: Application settings (used to unwrap signed tokens). + session_cache: Session cache for invalidation. Returns: :class:`~app.models.auth.LogoutResponse`. @@ -182,10 +179,10 @@ async def logout( token = _extract_token(request) if token: raw_token = await auth_service.logout( - db, + session_ctx.db, token, settings.session_secret, - session_repo=session_repo, + session_repo=session_ctx.session_repo, ) if raw_token: session_cache.invalidate(raw_token) diff --git a/backend/app/routers/bans.py b/backend/app/routers/bans.py index 9eb0c3a..8857c92 100644 --- a/backend/app/routers/bans.py +++ b/backend/app/routers/bans.py @@ -14,7 +14,7 @@ from fastapi import APIRouter, Request, status from app.dependencies import ( AuthDep, - DbDep, + BanServiceContextDep, Fail2BanSocketDep, GeoCacheDep, HttpSessionDep, @@ -34,7 +34,7 @@ router: APIRouter = APIRouter(prefix="/api/bans", tags=["Bans"]) async def get_active_bans( request: Request, _auth: AuthDep, - db: DbDep, + ban_ctx: BanServiceContextDep, socket_path: Fail2BanSocketDep, http_session: HttpSessionDep, geo_cache: GeoCacheDep, @@ -47,6 +47,10 @@ async def get_active_bans( Args: request: Incoming request (used to access ``app.state``). _auth: Validated session — enforces authentication. + ban_ctx: Ban service context containing db and repository. + socket_path: Path to fail2ban Unix domain socket. + http_session: Shared HTTP session for geolocation. + geo_cache: Geolocation cache instance. Returns: :class:`~app.models.ban.ActiveBanListResponse` with all active bans. @@ -58,7 +62,7 @@ async def get_active_bans( socket_path, geo_cache=geo_cache, http_session=http_session, - app_db=db, + app_db=ban_ctx.db, ) diff --git a/backend/app/routers/blocklist.py b/backend/app/routers/blocklist.py index e324667..a60faa6 100644 --- a/backend/app/routers/blocklist.py +++ b/backend/app/routers/blocklist.py @@ -26,7 +26,7 @@ from fastapi import APIRouter, HTTPException, Query, status from app.dependencies import ( AuthDep, - DbDep, + BlocklistServiceContextDep, Fail2BanSocketDep, GeoCacheDep, HttpSessionDep, @@ -61,19 +61,19 @@ router: APIRouter = APIRouter(prefix="/api/blocklists", tags=["Blocklists"]) summary="List all blocklist sources", ) async def list_blocklists( - db: DbDep, + blocklist_ctx: BlocklistServiceContextDep, _auth: AuthDep, ) -> BlocklistListResponse: """Return all configured blocklist source definitions. Args: - db: Application database connection (injected). + blocklist_ctx: Blocklist service context containing db and repositories. _auth: Validated session — enforces authentication. Returns: :class:`~app.models.blocklist.BlocklistListResponse` with all sources. """ - sources = await blocklist_service.list_sources(db) + sources = await blocklist_service.list_sources(blocklist_ctx.db) return BlocklistListResponse(sources=sources) @@ -85,14 +85,14 @@ async def list_blocklists( ) async def create_blocklist( payload: BlocklistSourceCreate, - db: DbDep, + blocklist_ctx: BlocklistServiceContextDep, _auth: AuthDep, ) -> BlocklistSource: """Create a new blocklist source definition. Args: payload: New source data (name, url, enabled). - db: Application database connection (injected). + blocklist_ctx: Blocklist service context containing db and repositories. _auth: Validated session — enforces authentication. Returns: @@ -103,7 +103,7 @@ async def create_blocklist( """ try: return await blocklist_service.create_source( - db, payload.name, str(payload.url), enabled=payload.enabled + blocklist_ctx.db, payload.name, str(payload.url), enabled=payload.enabled ) except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc @@ -121,7 +121,7 @@ async def create_blocklist( ) async def run_import_now( http_session: HttpSessionDep, - db: DbDep, + blocklist_ctx: BlocklistServiceContextDep, _auth: AuthDep, socket_path: Fail2BanSocketDep, geo_cache: GeoCacheDep, @@ -130,8 +130,10 @@ async def run_import_now( Args: http_session: Shared HTTP session (injected). - db: Application database connection (injected). + blocklist_ctx: Blocklist service context containing db and repositories. _auth: Validated session — enforces authentication. + socket_path: Path to fail2ban Unix domain socket. + geo_cache: Geolocation cache instance. Returns: :class:`~app.models.blocklist.ImportRunResult` with per-source @@ -139,7 +141,7 @@ async def run_import_now( """ return await blocklist_service.import_all( - db, + blocklist_ctx.db, http_session, socket_path, geo_is_cached=geo_cache.is_cached, @@ -154,7 +156,7 @@ async def run_import_now( summary="Get the current import schedule", ) async def get_schedule( - db: DbDep, + blocklist_ctx: BlocklistServiceContextDep, _auth: AuthDep, scheduler: SchedulerDep, ) -> ScheduleInfo: @@ -163,14 +165,15 @@ async def get_schedule( The ``next_run_at`` field is read from APScheduler if the job is active. Args: - db: Application database connection (injected). + blocklist_ctx: Blocklist service context containing db and repositories. _auth: Validated session — enforces authentication. + scheduler: APScheduler instance. Returns: :class:`~app.models.blocklist.ScheduleInfo` with config and run times. """ - return await blocklist_service.get_schedule_info_with_runtime(db, scheduler) + return await blocklist_service.get_schedule_info_with_runtime(blocklist_ctx.db, scheduler) @router.put( @@ -180,7 +183,7 @@ async def get_schedule( ) async def update_schedule( payload: ScheduleConfig, - db: DbDep, + blocklist_ctx: BlocklistServiceContextDep, _auth: AuthDep, scheduler: SchedulerDep, http_session: HttpSessionDep, @@ -190,7 +193,7 @@ async def update_schedule( Args: payload: New :class:`~app.models.blocklist.ScheduleConfig`. - db: Application database connection (injected). + blocklist_ctx: Blocklist service context containing db and repositories. _auth: Validated session — enforces authentication. scheduler: Shared APScheduler instance (injected). http_session: Shared HTTP session used by the scheduler job. @@ -200,7 +203,7 @@ async def update_schedule( Updated :class:`~app.models.blocklist.ScheduleInfo`. """ return await blocklist_service.update_schedule( - db, + blocklist_ctx.db, scheduler, http_session, settings, @@ -215,7 +218,7 @@ async def update_schedule( summary="Get the paginated import log", ) async def get_import_log( - db: DbDep, + blocklist_ctx: BlocklistServiceContextDep, _auth: AuthDep, source_id: int | None = Query(default=None, description="Filter by source id"), page: int = Query(default=1, ge=1), @@ -224,7 +227,7 @@ async def get_import_log( """Return a paginated log of all import runs. Args: - db: Application database connection (injected). + blocklist_ctx: Blocklist service context containing db and repositories. _auth: Validated session — enforces authentication. source_id: Optional filter — only show logs for this source. page: 1-based page number. @@ -234,7 +237,7 @@ async def get_import_log( :class:`~app.models.blocklist.ImportLogListResponse`. """ return await blocklist_service.list_import_logs( - db, source_id=source_id, page=page, page_size=page_size + blocklist_ctx.db, source_id=source_id, page=page, page_size=page_size ) @@ -250,20 +253,20 @@ async def get_import_log( ) async def get_blocklist( source_id: int, - db: DbDep, + blocklist_ctx: BlocklistServiceContextDep, _auth: AuthDep, ) -> BlocklistSource: """Return a single blocklist source by id. Args: source_id: Primary key of the source. - db: Application database connection (injected). + blocklist_ctx: Blocklist service context containing db and repositories. _auth: Validated session — enforces authentication. Raises: HTTPException: 404 if the source does not exist. """ - source = await blocklist_service.get_source(db, source_id) + source = await blocklist_service.get_source(blocklist_ctx.db, source_id) if source is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Blocklist source not found.") return source @@ -277,7 +280,7 @@ async def get_blocklist( async def update_blocklist( source_id: int, payload: BlocklistSourceUpdate, - db: DbDep, + blocklist_ctx: BlocklistServiceContextDep, _auth: AuthDep, ) -> BlocklistSource: """Update one or more fields on a blocklist source. @@ -285,7 +288,7 @@ async def update_blocklist( Args: source_id: Primary key of the source to update. payload: Fields to update (all optional). - db: Application database connection (injected). + blocklist_ctx: Blocklist service context containing db and repositories. _auth: Validated session — enforces authentication. Raises: @@ -294,7 +297,7 @@ async def update_blocklist( """ try: updated = await blocklist_service.update_source( - db, + blocklist_ctx.db, source_id, name=payload.name, url=str(payload.url) if payload.url is not None else None, @@ -314,20 +317,20 @@ async def update_blocklist( ) async def delete_blocklist( source_id: int, - db: DbDep, + blocklist_ctx: BlocklistServiceContextDep, _auth: AuthDep, ) -> None: """Delete a blocklist source by id. Args: source_id: Primary key of the source to remove. - db: Application database connection (injected). + blocklist_ctx: Blocklist service context containing db and repositories. _auth: Validated session — enforces authentication. Raises: HTTPException: 404 if the source does not exist. """ - deleted = await blocklist_service.delete_source(db, source_id) + deleted = await blocklist_service.delete_source(blocklist_ctx.db, source_id) if not deleted: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Blocklist source not found.") @@ -340,7 +343,7 @@ async def delete_blocklist( async def preview_blocklist( source_id: int, http_session: HttpSessionDep, - db: DbDep, + blocklist_ctx: BlocklistServiceContextDep, _auth: AuthDep, ) -> PreviewResponse: """Download and preview a sample of a blocklist source. @@ -350,15 +353,15 @@ async def preview_blocklist( Args: source_id: Primary key of the source to preview. - request: Incoming request (used to access the HTTP session). - db: Application database connection (injected). + http_session: Shared HTTP session for downloading. + blocklist_ctx: Blocklist service context containing db and repositories. _auth: Validated session — enforces authentication. Raises: HTTPException: 404 if the source does not exist. HTTPException: 502 if the URL cannot be reached. """ - source = await blocklist_service.get_source(db, source_id) + source = await blocklist_service.get_source(blocklist_ctx.db, source_id) if source is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Blocklist source not found.") diff --git a/backend/app/routers/config_misc.py b/backend/app/routers/config_misc.py index 2c9dbd7..0e1827a 100644 --- a/backend/app/routers/config_misc.py +++ b/backend/app/routers/config_misc.py @@ -8,9 +8,9 @@ from fastapi import APIRouter, HTTPException, Query, Request, status from app.dependencies import ( AuthDep, - DbDep, Fail2BanSocketDep, Fail2BanStartCommandDep, + SettingsServiceContextDep, ) from app.models.config import ( Fail2BanLogResponse, @@ -241,19 +241,20 @@ async def preview_log( async def get_map_color_thresholds( _request: Request, _auth: AuthDep, - db: DbDep, + settings_ctx: SettingsServiceContextDep, ) -> MapColorThresholdsResponse: """Return the configured map color thresholds. Args: - request: FastAPI request object. + _request: FastAPI request object. _auth: Validated session. + settings_ctx: Settings service context containing db and repository. Returns: :class:`~app.models.config.MapColorThresholdsResponse` with current thresholds. """ - return await config_service.get_map_color_thresholds(db) + return await config_service.get_map_color_thresholds(settings_ctx.db) @router.put( "/map-color-thresholds", @@ -263,14 +264,15 @@ async def get_map_color_thresholds( async def update_map_color_thresholds( _request: Request, _auth: AuthDep, - db: DbDep, + settings_ctx: SettingsServiceContextDep, body: MapColorThresholdsUpdate, ) -> MapColorThresholdsResponse: """Update the map color threshold configuration. Args: - request: FastAPI request object. + _request: FastAPI request object. _auth: Validated session. + settings_ctx: Settings service context containing db and repository. body: New threshold values. Returns: @@ -281,8 +283,8 @@ async def update_map_color_thresholds( HTTPException: 400 if validation fails (thresholds not properly ordered). """ - await config_service.update_map_color_thresholds(db, body) - return await config_service.get_map_color_thresholds(db) + await config_service.update_map_color_thresholds(settings_ctx.db, body) + return await config_service.get_map_color_thresholds(settings_ctx.db) @router.get( diff --git a/backend/app/routers/dashboard.py b/backend/app/routers/dashboard.py index c8288e1..bed3a99 100644 --- a/backend/app/routers/dashboard.py +++ b/backend/app/routers/dashboard.py @@ -19,7 +19,7 @@ from fastapi import APIRouter, Query from app import __version__ from app.dependencies import ( AuthDep, - DbDep, + BanServiceContextDep, Fail2BanSocketDep, GeoCacheDep, HttpSessionDep, @@ -81,7 +81,7 @@ async def get_server_status( ) async def get_dashboard_bans( _auth: AuthDep, - db: DbDep, + ban_ctx: BanServiceContextDep, socket_path: Fail2BanSocketDep, http_session: HttpSessionDep, geo_cache: GeoCacheDep, @@ -107,6 +107,10 @@ async def get_dashboard_bans( Args: _auth: Validated session dependency. + ban_ctx: Ban service context containing db and repository. + socket_path: Path to fail2ban Unix domain socket. + http_session: Shared HTTP session for geolocation. + geo_cache: Geolocation cache instance. range: Time-range preset — ``"24h"``, ``"7d"``, ``"30d"``, or ``"365d"``. page: 1-based page number. @@ -124,7 +128,7 @@ async def get_dashboard_bans( page=page, page_size=page_size, http_session=http_session, - app_db=db, + app_db=ban_ctx.db, geo_cache=geo_cache, origin=origin, ) @@ -137,7 +141,7 @@ async def get_dashboard_bans( ) async def get_bans_by_country( _auth: AuthDep, - db: DbDep, + ban_ctx: BanServiceContextDep, socket_path: Fail2BanSocketDep, http_session: HttpSessionDep, geo_cache: GeoCacheDep, @@ -165,6 +169,10 @@ async def get_bans_by_country( Args: _auth: Validated session dependency. + ban_ctx: Ban service context containing db and repository. + socket_path: Path to fail2ban Unix domain socket. + http_session: Shared HTTP session for geolocation. + geo_cache: Geolocation cache instance. range: Time-range preset. origin: Optional filter by ban origin. @@ -179,7 +187,7 @@ async def get_bans_by_country( http_session=http_session, geo_cache_lookup=geo_cache.lookup_cached_only, geo_cache=geo_cache, - app_db=db, + app_db=ban_ctx.db, origin=origin, country_code=country_code, ) @@ -192,7 +200,7 @@ async def get_bans_by_country( ) async def get_ban_trend( _auth: AuthDep, - db: DbDep, + ban_ctx: BanServiceContextDep, socket_path: Fail2BanSocketDep, range: TimeRange = Query(default=_DEFAULT_RANGE, description="Time-range preset."), source: Literal["fail2ban", "archive"] = Query( @@ -220,6 +228,8 @@ async def get_ban_trend( Args: _auth: Validated session dependency. + ban_ctx: Ban service context containing db and repository. + socket_path: Path to fail2ban Unix domain socket. range: Time-range preset. origin: Optional filter by ban origin. @@ -231,7 +241,7 @@ async def get_ban_trend( socket_path, range, source=source, - app_db=db, + app_db=ban_ctx.db, origin=origin, ) @@ -243,7 +253,7 @@ async def get_ban_trend( ) async def get_bans_by_jail( _auth: AuthDep, - db: DbDep, + ban_ctx: BanServiceContextDep, socket_path: Fail2BanSocketDep, range: TimeRange = Query(default=_DEFAULT_RANGE, description="Time-range preset."), source: Literal["fail2ban", "archive"] = Query( @@ -263,6 +273,8 @@ async def get_bans_by_jail( Args: _auth: Validated session dependency. + ban_ctx: Ban service context containing db and repository. + socket_path: Path to fail2ban Unix domain socket. range: Time-range preset — ``"24h"``, ``"7d"``, ``"30d"``, or ``"365d"``. origin: Optional filter by ban origin. @@ -275,6 +287,6 @@ async def get_bans_by_jail( socket_path, range, source=source, - app_db=db, + app_db=ban_ctx.db, origin=origin, ) diff --git a/backend/app/routers/geo.py b/backend/app/routers/geo.py index fd483e2..0464363 100644 --- a/backend/app/routers/geo.py +++ b/backend/app/routers/geo.py @@ -17,7 +17,7 @@ from fastapi import APIRouter, Path from app.dependencies import ( AuthDep, - DbDep, + BanServiceContextDep, Fail2BanSocketDep, HttpSessionDep, ) @@ -83,7 +83,7 @@ async def lookup_ip( ) async def geo_stats( _auth: AuthDep, - db: DbDep, + ban_ctx: BanServiceContextDep, ) -> GeoCacheStatsResponse: """Return diagnostic counters for the geo cache subsystem. @@ -91,12 +91,12 @@ async def geo_stats( Args: _auth: Validated session — enforces authentication. - db: BanGUI application database connection. + 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(db) + stats: dict[str, int] = await geo_service.cache_stats(ban_ctx.db) return GeoCacheStatsResponse(**stats) @@ -107,7 +107,7 @@ async def geo_stats( ) async def re_resolve_geo( _auth: AuthDep, - db: DbDep, + ban_ctx: BanServiceContextDep, http_session: HttpSessionDep, ) -> GeoReResolveResponse: """Retry geo resolution for every IP in ``geo_cache`` with a null country. @@ -117,10 +117,10 @@ async def re_resolve_geo( Args: _auth: Validated session — enforces authentication. - db: BanGUI application database (for reading/writing ``geo_cache``). + 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(db, http_session) + return await geo_service.re_resolve_all(ban_ctx.db, http_session) diff --git a/backend/app/routers/history.py b/backend/app/routers/history.py index 553e722..f930578 100644 --- a/backend/app/routers/history.py +++ b/backend/app/routers/history.py @@ -21,10 +21,10 @@ from fastapi import APIRouter, HTTPException, Query, Request from app.dependencies import ( AuthDep, - DbDep, - Fail2BanSocketDep, - HttpSessionDep, Fail2BanMetadataServiceDep, + Fail2BanSocketDep, + HistoryServiceContextDep, + HttpSessionDep, ) from app.models.ban import BanOrigin, TimeRange from app.models.history import HistoryListResponse, IpDetailResponse @@ -42,7 +42,7 @@ router: APIRouter = APIRouter(prefix="/api/history", tags=["History"]) async def get_history( request: Request, _auth: AuthDep, - db: DbDep, + history_ctx: HistoryServiceContextDep, socket_path: Fail2BanSocketDep, http_session: HttpSessionDep, fail2ban_metadata_service: Fail2BanMetadataServiceDep, @@ -82,6 +82,10 @@ async def get_history( Args: request: The incoming request (used to access ``app.state``). _auth: Validated session — enforces authentication. + history_ctx: History service context containing db and repositories. + socket_path: Path to fail2ban Unix domain socket. + http_session: Shared HTTP session for geolocation. + fail2ban_metadata_service: Fail2Ban metadata service. range: Optional time-range preset. ``None`` means all-time. jail: Optional jail name filter (exact match). ip: Optional IP prefix filter (prefix match). @@ -103,7 +107,7 @@ async def get_history( page=page, page_size=page_size, http_session=http_session, - db=db, + db=history_ctx.db, fail2ban_metadata_service=fail2ban_metadata_service, ) @@ -116,7 +120,7 @@ async def get_history( async def get_history_archive( request: Request, _auth: AuthDep, - db: DbDep, + history_ctx: HistoryServiceContextDep, socket_path: Fail2BanSocketDep, http_session: HttpSessionDep, fail2ban_metadata_service: Fail2BanMetadataServiceDep, @@ -139,7 +143,7 @@ async def get_history_archive( page=page, page_size=page_size, http_session=http_session, - db=db, + db=history_ctx.db, fail2ban_metadata_service=fail2ban_metadata_service, ) diff --git a/backend/app/routers/jails.py b/backend/app/routers/jails.py index e82b859..e45a4c3 100644 --- a/backend/app/routers/jails.py +++ b/backend/app/routers/jails.py @@ -25,7 +25,7 @@ from fastapi import APIRouter, Body, HTTPException, Path, status from app.dependencies import ( AuthDep, - DbDep, + BanServiceContextDep, Fail2BanSocketDep, GeoCacheDep, HttpSessionDep, @@ -422,7 +422,7 @@ async def toggle_ignore_self( ) async def get_jail_banned_ips( _auth: AuthDep, - db: DbDep, + ban_ctx: BanServiceContextDep, name: _NamePath, socket_path: Fail2BanSocketDep, http_session: HttpSessionDep, @@ -439,7 +439,11 @@ async def get_jail_banned_ips( Args: _auth: Validated session — enforces authentication. + ban_ctx: Ban service context containing db and repository. name: Jail name. + socket_path: Path to fail2ban Unix domain socket. + http_session: Shared HTTP session for geolocation. + geo_cache: Geolocation cache instance. page: 1-based page number (default 1, min 1). page_size: Items per page (default 25, max 100). search: Optional case-insensitive substring filter on the IP address. @@ -471,5 +475,5 @@ async def get_jail_banned_ips( search=search, geo_cache=geo_cache, http_session=http_session, - app_db=db, + app_db=ban_ctx.db, ) diff --git a/backend/app/routers/setup.py b/backend/app/routers/setup.py index a7141e7..96ff0f4 100644 --- a/backend/app/routers/setup.py +++ b/backend/app/routers/setup.py @@ -10,7 +10,7 @@ from __future__ import annotations import structlog from fastapi import APIRouter, HTTPException, status -from app.dependencies import AppDep, DbDep, SettingsDep +from app.dependencies import AppDep, SettingsDep, SettingsServiceContextDep from app.models.setup import SetupRequest, SetupResponse, SetupStatusResponse, SetupTimezoneResponse from app.services import setup_service from app.utils.runtime_state import update_app_settings @@ -46,14 +46,14 @@ async def get_setup_status(app: AppDep) -> SetupStatusResponse: async def post_setup( app: AppDep, body: SetupRequest, - db: DbDep, + settings_ctx: SettingsServiceContextDep, ) -> SetupResponse: """Persist the initial BanGUI configuration. Args: app: The FastAPI application instance. body: Setup request payload validated by Pydantic. - db: Injected aiosqlite connection. + settings_ctx: Settings service context containing db and repository. Returns: :class:`~app.models.setup.SetupResponse` on success. @@ -61,14 +61,14 @@ async def post_setup( Raises: HTTPException: 409 if setup has already been completed. """ - if is_setup_complete_cached(app) or await setup_service.is_setup_complete(db): + if is_setup_complete_cached(app) or await setup_service.is_setup_complete(settings_ctx.db): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Setup has already been completed.", ) await setup_service.run_setup( - db, + settings_ctx.db, master_password=body.master_password, database_path=body.database_path, fail2ban_socket=body.fail2ban_socket,