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:
+45
-83
@@ -6,7 +6,7 @@ import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, Request
|
||||
from fastapi import APIRouter, Body, HTTPException, Query, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
|
||||
from app.auth.session import SessionManager
|
||||
@@ -14,7 +14,7 @@ from app.config import settings
|
||||
from app.db import get_conn
|
||||
from app.routers.dashboard import _get_app_version
|
||||
from app.routers.sidebar_config import get_sidebar_config_sync
|
||||
from app.services.automations import fire_event
|
||||
from app.services.automations import fire_event, run_event_sync
|
||||
from app.services.gitea_client import gitea
|
||||
from app.services.permission_manager import PermissionManager
|
||||
from app.services.publish import fire_published, fire_unpublished, publish, unpublish
|
||||
@@ -124,16 +124,12 @@ def wiki_titles(request: Request, ids: str = Query(default="")):
|
||||
|
||||
|
||||
@router.post("/api/pages/{page_id}/lock")
|
||||
async def set_page_lock(request: Request, page_id: int):
|
||||
def set_page_lock(request: Request, page_id: int, body: dict = Body(default={})):
|
||||
"""v5.12.0: lock/unlock a page (read-only for everyone except the locker,
|
||||
admins and the page creator)."""
|
||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||
if not user or not user.get("id"):
|
||||
raise HTTPException(401, "Authentication required")
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
locked = bool(body.get("locked"))
|
||||
with get_conn() as conn:
|
||||
row = conn.execute("SELECT id, is_locked, locked_by, workspace FROM pages WHERE id=?",
|
||||
@@ -146,21 +142,17 @@ async def set_page_lock(request: Request, page_id: int):
|
||||
conn.execute("UPDATE pages SET is_locked=?, locked_by=? WHERE id=?",
|
||||
(1 if locked else 0, user["id"] if locked else None, page_id))
|
||||
conn.commit()
|
||||
await fire_event("page.locked" if locked else "page.unlocked",
|
||||
{"page_id": page_id, "by": user["id"]})
|
||||
run_event_sync(fire_event("page.locked" if locked else "page.unlocked",
|
||||
{"page_id": page_id, "by": user["id"]}))
|
||||
return {"status": "ok", "is_locked": int(locked)}
|
||||
|
||||
|
||||
@router.post("/api/pages/{page_id}/options")
|
||||
async def set_page_options(request: Request, page_id: int):
|
||||
def set_page_options(request: Request, page_id: int, body: dict = Body(default={})):
|
||||
"""v5.12.0: page layout options — full-width and compact typography."""
|
||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||
if not user or not user.get("id"):
|
||||
raise HTTPException(401, "Authentication required")
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
updates = {}
|
||||
for key in ("full_width", "font_small"):
|
||||
if key in body:
|
||||
@@ -199,16 +191,12 @@ def list_page_templates_api(request: Request):
|
||||
|
||||
|
||||
@router.post("/api/page-templates")
|
||||
async def create_page_template(request: Request):
|
||||
def create_page_template(request: Request, body: dict = Body(default={})):
|
||||
"""v5.12.0: save the current page (or a raw block list) as a personal
|
||||
global template: {name, icon?, description?, page_id? | blocks?}."""
|
||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||
if not user or not user.get("id"):
|
||||
raise HTTPException(401, "Authentication required")
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
name = (body.get("name") or "").strip()
|
||||
if not name:
|
||||
raise HTTPException(400, "name is required")
|
||||
@@ -239,17 +227,13 @@ async def create_page_template(request: Request):
|
||||
|
||||
|
||||
@router.post("/api/page-templates/{template_id}/use")
|
||||
async def use_page_template(request: Request, template_id: int):
|
||||
def use_page_template(request: Request, template_id: int, body: dict = Body(default={})):
|
||||
"""v5.12.0: instantiate a page from a template (built-in or user).
|
||||
|
||||
Body: {key?} for built-ins OR uses the row id for user templates.
|
||||
Creates 'blocks'-format page in the caller's workspace and returns its id.
|
||||
"""
|
||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
title = (body.get("title") or "").strip()
|
||||
blocks_json = None
|
||||
if template_id == 0:
|
||||
@@ -331,8 +315,8 @@ async def use_page_template(request: Request, template_id: int):
|
||||
)
|
||||
conn.commit()
|
||||
page_id = cur.lastrowid
|
||||
await fire_event("page.created", {"page_id": page_id, "title": title or name,
|
||||
"workspace": ws_key, "from_template": name})
|
||||
run_event_sync(fire_event("page.created", {"page_id": page_id, "title": title or name,
|
||||
"workspace": ws_key, "from_template": name}))
|
||||
return {"status": "ok", "id": page_id, "title": title or name}
|
||||
|
||||
|
||||
@@ -955,7 +939,7 @@ def list_favorites(request: Request):
|
||||
|
||||
|
||||
@router.post("/api/favorites/{page_id:int}")
|
||||
async def add_favorite(request: Request, page_id: int):
|
||||
def add_favorite(request: Request, page_id: int):
|
||||
"""Add a page to favorites."""
|
||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||
uid = user["id"] if user and user.get("id") else 1
|
||||
@@ -973,14 +957,14 @@ async def add_favorite(request: Request, page_id: int):
|
||||
)
|
||||
conn.commit()
|
||||
try:
|
||||
await fire_event("favorite.added", {"page_id": page_id, "user_id": uid})
|
||||
run_event_sync(fire_event("favorite.added", {"page_id": page_id, "user_id": uid}))
|
||||
except Exception:
|
||||
logger.exception("add_favorite")
|
||||
return {"status": "added", "page_id": page_id}
|
||||
|
||||
|
||||
@router.delete("/api/favorites/{page_id:int}")
|
||||
async def remove_favorite(request: Request, page_id: int):
|
||||
def remove_favorite(request: Request, page_id: int):
|
||||
"""Remove a page from favorites."""
|
||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||
uid = user["id"] if user and user.get("id") else 1
|
||||
@@ -988,7 +972,7 @@ async def remove_favorite(request: Request, page_id: int):
|
||||
conn.execute("DELETE FROM favorites WHERE user_id=? AND page_id=?", (uid, page_id))
|
||||
conn.commit()
|
||||
try:
|
||||
await fire_event("favorite.removed", {"page_id": page_id, "user_id": uid})
|
||||
run_event_sync(fire_event("favorite.removed", {"page_id": page_id, "user_id": uid}))
|
||||
except Exception:
|
||||
logger.exception("remove_favorite")
|
||||
return {"status": "removed", "page_id": page_id}
|
||||
@@ -996,9 +980,8 @@ async def remove_favorite(request: Request, page_id: int):
|
||||
# ═══════════ Share API ═══════════
|
||||
|
||||
@router.post("/api/share/{page_id:int}")
|
||||
async def update_share(request: Request, page_id: int):
|
||||
def update_share(request: Request, page_id: int, body: dict = Body(default={})):
|
||||
"""Save share settings for a page."""
|
||||
body = await request.json()
|
||||
mode = body.get("mode", "private")
|
||||
published = body.get("published", False)
|
||||
with get_conn() as conn:
|
||||
@@ -1011,22 +994,22 @@ async def update_share(request: Request, page_id: int):
|
||||
|
||||
|
||||
@router.post("/api/pages/{page_id:int}/publish")
|
||||
async def publish_page(request: Request, page_id: int):
|
||||
def publish_page(request: Request, page_id: int):
|
||||
"""Publish a page to the web (generates publish_slug)."""
|
||||
if not SessionManager.decode_session(request.cookies.get("flowdeck_session", "")):
|
||||
raise HTTPException(401, "Authentication required")
|
||||
slug, title = publish(page_id)
|
||||
await fire_published(page_id, slug)
|
||||
run_event_sync(fire_published(page_id, slug))
|
||||
return {"is_published": True, "publish_slug": slug, "title": title}
|
||||
|
||||
|
||||
@router.delete("/api/pages/{page_id:int}/publish")
|
||||
async def unpublish_page(request: Request, page_id: int):
|
||||
def unpublish_page(request: Request, page_id: int):
|
||||
"""Unpublish a page from the web."""
|
||||
if not SessionManager.decode_session(request.cookies.get("flowdeck_session", "")):
|
||||
raise HTTPException(401, "Authentication required")
|
||||
unpublish(page_id)
|
||||
await fire_unpublished(page_id)
|
||||
run_event_sync(fire_unpublished(page_id))
|
||||
return {"is_published": False}
|
||||
|
||||
|
||||
@@ -1040,12 +1023,12 @@ def list_trash(request: Request):
|
||||
|
||||
|
||||
@router.post("/api/trash/{page_id}/restore")
|
||||
async def restore_page(request: Request, page_id: int):
|
||||
def restore_page(request: Request, page_id: int):
|
||||
with get_conn() as conn:
|
||||
conn.execute("UPDATE pages SET deleted_at=NULL WHERE id=?", (page_id,))
|
||||
conn.commit()
|
||||
try:
|
||||
await fire_event("page.restored", {"page_id": page_id})
|
||||
run_event_sync(fire_event("page.restored", {"page_id": page_id}))
|
||||
except Exception:
|
||||
logger.exception("restore_page")
|
||||
return {"status": "ok", "restored": page_id}
|
||||
@@ -1080,12 +1063,8 @@ def list_synced_blocks_api(request: Request, workspace: str = Query(default=""))
|
||||
|
||||
|
||||
@router.post("/api/synced-blocks")
|
||||
async def create_synced_block_api(request: Request):
|
||||
def create_synced_block_api(request: Request, body: dict = Body(...)):
|
||||
"""Create a new synced block."""
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
raise HTTPException(400, "Invalid JSON body") from None
|
||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||
from app.services.synced_blocks import create_synced_block
|
||||
sid = create_synced_block(
|
||||
@@ -1157,12 +1136,8 @@ def get_synced_block_api(sid: int):
|
||||
|
||||
|
||||
@router.post("/api/pages/{page_id}/synced")
|
||||
async def add_synced_to_page(request: Request, page_id: int):
|
||||
def add_synced_to_page(request: Request, page_id: int, body: dict = Body(...)):
|
||||
"""Add a synced block reference to a page."""
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
raise HTTPException(400, "Invalid JSON body") from None
|
||||
from app.services.synced_blocks import add_page_synced, get_synced_block
|
||||
sid = body.get("synced_block_id")
|
||||
sb = get_synced_block(sid)
|
||||
@@ -1357,7 +1332,7 @@ async def extract_ai_keywords(owner: str, repo: str):
|
||||
# ═══════════ Pages Markdown ═══════════
|
||||
|
||||
@router.post("/api/pages")
|
||||
async def create_page(request: Request, title: str = Query(default=""),
|
||||
def create_page(request: Request, title: str = Query(default=""),
|
||||
section: str = Query(default="Private"),
|
||||
project: str = Query(default=""),
|
||||
parent_id: int = Query(default=0)):
|
||||
@@ -1385,8 +1360,8 @@ async def create_page(request: Request, title: str = Query(default=""),
|
||||
)
|
||||
conn.commit()
|
||||
page_id = cur.lastrowid
|
||||
await fire_event("page.created", {"page_id": page_id, "title": page_title,
|
||||
"workspace": ws_key, "parent_id": parent_id})
|
||||
run_event_sync(fire_event("page.created", {"page_id": page_id, "title": page_title,
|
||||
"workspace": ws_key, "parent_id": parent_id}))
|
||||
return {"status": "ok", "id": page_id, "title": page_title, "workspace": ws_key, "parent_id": parent_id}
|
||||
except Exception as e:
|
||||
logger.error("create_page failed: %s", e)
|
||||
@@ -1411,7 +1386,7 @@ def get_page(request: Request, page_id: int):
|
||||
|
||||
|
||||
@router.put("/api/pages/{page_id}")
|
||||
async def update_page(request: Request, page_id: int, title: str = Query(default=""),
|
||||
def update_page(request: Request, page_id: int, title: str = Query(default=""),
|
||||
content: str = Query(default=""),
|
||||
content_format: str = Query(default="")):
|
||||
"""Update a page's title and/or content. Accepts JSON body for blocks."""
|
||||
@@ -1437,24 +1412,20 @@ async def update_page(request: Request, page_id: int, title: str = Query(default
|
||||
if content_format:
|
||||
conn.execute("UPDATE pages SET content_format=?, updated_at=CURRENT_TIMESTAMP WHERE id=?", (content_format, page_id))
|
||||
conn.commit()
|
||||
await fire_event("page.updated", {"page_id": page_id, "title": title,
|
||||
run_event_sync(fire_event("page.updated", {"page_id": page_id, "title": title,
|
||||
"content_format": content_format or "markdown",
|
||||
"actor_id": user.get("id")})
|
||||
"actor_id": user.get("id")}))
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.post("/api/pages/{page_id}/blocks")
|
||||
async def save_page_blocks(request: Request, page_id: int):
|
||||
def save_page_blocks(request: Request, page_id: int, body: dict = Body(...)):
|
||||
"""Save blocks JSON content (Notion-style block editor).
|
||||
|
||||
v5.4.0: a version snapshot is recorded (if the block content actually
|
||||
changed) so the UI can browse the version history and restore any of them.
|
||||
v5.14.0: synced block references are tracked in page_synced_blocks.
|
||||
"""
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
raise HTTPException(400, "Invalid JSON body") from None
|
||||
blocks = body.get("blocks", [])
|
||||
blocks_json = json.dumps(blocks)
|
||||
title = body.get("title", "")
|
||||
@@ -1504,9 +1475,9 @@ async def save_page_blocks(request: Request, page_id: int):
|
||||
(page_id, sid),
|
||||
)
|
||||
conn.commit()
|
||||
await fire_event("page.updated", {"page_id": page_id, "title": title or "",
|
||||
run_event_sync(fire_event("page.updated", {"page_id": page_id, "title": title or "",
|
||||
"content_format": "blocks",
|
||||
"actor_id": uid})
|
||||
"actor_id": uid}))
|
||||
return {"status": "ok", "id": page_id}
|
||||
|
||||
|
||||
@@ -1605,7 +1576,7 @@ def page_versions(request: Request, page_id: int):
|
||||
|
||||
|
||||
@router.post("/api/pages/{page_id}/versions/{version_id}/restore")
|
||||
async def restore_version(request: Request, page_id: int, version_id: int):
|
||||
def restore_version(request: Request, page_id: int, version_id: int):
|
||||
"""v5.4.0: restore a page from a version snapshot."""
|
||||
with get_conn() as conn:
|
||||
ver = conn.execute(
|
||||
@@ -1619,8 +1590,8 @@ async def restore_version(request: Request, page_id: int, version_id: int):
|
||||
(ver["blocks_json"], ver["title"] or "", page_id),
|
||||
)
|
||||
conn.commit()
|
||||
await fire_event("page.updated", {"page_id": page_id, "title": ver["title"] or "",
|
||||
"content_format": "blocks"})
|
||||
run_event_sync(fire_event("page.updated", {"page_id": page_id, "title": ver["title"] or "",
|
||||
"content_format": "blocks"}))
|
||||
return {"status": "ok", "restored": version_id}
|
||||
|
||||
|
||||
@@ -1628,7 +1599,7 @@ async def restore_version(request: Request, page_id: int, version_id: int):
|
||||
|
||||
|
||||
@router.post("/api/pages/{page_id}/duplicate")
|
||||
async def duplicate_page(request: Request, page_id: int):
|
||||
def duplicate_page(request: Request, page_id: int):
|
||||
"""Duplicate a page (block/markdown content included) as a sibling."""
|
||||
SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||
with get_conn() as conn:
|
||||
@@ -1664,8 +1635,8 @@ async def duplicate_page(request: Request, page_id: int):
|
||||
with get_conn() as conn:
|
||||
conn.execute("UPDATE pages SET title=? WHERE id=?", (title, new_id))
|
||||
conn.commit()
|
||||
await fire_event("page.created", {"page_id": new_id, "title": title,
|
||||
"workspace": page.get("workspace")})
|
||||
run_event_sync(fire_event("page.created", {"page_id": new_id, "title": title,
|
||||
"workspace": page.get("workspace")}))
|
||||
return {"status": "ok", "id": new_id, "title": title}
|
||||
|
||||
|
||||
@@ -1758,9 +1729,8 @@ def remove_page_cover(request: Request, page_id: int):
|
||||
|
||||
|
||||
@router.post("/api/pages/{page_id}/icon")
|
||||
async def set_page_icon(request: Request, page_id: int):
|
||||
def set_page_icon(request: Request, page_id: int, body: dict = Body(default={})):
|
||||
"""v5.5.0: set a page emoji/icon label (or a custom-emoji image URL)."""
|
||||
body = await request.json()
|
||||
icon = (body.get("icon") or "").strip()
|
||||
if len(icon) > 512:
|
||||
raise HTTPException(400, "icon too long")
|
||||
@@ -1935,16 +1905,12 @@ async def _unfurl_repo(forge: str, owner: str, repo: str):
|
||||
|
||||
|
||||
@router.post("/api/embed/resolve")
|
||||
async def resolve_embed(request: Request):
|
||||
def resolve_embed(request: Request, body: dict = Body(...)):
|
||||
"""v5.5.0: rewrite a pasted URL to its provider embed src.
|
||||
|
||||
Powers the universal ``/embed`` block (YouTube, Vimeo, Figma, Maps,
|
||||
Docs, Loom, CodePen, Miro, Spotify, SoundCloud, Twitch, X/Twitter…).
|
||||
"""
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
raise HTTPException(400, "Invalid JSON body") from None
|
||||
url = (body.get("url") or "").strip()
|
||||
if not url:
|
||||
raise HTTPException(400, "url required")
|
||||
@@ -1955,7 +1921,7 @@ async def resolve_embed(request: Request):
|
||||
|
||||
|
||||
@router.put("/api/pages/{page_id}/move")
|
||||
async def move_page(request: Request, page_id: int):
|
||||
def move_page(request: Request, page_id: int, body: dict = Body(default={})):
|
||||
"""Move a page to another workspace or reorder within tree.
|
||||
|
||||
Body (JSON): { workspace_id?: int, parent_id?: int, new_order?: int }
|
||||
@@ -1963,10 +1929,6 @@ async def move_page(request: Request, page_id: int):
|
||||
- parent_id: change parent (0 = root level)
|
||||
- new_order: position among siblings (0 = append)
|
||||
"""
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
new_ws_id = body.get("workspace_id")
|
||||
new_parent_id = body.get("parent_id", 0)
|
||||
new_order = body.get("new_order", 0)
|
||||
@@ -1997,13 +1959,13 @@ async def move_page(request: Request, page_id: int):
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
await fire_event("page.moved", {"page_id": page_id, "workspace_id": new_ws_id or 0,
|
||||
"parent_id": new_parent_id})
|
||||
run_event_sync(fire_event("page.moved", {"page_id": page_id, "workspace_id": new_ws_id or 0,
|
||||
"parent_id": new_parent_id}))
|
||||
return {"status": "ok", "id": page_id}
|
||||
|
||||
|
||||
@router.delete("/api/pages/{page_id}")
|
||||
async def delete_page(request: Request, page_id: int):
|
||||
def delete_page(request: Request, page_id: int):
|
||||
"""Move a page to trash (soft delete)."""
|
||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||
uid = (user or {}).get("id")
|
||||
@@ -2019,7 +1981,7 @@ async def delete_page(request: Request, page_id: int):
|
||||
import datetime
|
||||
conn.execute("UPDATE pages SET deleted_at=? WHERE id=?", (datetime.datetime.now(datetime.UTC).replace(tzinfo=None).isoformat(), page_id,))
|
||||
conn.commit()
|
||||
await fire_event("page.deleted", {"page_id": page_id, "title": row["title"] or ""})
|
||||
run_event_sync(fire_event("page.deleted", {"page_id": page_id, "title": row["title"] or ""}))
|
||||
return {"status": "ok", "deleted": page_id, "title": row["title"]}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user