refactoring-backend #3
@@ -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