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>
This commit is contained in:
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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.")
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user