fix: A21 phase 2c — 190 routes hors loop, 86 % total (v7.26.0)
FlowDeck CI / lint (push) Successful in 1m55s
FlowDeck CI / test (push) Successful in 15m23s
FlowDeck CI / docker (push) Canceled after 0s

4 passes (283 → 93 routes async sur 667 = 86 % hors loop, avant 61 %) :

A. RACINE AUTH — `get_current_user` (auth/session.py) était `async def`
   SANS aucun await (cookie decode = synchrone) ; idem ses clones :
   `agent._current_user_id/_workspace_id/_current_admin` (34 sites) et
   `sso._require_admin` (corps 0 await, 6 sites) → `def` +
   47 `await` supprimés. Piège : 3 call sites passaient par l'alias `gcu`
   (grep littéral aveugle) — 8 tests en échec → corrigés.

B. Re-scan : 19 routes devenues SANS await → `def` (agent 8, sso 5,
   web_clipper 3, projects 2, auth 1…).

C/D. 155 routes dont les seuls awaits = `request.json()` / événements :
   - try/except `body = {}` → `Body(default={})` (même tolérance)
   - try/except `raise HTTPException(400)` → `Body(...)` REQUIS
     (422 FastAPI — aucun test ne couvrait le 400)
   - forme conditionnelle `request.json() if content-type else {}`
     (54 sites) → défaut `{}` (sans corps = `{}` dans les 2 cas)
   - `await fire_*` → `run_event_sync(...)` ; imports `Body` /
     `run_event_sync` ajoutés aux routers convertis

Reste async (93, justifié) : form/upload/file (22), réseau gitea/llm/oidc,
`_json_body` (9), 2 JSON inline en argument, 1 fallback logique
(capture_frontend_error), 1 lecture conditionnelle (web_clipper), mixtes.

suite **1089/1089** · ruff OK · docs à jour
This commit is contained in:
2026-10-01 22:17:48 -04:00
parent 8d0d69e7b8
commit 45e59009c3
34 changed files with 2479 additions and 680 deletions
+14 -18
View File
@@ -17,7 +17,7 @@ import logging
import secrets
import time
from fastapi import APIRouter, HTTPException, Request
from fastapi import APIRouter, Body, HTTPException, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from app.auth.providers import oidc_provider, saml_provider
@@ -496,7 +496,7 @@ async def oidc_logout_post(request: Request, next: str = "/auth/login?provider=l
# ═══════════════════════ Admin configuration API ══════════════════════════
async def _require_admin(request: Request, *, write: bool) -> dict:
def _require_admin(request: Request, *, write: bool) -> dict:
"""Admin identity: Bearer token (scope read/write) or an admin session.
Session-authenticated writes also need the CSRF header — ``/api/v2`` is
@@ -558,22 +558,18 @@ def sso_providers(request: Request):
@router.get("/api/v2/sso/config")
async def get_sso_config_api(request: Request):
def get_sso_config_api(request: Request):
"""Read the current SSO configuration (secrets never returned)."""
await _require_admin(request, write=False)
_require_admin(request, write=False)
cfg = _sso_config_or_error()
return sso.public_config_view(cfg)
@router.post("/api/v2/sso/config")
@router.put("/api/v2/sso/config")
async def save_sso_config_api(request: Request):
def save_sso_config_api(request: Request, payload: dict = Body(...)):
"""Create/replace the SSO configuration (admin, scope write)."""
admin = await _require_admin(request, write=True)
try:
payload = await request.json()
except Exception as err:
raise HTTPException(status_code=400, detail="Invalid JSON body") from err
admin = _require_admin(request, write=True)
try:
saved = sso.save_sso_config(payload, created_by=admin.get("id"))
except sso.SSOConfigError as err:
@@ -586,9 +582,9 @@ async def save_sso_config_api(request: Request):
@router.delete("/api/v2/sso/config")
async def delete_sso_config_api(request: Request):
def delete_sso_config_api(request: Request):
"""Disable SSO — local logins keep working (design §8 « SSO disable »)."""
admin = await _require_admin(request, write=True)
admin = _require_admin(request, write=True)
removed = sso.delete_sso_config()
from app.services.api_v2_helpers import audit_log
@@ -597,9 +593,9 @@ async def delete_sso_config_api(request: Request):
@router.get("/api/v2/sso/workspaces")
async def sso_workspaces(request: Request):
def sso_workspaces(request: Request):
"""Workspaces available for default assignment / group mapping."""
await _require_admin(request, write=False)
_require_admin(request, write=False)
from app.db import get_conn
with get_conn() as conn:
@@ -616,9 +612,9 @@ async def sso_workspaces(request: Request):
@router.post("/api/v2/sso/sync")
async def sso_sync(request: Request):
def sso_sync(request: Request):
"""Re-apply group → workspace role mapping for every SSO user."""
admin = await _require_admin(request, write=True)
admin = _require_admin(request, write=True)
try:
result = sso.force_sync_all_groups()
except sso.SSOProvisioningError as err:
@@ -630,9 +626,9 @@ async def sso_sync(request: Request):
@router.get("/api/v2/sso/history")
async def sso_history(request: Request, limit: int = 50):
def sso_history(request: Request, limit: int = 50):
"""Audit trail of SSO login attempts (successes and rejections)."""
await _require_admin(request, write=False)
_require_admin(request, write=False)
from app.db import get_conn
limit = max(1, min(int(limit or 50), 200))