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

@@ -3,7 +3,7 @@ from __future__ import annotations
import shlex
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 (
AppDep,
@@ -11,6 +11,7 @@ from app.dependencies import (
Fail2BanConfigDirDep,
Fail2BanSocketDep,
Fail2BanStartCommandDep,
GlobalRateLimiterDep,
HealthProbeDep,
PendingRecoveryDep,
)
@@ -37,6 +38,13 @@ from app.services import (
jail_config_service,
)
from app.utils.path_utils import validate_log_path
from app.utils.constants import (
RATE_LIMIT_JAIL_ACTIVATE_REQUESTS,
RATE_LIMIT_JAIL_CREATE_REQUESTS,
RATE_LIMIT_JAIL_DEACTIVATE_REQUESTS,
RATE_LIMIT_JAIL_DELETE_REQUESTS,
RATE_LIMIT_JAIL_UPDATE_REQUESTS,
)
from app.utils.runtime_state import (
clear_activation_record,
clear_pending_recovery,
@@ -45,6 +53,160 @@ from app.utils.runtime_state import (
router: APIRouter = APIRouter(prefix="/jails", tags=["Jail Config"])
_MINUTE = 60
_JAIL_UPDATE_BUCKET = "jail:update"
_JAIL_CREATE_BUCKET = "jail:create"
_JAIL_DELETE_BUCKET = "jail:delete"
_JAIL_ACTIVATE_BUCKET = "jail:activate"
_JAIL_DEACTIVATE_BUCKET = "jail:deactivate"
def _check_jail_update_rate_limit(
request: Request,
rate_limiter: GlobalRateLimiterDep,
) -> None:
"""Check rate limit for jail 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(
_JAIL_UPDATE_BUCKET, client_ip, RATE_LIMIT_JAIL_UPDATE_REQUESTS, _MINUTE
)
if not is_allowed:
from app.exceptions import RateLimitError
import structlog
log = structlog.get_logger()
log.warning(
"jail_update_rate_limit_exceeded",
client_ip=client_ip,
path=request.url.path,
retry_after=retry_after,
)
raise RateLimitError(
"Rate limit exceeded for jail update operations. Please try again later.",
retry_after_seconds=retry_after,
)
def _check_jail_create_rate_limit(
request: Request,
rate_limiter: GlobalRateLimiterDep,
) -> None:
"""Check rate limit for jail 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(
_JAIL_CREATE_BUCKET, client_ip, RATE_LIMIT_JAIL_CREATE_REQUESTS, _MINUTE
)
if not is_allowed:
from app.exceptions import RateLimitError
import structlog
log = structlog.get_logger()
log.warning(
"jail_create_rate_limit_exceeded",
client_ip=client_ip,
path=request.url.path,
retry_after=retry_after,
)
raise RateLimitError(
"Rate limit exceeded for jail create operations. Please try again later.",
retry_after_seconds=retry_after,
)
def _check_jail_delete_rate_limit(
request: Request,
rate_limiter: GlobalRateLimiterDep,
) -> None:
"""Check rate limit for jail 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(
_JAIL_DELETE_BUCKET, client_ip, RATE_LIMIT_JAIL_DELETE_REQUESTS, _MINUTE
)
if not is_allowed:
from app.exceptions import RateLimitError
import structlog
log = structlog.get_logger()
log.warning(
"jail_delete_rate_limit_exceeded",
client_ip=client_ip,
path=request.url.path,
retry_after=retry_after,
)
raise RateLimitError(
"Rate limit exceeded for jail delete operations. Please try again later.",
retry_after_seconds=retry_after,
)
def _check_jail_activate_rate_limit(
request: Request,
rate_limiter: GlobalRateLimiterDep,
) -> None:
"""Check rate limit for jail activate 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(
_JAIL_ACTIVATE_BUCKET, client_ip, RATE_LIMIT_JAIL_ACTIVATE_REQUESTS, _MINUTE
)
if not is_allowed:
from app.exceptions import RateLimitError
import structlog
log = structlog.get_logger()
log.warning(
"jail_activate_rate_limit_exceeded",
client_ip=client_ip,
path=request.url.path,
retry_after=retry_after,
)
raise RateLimitError(
"Rate limit exceeded for jail activate operations. Please try again later.",
retry_after_seconds=retry_after,
)
def _check_jail_deactivate_rate_limit(
request: Request,
rate_limiter: GlobalRateLimiterDep,
) -> None:
"""Check rate limit for jail deactivate 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(
_JAIL_DEACTIVATE_BUCKET, client_ip, RATE_LIMIT_JAIL_DEACTIVATE_REQUESTS, _MINUTE
)
if not is_allowed:
from app.exceptions import RateLimitError
import structlog
log = structlog.get_logger()
log.warning(
"jail_deactivate_rate_limit_exceeded",
client_ip=client_ip,
path=request.url.path,
retry_after=retry_after,
)
raise RateLimitError(
"Rate limit exceeded for jail deactivate operations. Please try again later.",
retry_after_seconds=retry_after,
)
_NamePath = Annotated[str, Path(description='Jail name as configured in fail2ban.')]
@router.get(
@@ -162,6 +324,7 @@ async def get_jail_config(
"/{name}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Update jail configuration",
dependencies=[Depends(_check_jail_update_rate_limit)],
)
async def update_jail_config(
request: Request,
@@ -201,6 +364,7 @@ async def update_jail_config(
"/{name}/logpath",
status_code=status.HTTP_204_NO_CONTENT,
summary="Add a log file path to an existing jail",
dependencies=[Depends(_check_jail_create_rate_limit)],
)
async def add_log_path(
request: Request,
@@ -235,6 +399,7 @@ async def add_log_path(
"/{name}/logpath",
status_code=status.HTTP_204_NO_CONTENT,
summary="Remove a monitored log path from a jail",
dependencies=[Depends(_check_jail_delete_rate_limit)],
)
async def delete_log_path(
request: Request,
@@ -274,6 +439,7 @@ async def delete_log_path(
"/{name}/activate",
response_model=JailActivationResponse,
summary="Activate an inactive jail",
dependencies=[Depends(_check_jail_activate_rate_limit)],
)
async def activate_jail(
app: AppDep,
@@ -327,6 +493,7 @@ async def activate_jail(
"/{name}/deactivate",
response_model=JailActivationResponse,
summary="Deactivate an active jail",
dependencies=[Depends(_check_jail_deactivate_rate_limit)],
)
async def deactivate_jail(
_auth: AuthDep,
@@ -486,6 +653,7 @@ async def rollback_jail(
"/{name}/filter",
status_code=status.HTTP_204_NO_CONTENT,
summary="Assign a filter to a jail",
dependencies=[Depends(_check_jail_create_rate_limit)],
)
async def assign_filter_to_jail(
request: Request,
@@ -520,6 +688,7 @@ async def assign_filter_to_jail(
"/{name}/action",
status_code=status.HTTP_204_NO_CONTENT,
summary="Add an action to a jail",
dependencies=[Depends(_check_jail_create_rate_limit)],
)
async def assign_action_to_jail(
request: Request,
@@ -555,6 +724,7 @@ async def assign_action_to_jail(
"/{name}/action/{action_name}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Remove an action from a jail",
dependencies=[Depends(_check_jail_delete_rate_limit)],
)
async def remove_action_from_jail(
request: Request,