feat: implement API versioning /api/v1/

- All backend routers moved to /api/v1/ prefix
- Frontend BASE_URL updated to /api/v1
- Setup redirect middleware updated to redirect to /api/v1/setup
- Health router path fixed: prefix=/api/v1/health, @router.get('')
- conftest.py: set server_status=online for test fixture
- Created Docs/API_VERSIONING.md with deprecation policy
- Updated Docs/Backend-Development.md with versioning section
- Updated Instructions.md curl examples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-02 21:29:30 +02:00
parent 0d5882b32f
commit cc6dbcf3f0
51 changed files with 1886 additions and 671 deletions

View File

@@ -41,6 +41,7 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler # type: ignore[impo
from fastapi import Depends, FastAPI, HTTPException, Request, status
from app.config import Settings
from app.exceptions import RateLimitError
from app.models.auth import Session
from app.models.config import PendingRecovery
from app.models.server import ServerStatus
@@ -57,7 +58,7 @@ from app.repositories.protocols import (
from app.services.geo_cache import GeoCache
from app.services.protocols import Fail2BanMetadataService
from app.utils.constants import SESSION_COOKIE_NAME
from app.utils.rate_limiter import RateLimiter
from app.utils.rate_limiter import GlobalRateLimiter, RateLimiter
from app.utils.runtime_state import ApplicationState, JailServiceState, RuntimeState
from app.utils.session_cache import NoOpSessionCache, SessionCache
@@ -93,6 +94,7 @@ class ApplicationContext:
runtime_state: RuntimeState
session_cache: SessionCache | None
login_rate_limiter: RateLimiter
global_rate_limiter: GlobalRateLimiter
# ---------------------------------------------------------------------------
@@ -122,6 +124,10 @@ def _build_app_context(request: Request) -> ApplicationContext:
if login_rate_limiter is None:
login_rate_limiter = RateLimiter()
global_rate_limiter: GlobalRateLimiter = getattr(state, "global_rate_limiter", None)
if global_rate_limiter is None:
global_rate_limiter = GlobalRateLimiter()
return ApplicationContext(
settings=state.settings,
http_session=getattr(state, "http_session", None),
@@ -133,6 +139,7 @@ def _build_app_context(request: Request) -> ApplicationContext:
runtime_state=state.runtime_state,
session_cache=session_cache,
login_rate_limiter=login_rate_limiter,
global_rate_limiter=global_rate_limiter,
)
@@ -264,6 +271,62 @@ async def get_login_rate_limiter(
return app_context.login_rate_limiter
async def get_global_rate_limiter(
app_context: Annotated[ApplicationContext, Depends(get_app_context)],
) -> GlobalRateLimiter:
"""Provide the global rate limiter from application context."""
return app_context.global_rate_limiter
def rate_limit_dependency(
bucket: str,
max_requests: int,
window_seconds: int,
) -> Callable[[Request, "GlobalRateLimiter"], None]:
"""Create a rate limit dependency for a specific bucket and limit.
Use this factory to create per-endpoint rate limit dependencies.
Each call returns a configured dependency that enforces the
specified rate limit before the endpoint handler runs.
Args:
bucket: Bucket name (e.g., "bans:ban", "blocklist:import").
max_requests: Maximum requests allowed within the window.
window_seconds: Time window for this bucket.
Returns:
A callable that can be used as a FastAPI Depends() dependency.
"""
async def check_rate_limit(
request: Request,
rate_limiter: GlobalRateLimiterDep,
) -> None:
from app.utils.client_ip import get_client_ip
settings: Settings = request.app.state.settings
client_ip = get_client_ip(request, trusted_proxies=settings.trusted_proxies)
is_allowed, retry_after = rate_limiter.check_allowed_for_bucket(
bucket, client_ip, max_requests, window_seconds
)
if not is_allowed:
log.warning(
"operation_rate_limit_exceeded",
client_ip=client_ip,
bucket=bucket,
path=request.url.path,
method=request.method,
retry_after=retry_after,
)
raise RateLimitError(
f"Rate limit exceeded for {bucket}. Please try again later.",
retry_after_seconds=retry_after,
)
return check_rate_limit
async def get_session_repo() -> SessionRepository:
"""Provide the concrete session repository implementation.
@@ -668,6 +731,7 @@ AppStateDep = Annotated[ApplicationContext, Depends(get_app_state)]
AppDep = Annotated[FastAPI, Depends(get_app)]
AuthDep = Annotated[Session, Depends(require_auth)]
LoginRateLimiterDep = Annotated[RateLimiter, Depends(get_login_rate_limiter)]
GlobalRateLimiterDep = Annotated[GlobalRateLimiter, Depends(get_global_rate_limiter)]
Fail2BanMetadataServiceDep = Annotated[Fail2BanMetadataService, Depends(get_fail2ban_metadata_service)]
# Service context dependencies (db + repositories combined for routers)