Add admin panel, WebSocket support, and API versioning
CI / Lint & Format (push) Failing after 28s
CI / Tests (push) Has been skipped
CI / Security Scan (push) Failing after 8s
CI / Docker Build (push) Has been skipped

Introduce an admin portal (React + Nginx), WebSocket routing, and
API versioning middleware with `/api/v1/` prefix deprecation.
Add master API key authentication, new Prometheus metrics for AI
token consumption and active WebSockets, and extend S3 config
with a public endpoint URL. Update test paths and fixtures to
align with the new routing structure.
This commit is contained in:
2026-06-22 11:25:22 -04:00
parent d68deb9c74
commit 2f2a5e9b4a
158 changed files with 64811 additions and 503 deletions
+24 -16
View File
@@ -46,12 +46,20 @@ async def _upload_test_image(
)
# Le pipeline ARQ est mocké globalement dans conftest.py
response = await async_client.post(
"/images/upload",
files={"file": (filename, io.BytesIO(jpeg_bytes), "image/jpeg")},
headers=headers,
)
return response
import httpx
print(f"Uploading {filename}...")
try:
response = await async_client.post(
"/api/v1/images/upload",
files={"file": (filename, io.BytesIO(jpeg_bytes), "image/jpeg")},
headers=headers,
timeout=httpx.Timeout(5.0)
)
print(f"Uploaded {filename}: {response.status_code}")
return response
except Exception as e:
print(f"Error uploading {filename}: {e}")
raise
# ─────────────────────────────────────────────────────────────
@@ -71,14 +79,14 @@ async def test_client_a_image_invisible_to_client_b(
assert upload_resp.status_code == 201
# Liste pour A → contient l'image
response = await async_client.get("/images", headers=auth_headers_a)
response = await async_client.get("/api/v1/images", headers=auth_headers_a)
assert response.status_code == 200
data_a = response.json()
assert data_a["total"] == 1
assert data_a["items"][0]["original_name"] == "photo_a.jpg"
# Liste pour B → vide
response = await async_client.get("/images", headers=auth_headers_b)
response = await async_client.get("/api/v1/images", headers=auth_headers_b)
assert response.status_code == 200
data_b = response.json()
assert data_b["total"] == 0
@@ -103,7 +111,7 @@ async def test_client_b_cannot_read_client_a_image(
image_id = upload_resp.json()["id"]
# B essaie de lire l'image de A → 404
response = await async_client.get(f"/images/{image_id}", headers=auth_headers_b)
response = await async_client.get(f"/api/v1/images/{image_id}", headers=auth_headers_b)
assert response.status_code == 404
@@ -125,11 +133,11 @@ async def test_client_b_cannot_delete_client_a_image(
image_id = upload_resp.json()["id"]
# B essaie de supprimer → 404
response = await async_client.delete(f"/images/{image_id}", headers=auth_headers_b)
response = await async_client.delete(f"/api/v1/images/{image_id}", headers=auth_headers_b)
assert response.status_code == 404
# L'image existe toujours pour A
response = await async_client.get(f"/images/{image_id}", headers=auth_headers_a)
response = await async_client.get(f"/api/v1/images/{image_id}", headers=auth_headers_a)
assert response.status_code == 200
@@ -153,11 +161,11 @@ async def test_listing_returns_only_own_images(
await _upload_test_image(async_client, auth_headers_b, "b1.jpg")
# A voit 2 images
resp_a = await async_client.get("/images", headers=auth_headers_a)
resp_a = await async_client.get("/api/v1/images", headers=auth_headers_a)
assert resp_a.json()["total"] == 2
# B voit 1 image
resp_b = await async_client.get("/images", headers=auth_headers_b)
resp_b = await async_client.get("/api/v1/images", headers=auth_headers_b)
assert resp_b.json()["total"] == 1
assert resp_b.json()["items"][0]["original_name"] == "b1.jpg"
@@ -181,7 +189,7 @@ async def test_reprocess_other_client_image_returns_404(
# B essaie de reprocess → 404
response = await async_client.post(
f"/images/{image_id}/reprocess",
f"/api/v1/images/{image_id}/reprocess",
headers=auth_headers_b,
)
assert response.status_code == 404
@@ -203,7 +211,7 @@ async def test_sub_endpoints_other_client_returns_404(
assert upload_resp.status_code == 201
image_id = upload_resp.json()["id"]
for endpoint in [f"/images/{image_id}/status", f"/images/{image_id}/exif",
f"/images/{image_id}/ocr", f"/images/{image_id}/ai"]:
for endpoint in [f"/api/v1/images/{image_id}/status", f"/api/v1/images/{image_id}/exif",
f"/api/v1/images/{image_id}/ocr", f"/api/v1/images/{image_id}/ai"]:
response = await async_client.get(endpoint, headers=auth_headers_b)
assert response.status_code == 404, f"Endpoint {endpoint} should return 404 for other client"