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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user