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

@@ -2,9 +2,9 @@ from __future__ import annotations
from typing import Annotated
from fastapi import APIRouter, Path, Query, Request, status
from fastapi import APIRouter, Depends, Path, Query, Request, status
from app.dependencies import AuthDep, Fail2BanConfigDirDep, Fail2BanSocketDep
from app.dependencies import AuthDep, Fail2BanConfigDirDep, Fail2BanSocketDep, GlobalRateLimiterDep
from app.models.config import (
ActionConfig,
ActionCreateRequest,
@@ -12,9 +12,108 @@ from app.models.config import (
ActionUpdateRequest,
)
from app.services import action_config_service
from app.utils.constants import (
RATE_LIMIT_ACTION_CREATE_REQUESTS,
RATE_LIMIT_ACTION_DELETE_REQUESTS,
RATE_LIMIT_ACTION_UPDATE_REQUESTS,
)
router: APIRouter = APIRouter(prefix="/actions", tags=["Action Config"])
_MINUTE = 60
_ACTION_UPDATE_BUCKET = "action:update"
_ACTION_CREATE_BUCKET = "action:create"
_ACTION_DELETE_BUCKET = "action:delete"
def _check_action_update_rate_limit(
request: Request,
rate_limiter: GlobalRateLimiterDep,
) -> None:
"""Check rate limit for action update operations."""
from app.utils.client_ip import get_client_ip
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(
_ACTION_UPDATE_BUCKET, client_ip, RATE_LIMIT_ACTION_UPDATE_REQUESTS, _MINUTE
)
if not is_allowed:
from app.exceptions import RateLimitError
import structlog
log = structlog.get_logger()
log.warning(
"action_update_rate_limit_exceeded",
client_ip=client_ip,
path=request.url.path,
retry_after=retry_after,
)
raise RateLimitError(
"Rate limit exceeded for action update operations. Please try again later.",
retry_after_seconds=retry_after,
)
def _check_action_create_rate_limit(
request: Request,
rate_limiter: GlobalRateLimiterDep,
) -> None:
"""Check rate limit for action create operations."""
from app.utils.client_ip import get_client_ip
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(
_ACTION_CREATE_BUCKET, client_ip, RATE_LIMIT_ACTION_CREATE_REQUESTS, _MINUTE
)
if not is_allowed:
from app.exceptions import RateLimitError
import structlog
log = structlog.get_logger()
log.warning(
"action_create_rate_limit_exceeded",
client_ip=client_ip,
path=request.url.path,
retry_after=retry_after,
)
raise RateLimitError(
"Rate limit exceeded for action create operations. Please try again later.",
retry_after_seconds=retry_after,
)
def _check_action_delete_rate_limit(
request: Request,
rate_limiter: GlobalRateLimiterDep,
) -> None:
"""Check rate limit for action delete operations."""
from app.utils.client_ip import get_client_ip
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(
_ACTION_DELETE_BUCKET, client_ip, RATE_LIMIT_ACTION_DELETE_REQUESTS, _MINUTE
)
if not is_allowed:
from app.exceptions import RateLimitError
import structlog
log = structlog.get_logger()
log.warning(
"action_delete_rate_limit_exceeded",
client_ip=client_ip,
path=request.url.path,
retry_after=retry_after,
)
raise RateLimitError(
"Rate limit exceeded for action delete operations. Please try again later.",
retry_after_seconds=retry_after,
)
_ActionNamePath = Annotated[
str,
Path(description='Action base name, e.g. ``iptables`` or ``iptables.conf``.'),
@@ -105,6 +204,7 @@ async def get_action(
"/{name}",
response_model=ActionConfig,
summary="Update an action's .local override with new lifecycle command values",
dependencies=[Depends(_check_action_update_rate_limit)],
)
async def update_action(
request: Request,
@@ -145,6 +245,7 @@ async def update_action(
response_model=ActionConfig,
status_code=status.HTTP_201_CREATED,
summary="Create a new user-defined action",
dependencies=[Depends(_check_action_create_rate_limit)],
)
async def create_action(
request: Request,
@@ -182,6 +283,7 @@ async def create_action(
"/{name}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete a user-created action's .local file",
dependencies=[Depends(_check_action_delete_rate_limit)],
)
async def delete_action(
request: Request,