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

@@ -26,14 +26,14 @@ _SETUP_PAYLOAD = {
async def _do_setup(client: AsyncClient) -> None:
"""Run the setup wizard so auth endpoints are reachable."""
resp = await client.post("/api/setup", json=_SETUP_PAYLOAD)
resp = await client.post("/api/v1/setup", json=_SETUP_PAYLOAD)
assert resp.status_code == 201
async def _login(client: AsyncClient, password: str = "Mysecretpass1!") -> str:
"""Helper: perform login and return the session token."""
resp = await client.post(
"/api/auth/login",
"/api/v1/auth/login",
json={"password": password},
headers={"X-BanGUI-Request": "1"},
)
@@ -58,7 +58,7 @@ class TestCsrfProtection:
# POST with correct CSRF header should succeed (endpoint may fail for other reasons)
response = await client.post(
"/api/auth/logout",
"/api/v1/auth/logout",
cookies={SESSION_COOKIE_NAME: token},
headers={"X-BanGUI-Request": "1"},
)
@@ -74,7 +74,7 @@ class TestCsrfProtection:
# POST without CSRF header should be rejected
response = await client.post(
"/api/auth/logout",
"/api/v1/auth/logout",
cookies={SESSION_COOKIE_NAME: token},
headers={}, # Explicitly omit X-BanGUI-Request
)
@@ -92,7 +92,7 @@ class TestCsrfProtection:
# POST with wrong CSRF header value should be rejected
response = await client.post(
"/api/auth/logout",
"/api/v1/auth/logout",
cookies={SESSION_COOKIE_NAME: token},
headers={"X-BanGUI-Request": "invalid"},
)
@@ -107,7 +107,7 @@ class TestCsrfProtection:
# POST with Bearer token but no CSRF header should succeed
response = await client.post(
"/api/auth/logout",
"/api/v1/auth/logout",
headers={"Authorization": f"Bearer {token}"},
)
# Expect 200 (logout succeeds) not 403 (CSRF check should be skipped)
@@ -122,7 +122,7 @@ class TestCsrfProtection:
# GET without CSRF header should succeed (safe method)
response = await client.get(
"/api/auth/session",
"/api/v1/auth/session",
cookies={SESSION_COOKIE_NAME: token},
headers={}, # Explicitly omit X-BanGUI-Request
)
@@ -138,7 +138,7 @@ class TestCsrfProtection:
# OPTIONS without CSRF header should succeed (safe method)
response = await client.options(
"/api/auth/session",
"/api/v1/auth/session",
cookies={SESSION_COOKIE_NAME: token},
headers={},
)
@@ -154,7 +154,7 @@ class TestCsrfProtection:
# HEAD without CSRF header should succeed (safe method)
response = await client.head(
"/api/auth/session",
"/api/v1/auth/session",
cookies={SESSION_COOKIE_NAME: token},
headers={},
)
@@ -172,7 +172,7 @@ class TestCsrfProtection:
# The endpoint may fail for other reasons (no ban to delete), but not 403 CSRF
response = await client.request(
"DELETE",
"/api/bans",
"/api/v1/bans",
content='{"ip": "192.0.2.1", "jail": "sshd"}',
cookies={SESSION_COOKIE_NAME: token},
headers={"X-BanGUI-Request": "1"},
@@ -190,7 +190,7 @@ class TestCsrfProtection:
# DELETE without CSRF header should be rejected
response = await client.request(
"DELETE",
"/api/bans",
"/api/v1/bans",
content='{"ip": "192.0.2.1", "jail": "sshd"}',
cookies={SESSION_COOKIE_NAME: token},
headers={},
@@ -206,7 +206,7 @@ class TestCsrfProtection:
# PUT with correct CSRF header should not be rejected by CSRF middleware
response = await client.put(
"/api/blocklists/schedule",
"/api/v1/blocklists/schedule",
json={"enabled": False},
cookies={SESSION_COOKIE_NAME: token},
headers={"X-BanGUI-Request": "1"},
@@ -223,7 +223,7 @@ class TestCsrfProtection:
# PUT without CSRF header should be rejected
response = await client.put(
"/api/blocklists/schedule",
"/api/v1/blocklists/schedule",
json={"enabled": False},
cookies={SESSION_COOKIE_NAME: token},
headers={},
@@ -240,7 +240,7 @@ class TestCsrfProtection:
# PATCH with correct CSRF header should not be rejected by CSRF middleware
# (endpoint may not exist, but CSRF check should pass)
response = await client.patch(
"/api/auth/logout",
"/api/v1/auth/logout",
cookies={SESSION_COOKIE_NAME: token},
headers={"X-BanGUI-Request": "1"},
)
@@ -256,7 +256,7 @@ class TestCsrfProtection:
# PATCH without CSRF header should be rejected
response = await client.patch(
"/api/auth/logout",
"/api/v1/auth/logout",
cookies={SESSION_COOKIE_NAME: token},
headers={},
)
@@ -271,7 +271,7 @@ class TestCsrfProtection:
# POST without any authentication should bypass CSRF check
# (the endpoint itself will reject it with 401, not 403)
response = await client.post(
"/api/auth/logout",
"/api/v1/auth/logout",
headers={},
)
# Should be 401 (auth required) not 403 (CSRF failed)
@@ -289,7 +289,7 @@ class TestCsrfProtection:
# POST with Bearer token via Authorization header and no CSRF header
# should NOT be rejected by CSRF middleware
response = await client.post(
"/api/auth/logout",
"/api/v1/auth/logout",
headers={"Authorization": f"Bearer {token}"},
)
# Should succeed (200) not fail with 403