fix: A21 phase 2c — 190 routes hors loop, 86 % total (v7.26.0)
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:
+19
-46
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
from datetime import UTC
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, Request
|
||||
from fastapi import APIRouter, Body, HTTPException, Query, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
|
||||
from app.auth.session import SessionManager
|
||||
@@ -720,8 +720,7 @@ def _require_user_id(request: Request) -> int:
|
||||
|
||||
|
||||
@router.put("/api/user/profile")
|
||||
async def update_profile(request: Request):
|
||||
body = await request.json()
|
||||
def update_profile(request: Request, body: dict = Body(default={})):
|
||||
full_name = body.get("full_name", "").strip()
|
||||
uid = _require_user_id(request)
|
||||
with get_conn() as conn:
|
||||
@@ -731,9 +730,8 @@ async def update_profile(request: Request):
|
||||
|
||||
|
||||
@router.put("/api/user/password")
|
||||
async def update_password(request: Request):
|
||||
def update_password(request: Request, body: dict = Body(default={})):
|
||||
from app.password_utils import hash_password, verify_password
|
||||
body = await request.json()
|
||||
password = body.get("password", "").strip()
|
||||
if len(password) < 6:
|
||||
return {"error": "Password must be at least 6 characters"}
|
||||
@@ -972,8 +970,7 @@ async def list_workspace_projects(request: Request):
|
||||
|
||||
|
||||
@router.post("/api/workspace/projects")
|
||||
async def create_workspace_project(request: Request):
|
||||
body = await request.json()
|
||||
def create_workspace_project(request: Request, body: dict = Body(default={})):
|
||||
name = body.get("name", "").strip()
|
||||
if not name:
|
||||
return {"error": "Name required"}
|
||||
@@ -1002,9 +999,8 @@ def list_members(request: Request, ws_id: int):
|
||||
|
||||
|
||||
@router.post("/api/workspace/{ws_id:int}/members")
|
||||
async def invite_member(request: Request, ws_id: int):
|
||||
def invite_member(request: Request, ws_id: int, body: dict = Body(default={})):
|
||||
"""Invite a user to a workspace by email."""
|
||||
body = await request.json()
|
||||
email = body.get("email", "").strip()
|
||||
role = body.get("role", "editor")
|
||||
if role not in ("owner", "admin", "editor", "viewer"):
|
||||
@@ -1025,9 +1021,8 @@ async def invite_member(request: Request, ws_id: int):
|
||||
|
||||
|
||||
@router.put("/api/workspace/{ws_id:int}/members/{user_id:int}")
|
||||
async def update_member_role(request: Request, ws_id: int, user_id: int):
|
||||
def update_member_role(request: Request, ws_id: int, user_id: int, body: dict = Body(default={})):
|
||||
"""Change a member's role."""
|
||||
body = await request.json()
|
||||
role = body.get("role", "editor")
|
||||
if role not in ("owner", "admin", "editor", "viewer"):
|
||||
return {"error": "Invalid role"}
|
||||
@@ -1468,9 +1463,8 @@ def nav_menu(request: Request, workspace_id: int = None, parent_id: int = None):
|
||||
|
||||
|
||||
@router.post("/api/local-workspace/items")
|
||||
async def create_local_workspace_item(request: Request):
|
||||
def create_local_workspace_item(request: Request, body: dict = Body(default={})):
|
||||
"""Create a new file in the active workspace."""
|
||||
body = await request.json()
|
||||
name = (body.get("name") or "").strip() or "Untitled"
|
||||
item_type = body.get("type", "page")
|
||||
parent_id = body.get("parent_id")
|
||||
@@ -1491,9 +1485,8 @@ async def create_local_workspace_item(request: Request):
|
||||
|
||||
|
||||
@router.put("/api/local-workspace/items/{item_id:int}")
|
||||
async def rename_local_workspace_item(request: Request, item_id: int):
|
||||
def rename_local_workspace_item(request: Request, item_id: int, body: dict = Body(default={})):
|
||||
"""Rename a file."""
|
||||
body = await request.json()
|
||||
name = body.get("name", "Untitled").strip()
|
||||
with get_conn() as conn:
|
||||
conn.execute("UPDATE pages SET title=? WHERE id=?", (name, item_id))
|
||||
@@ -1547,9 +1540,8 @@ def serve_uploaded_file(ws_id: int, filename: str):
|
||||
|
||||
|
||||
@router.put("/api/local-workspace/items/{item_id:int}/move")
|
||||
async def move_local_workspace_item(request: Request, item_id: int):
|
||||
def move_local_workspace_item(request: Request, item_id: int, body: dict = Body(default={})):
|
||||
"""Move an item to a new parent (drag & drop)."""
|
||||
body = await request.json()
|
||||
new_parent_id = body.get("parent_id") # None = move to root
|
||||
with get_conn() as conn:
|
||||
conn.execute(
|
||||
@@ -1829,9 +1821,8 @@ def list_workspaces(request: Request):
|
||||
|
||||
|
||||
@router.post("/api/workspaces")
|
||||
async def create_workspace(request: Request):
|
||||
def create_workspace(request: Request, body: dict = Body(default={})):
|
||||
"""Create a new workspace."""
|
||||
body = await request.json()
|
||||
name = body.get("name", "New Workspace").strip()
|
||||
if not name:
|
||||
return {"error": "Name required"}
|
||||
@@ -1861,9 +1852,8 @@ async def create_workspace(request: Request):
|
||||
|
||||
|
||||
@router.put("/api/workspaces/{ws_id:int}")
|
||||
async def rename_workspace(request: Request, ws_id: int):
|
||||
def rename_workspace(request: Request, ws_id: int, body: dict = Body(default={})):
|
||||
"""Rename a workspace."""
|
||||
body = await request.json()
|
||||
name = body.get("name", "").strip()
|
||||
if not name:
|
||||
return {"error": "Name required"}
|
||||
@@ -1971,9 +1961,8 @@ def get_avatar(user_id: int):
|
||||
|
||||
|
||||
@router.post("/api/settings/avatar-color")
|
||||
async def set_avatar_color(request: Request):
|
||||
def set_avatar_color(request: Request, body: dict = Body(default={})):
|
||||
"""Set the user's avatar background color."""
|
||||
body = await request.json()
|
||||
color = body.get("color", "#3A3A3A")
|
||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||
if not user:
|
||||
@@ -1987,9 +1976,8 @@ async def set_avatar_color(request: Request):
|
||||
# ═══════════ Tag Management API (per-user) ═══════════
|
||||
|
||||
@router.post("/api/settings/tags")
|
||||
async def create_tag_global(request: Request):
|
||||
def create_tag_global(request: Request, body: dict = Body(default={})):
|
||||
"""Create a tag for the current user."""
|
||||
body = await request.json()
|
||||
tag_name = body.get("name", "").strip().lower()
|
||||
color = body.get("color", "#787774")
|
||||
uid = _get_user_id(request)
|
||||
@@ -2007,9 +1995,8 @@ async def create_tag_global(request: Request):
|
||||
|
||||
|
||||
@router.put("/api/settings/tags/{tag_id:int}")
|
||||
async def update_tag_global(tag_id: int, request: Request):
|
||||
def update_tag_global(tag_id: int, request: Request, body: dict = Body(default={})):
|
||||
"""Update a tag (name or color) — only if owned by user."""
|
||||
body = await request.json()
|
||||
uid = _get_user_id(request)
|
||||
with get_conn() as conn:
|
||||
if "name" in body:
|
||||
@@ -2087,9 +2074,8 @@ def get_item_tags(item_id: int):
|
||||
|
||||
|
||||
@router.post("/api/local-workspace/items/{item_id:int}/tags")
|
||||
async def add_item_tag(request: Request, item_id: int):
|
||||
def add_item_tag(request: Request, item_id: int, body: dict = Body(default={})):
|
||||
"""Add a tag to an item (creates tag if new, scoped to user)."""
|
||||
body = await request.json()
|
||||
tag_name = body.get("name", "").strip().lower()
|
||||
tag_color = body.get("color", "#787774")
|
||||
uid = _get_user_id(request)
|
||||
@@ -2175,16 +2161,12 @@ def search_by_tags(request: Request, tags: str = ""):
|
||||
|
||||
# ── Account update ──
|
||||
@router.put("/api/settings/account")
|
||||
async def update_account(request: Request):
|
||||
def update_account(request: Request, body: dict = Body(default={})):
|
||||
"""Update current user's profile: full_name, login, email, password."""
|
||||
from app.password_utils import hash_password
|
||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||
if not user:
|
||||
return JSONResponse({"error": "Not authenticated"}, status_code=401)
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
with get_conn() as conn:
|
||||
uid = user["id"]
|
||||
if "full_name" in body:
|
||||
@@ -2563,9 +2545,8 @@ def api_page_content(page_id: int):
|
||||
|
||||
|
||||
@router.put("/api/pages/{page_id:int}/rename")
|
||||
async def api_rename_page(page_id: int, request: Request):
|
||||
def api_rename_page(page_id: int, request: Request, body: dict = Body(default={})):
|
||||
"""Inline rename a page title."""
|
||||
body = await request.json()
|
||||
title = (body.get("title") or "").strip()
|
||||
if not title:
|
||||
return JSONResponse({"error": "Title required"}, status_code=400)
|
||||
@@ -2594,17 +2575,13 @@ def api_trash_page(page_id: int):
|
||||
|
||||
|
||||
@router.post("/api/pages/{page_id:int}/convert-to-database")
|
||||
async def api_convert_to_database(page_id: int, request: Request):
|
||||
def api_convert_to_database(page_id: int, request: Request, body: dict = Body(default={})):
|
||||
"""Convert a page into a full-page database (Notion-style).
|
||||
|
||||
Creates a collection linked to this page, adds the default 'Name' property,
|
||||
and sets the page's content_format to 'collection'.
|
||||
"""
|
||||
import json as _json
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
db_name = (body.get("name") or "").strip()
|
||||
|
||||
with get_conn() as conn:
|
||||
@@ -2699,13 +2676,9 @@ def api_collection_table_data(collection_id: int):
|
||||
|
||||
|
||||
@router.post("/api/collections/{collection_id:int}/pages")
|
||||
async def api_create_collection_page(collection_id: int, request: Request):
|
||||
def api_create_collection_page(collection_id: int, request: Request, body: dict = Body(default={})):
|
||||
"""Create a new page (row) in a collection."""
|
||||
import json as _json
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
title = body.get("title", "New page").strip() or "New page"
|
||||
icon = body.get("icon", "file")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user