refactoring-backend #3

Merged
lukas.pupkalipinski merged 403 commits from refactoring-backend into main 2026-05-20 20:23:46 +02:00
3 changed files with 68 additions and 25 deletions
Showing only changes of commit 813cf09bed - Show all commits

View File

@@ -247,6 +247,51 @@ async def list_jails(service: JailService = Depends()) -> JailListResponse:
return JailListResponse(jails=jails)
```
### Dependency Layering: Enforcing the Repository Boundary
The **repository boundary** separates database-aware code from application logic. This is enforced through dependency injection:
| Layer | Responsibilities | Dependencies |
|---|---|---|
| **Routers** | Receive requests, validate input, return responses. | Repository dependencies (SessionRepoDep, BlocklistRepositoryDep), 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.
**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.
- **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,
_auth: AuthDep,
) -> BlocklistListResponse:
sources = await blocklist_repo.list_sources(???) # db comes from where?
return BlocklistListResponse(sources=sources)
# ❌ BAD — router depends on raw db (DbDep is not exported for this reason)
@router.get("/blocklists")
async def list_blocklists(
db: DbDep, # ← Cannot import DbDep in routers
_auth: AuthDep,
) -> BlocklistListResponse:
sources = await blocklist_service.list_sources(db)
return BlocklistListResponse(sources=sources)
```
**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.
---
## 5. Pydantic Models

View File

@@ -1,25 +1,3 @@
## 5) Inconsistent domain exception contracts across services
- Where found:
- [backend/app/routers/jails.py](backend/app/routers/jails.py)
- [backend/app/routers/config.py](backend/app/routers/config.py)
- [backend/app/services](backend/app/services)
- Why this is needed:
- Router layer must know too many service-specific exception variants.
- Goal:
- Standardize domain exception taxonomy and HTTP mapping.
- What to do:
- Define error categories and mandatory service error types.
- Align router exception handlers to those categories.
- Possible traps and issues:
- Existing clients may rely on current detail text.
- Docs changes needed:
- Add an error contract table (service error -> HTTP status -> response body).
- Doc references:
- [backend/app/main.py](backend/app/main.py)
- [Docs/Backend-Development.md](Docs/Backend-Development.md)
---
## 6) Raw DB connection exposed as dependency for all routes
- Where found:
- [backend/app/dependencies.py](backend/app/dependencies.py)

View File

@@ -4,6 +4,11 @@ All ``Depends()`` callables that inject shared resources (database
connection, settings, services, auth guard) are defined here.
Routers import directly from this module — never from ``app.state``
directly — to keep coupling explicit and testable.
IMPORTANT: Routers should depend on repository dependencies (e.g., SessionRepoDep,
BlocklistRepositoryDep) rather than on database connections. This enforces the
repository boundary: only repositories and services access the database directly.
See Backend-Development.md § 6 for the dependency layering rules.
"""
import datetime
@@ -369,9 +374,14 @@ async def get_fail2ban_metadata_service() -> object:
return default_fail2ban_metadata_service
# 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)]
async def require_auth(
request: Request,
db: Annotated[aiosqlite.Connection, Depends(get_db)],
db: _DbDep,
settings: Annotated[Settings, Depends(get_settings)],
session_cache: Annotated[SessionCache, Depends(get_session_cache)],
session_repo: Annotated[SessionRepository, Depends(get_session_repo)],
@@ -390,8 +400,10 @@ async def require_auth(
Args:
request: The incoming FastAPI request.
db: Injected aiosqlite connection.
db: Injected aiosqlite connection (for repository operations).
settings: Application settings used for signed session token validation.
session_cache: Session validation cache backend.
session_repo: Session repository for persistence operations.
Returns:
The active :class:`~app.models.auth.Session`.
@@ -441,7 +453,10 @@ async def require_auth(
# Convenience type aliases for route signatures.
DbDep = Annotated[aiosqlite.Connection, Depends(get_db)]
# NOTE: Database connections are NOT exported to routers. Routers should depend on
# repository dependencies (SessionRepoDep, BlocklistRepositoryDep, etc.) instead.
# See Backend-Development.md for the dependency layering rules.
SettingsDep = Annotated[Settings, Depends(get_settings)]
HttpSessionDep = Annotated[aiohttp.ClientSession, Depends(get_http_session)]
SchedulerDep = Annotated[AsyncIOScheduler, Depends(get_scheduler)]
@@ -466,3 +481,8 @@ AppDep = Annotated[FastAPI, Depends(get_app)]
AuthDep = Annotated[Session, Depends(require_auth)]
LoginRateLimiterDep = Annotated[RateLimiter, Depends(get_login_rate_limiter)]
Fail2BanMetadataServiceDep = Annotated[Fail2BanMetadataService, Depends(get_fail2ban_metadata_service)]
# 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.
DbDep = _DbDep