feat: v6.3.0 API publique complete v2 (REST /api/v2, scopes, OpenAPI)
FlowDeck CI / lint (push) Successful in 1m13s
FlowDeck CI / test (push) Successful in 9m20s
FlowDeck CI / docker (push) Successful in 1m10s

- Router api_v2.py (~100 endpoints) : tokens, users, workspaces/members,
  collections, pages, proprietes, vues/dashboards, commentaires/mentions,
  notifications, favoris/tags/recents, partage/publish, historique, sprints,
  templates, export/import, forges, recherche FTS, admin, webhooks CRUD
- Helpers api_v2_helpers.py : Bearer unifie (sha256/expires_at/extension_devices),
  scopes hierarchiques read<write<admin, pagination + X-Total-Count, ISO-8601,
  RFC 7807, idempotence, audit, rate-limit par token
- Migration 20 : api_tokens.scopes/expires_at, webhook_deliveries,
  api_audit_log, idempotency_keys
- main.py : handler d'erreurs unifie StarletteHTTPException, /docs + /redoc
- config : PUBLIC_API_INSECURE_OK (dev only), API_V2_RATE_LIMIT_PER_TOKEN
- OpenAPI docs/openapi-v2.json (402 chemins), tests/test_public_api_v2.py (24)
- Docs : CHANGELOG (v6.2.0/6.2.1 clipper + v6.3.0), ROADMAP, API_GUIDE_V6,
  V6_Web_Clipper, README, ARCHITECTURE, /help
- Suite complete 668 verte, ruff OK
This commit is contained in:
2026-09-20 13:19:29 -04:00
parent ea19d1d050
commit 95bc861cdb
25 changed files with 25078 additions and 66 deletions
+30 -12
View File
@@ -8,6 +8,7 @@ from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from starlette.exceptions import HTTPException as _StarHTTPException
from starlette.middleware.sessions import SessionMiddleware
from app.config import settings
@@ -37,6 +38,7 @@ from app.routers import (
webhooks,
workspace,
)
from app.routers.api_v2 import router as api_v2_router
from app.routers.automations import router as automations_router
from app.routers.collaboration import router as collaboration_router
from app.routers.emoji import router as emoji_router
@@ -111,9 +113,9 @@ async def lifespan(_app: FastAPI):
app = FastAPI(
title="FlowDeck",
version="6.2.0",
docs_url="/docs" if settings.log_level == "DEBUG" else None,
redoc_url=None,
version="6.3.0",
docs_url="/docs",
redoc_url="/redoc",
lifespan=lifespan,
)
@@ -157,6 +159,7 @@ app.include_router(import_page_router)
app.include_router(permissions_router)
app.include_router(web_clipper_api_router)
app.include_router(web_clipper_router)
app.include_router(api_v2_router)
app.mount("/static", StaticFiles(directory="static"), name="static")
@@ -237,12 +240,27 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;backgrou
</html>"""
@app.exception_handler(404)
async def not_found_handler(request: Request, exc):
"""Redirect 404 HTML pages to /workspaces. API routes still get JSON."""
# Preserve JSON 404 for all API-like paths (including /db/xxx/api)
if "/api" in request.url.path:
from fastapi.responses import JSONResponse
return JSONResponse({"detail": "Not found"}, status_code=404)
from fastapi.responses import RedirectResponse
return RedirectResponse("/workspaces", status_code=302)
@app.exception_handler(_StarHTTPException)
async def http_exception_handler(request: Request, exc: _StarHTTPException):
"""Unified handler: RFC7807 for /api/v2, JSON for other /api, redirect for HTML.
Registered on Starlette's HTTPException (the base class) so it catches both
raised exceptions and route-miss 404s.
"""
status = getattr(exc, "status_code", 500)
detail = getattr(exc, "detail", str(exc))
if status == 404:
if request.url.path.startswith("/api/v2"):
from app.services.api_v2_helpers import problem_response
return problem_response(request, exc)
if "/api" in request.url.path:
from fastapi.responses import JSONResponse
return JSONResponse({"detail": detail if isinstance(detail, str) else "Not found"}, status_code=404)
from fastapi.responses import RedirectResponse
return RedirectResponse("/workspaces", status_code=302)
# Non-404: RFC7807 for /api/v2
if request.url.path.startswith("/api/v2"):
from app.services.api_v2_helpers import problem_response
return problem_response(request, exc)
from fastapi.responses import JSONResponse
return JSONResponse({"detail": detail if isinstance(detail, str) else str(detail)}, status_code=status)