fix: A21 phase 2a — api_v2 : body JSON en paramètre, 36 routes hors loop (v7.9.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s

- Les 36 routes d'api_v2 dont le SEUL `await` était
  `body = await request.json()` (bloc try/except uniforme) → paramètre FastAPI
  `body: dict = Body(default={})` + conversion en `def` → threadpool :
  toute leur séquence SQLite quitte l'event loop.
- Équivalences vérifiées avant engament (probe FastAPI) :
  · corps absent → `{}` (identique à l'ancien try/except)
  · JSON invalide → 422 (avant : avalé comme `{}` — 422 est plus juste)
  · zéro `body[...]=` / setdefault / update dans api_v2 → défaut partagé
    jamais muté
- verify_webhook_signature (signature multi-ligne) traitée à la main.
- Piège courant évité : première version du script supprimait 5 lignes au
  lieu de 4 (slice m-1:m+4) → fichier restauré depuis git, slice corrigée,
  0 ligne perdue (diff logique +39/-183).

api_v2 : 96/115 routes hors loop (60 phase 1 + 36 ici) ; 19 async restantes
(fire_event, request.form, gitea/webhooks) = phase 2b.

suite **1037/1037** (242 s) · `ruff check app tests` OK · docs à jour
This commit is contained in:
2026-10-01 11:19:58 -04:00
parent 224bda74d5
commit 07904f05e5
7 changed files with 465 additions and 189 deletions
+1 -1
View File
@@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI):
app = FastAPI(
title="FlowDeck",
version="7.8.0",
version="7.9.0",
docs_url="/docs",
redoc_url="/redoc",
lifespan=lifespan,
+39 -183
View File
@@ -12,7 +12,7 @@ import logging
import secrets
from datetime import datetime
from fastapi import APIRouter, Header, HTTPException, Request
from fastapi import APIRouter, Body, Header, HTTPException, Request
from fastapi.responses import JSONResponse, Response
from app.db import get_conn
@@ -48,13 +48,9 @@ def _v2_rate_check(request: Request, user: dict) -> None:
# ── Tokens ────────────────────────────────────────────────────────────────
@router.post("/tokens")
async def create_token(request: Request, authorization: str | None = Header(default=None)):
def create_token(request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
name = (body.get("name") or "API token").strip()[:100]
scopes = validate_scopes_input(body.get("scopes") or "read,write")
expires_at = body.get("expires_at")
@@ -166,13 +162,9 @@ def get_me(request: Request, authorization: str | None = Header(default=None)):
return d
@router.patch("/users/me")
async def patch_me(request: Request, authorization: str | None = Header(default=None)):
def patch_me(request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
allowed = {"full_name", "email", "avatar_color", "notification_prefs", "sidebar_config", "timezone"}
updates = {}
for k in allowed:
@@ -232,16 +224,12 @@ def list_workspaces(request: Request, authorization: str | None = Header(default
return {"workspaces": out, "total": total, "limit": limit, "offset": offset}
@router.post("/workspaces")
async def create_workspace(request: Request, authorization: str | None = Header(default=None)):
def create_workspace(request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
idem = check_idempotency(request, user["id"])
if idem:
return JSONResponse(content=idem["data"], status_code=idem["status"])
try:
body = await request.json()
except Exception:
body = {}
name = (body.get("name") or "").strip()
if not name:
raise HTTPException(400, "name is required")
@@ -282,13 +270,9 @@ def get_workspace(workspace_id: int, request: Request, authorization: str | None
return d
@router.patch("/workspaces/{workspace_id}")
async def patch_workspace(workspace_id: int, request: Request, authorization: str | None = Header(default=None)):
def patch_workspace(workspace_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
with get_conn() as conn:
row = conn.execute("SELECT * FROM workspaces WHERE id=?", (workspace_id,)).fetchone()
if not row:
@@ -336,13 +320,9 @@ def list_workspace_members(workspace_id: int, request: Request, authorization: s
return {"members": [row_to_dict(r) for r in rows]}
@router.post("/workspaces/{workspace_id}/members")
async def invite_member(workspace_id: int, request: Request, authorization: str | None = Header(default=None)):
def invite_member(workspace_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
target_id = body.get("user_id") or body.get("uid")
email = (body.get("email") or "").strip()
role = (body.get("role") or "editor").strip().lower()
@@ -374,13 +354,9 @@ async def invite_member(workspace_id: int, request: Request, authorization: str
return {"workspace_id": workspace_id, "user_id": uid, "role": role, "status": "added"}
@router.patch("/workspaces/{workspace_id}/members/{uid}")
async def update_member_role(workspace_id: int, uid: int, request: Request, authorization: str | None = Header(default=None)):
def update_member_role(workspace_id: int, uid: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
role = (body.get("role") or "").strip().lower()
if role not in ("owner", "admin", "editor", "viewer", "commenter"):
raise HTTPException(400, "Invalid role")
@@ -451,16 +427,12 @@ def list_collections_v2(request: Request, authorization: str | None = Header(def
return JSONResponse(content=resp, headers=paginate_headers(total))
@router.post("/collections")
async def create_collection_v2(request: Request, authorization: str | None = Header(default=None)):
def create_collection_v2(request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
idem = check_idempotency(request, user["id"])
if idem:
return JSONResponse(content=idem["data"], status_code=idem["status"])
try:
body = await request.json()
except Exception:
body = {}
name = (body.get("name") or "").strip()
if not name:
raise HTTPException(400, "name is required")
@@ -507,13 +479,9 @@ def get_collection_v2(collection_id: int, request: Request, authorization: str |
return d
@router.patch("/collections/{collection_id}")
async def patch_collection_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
def patch_collection_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
with get_conn() as conn:
row = conn.execute("SELECT * FROM collections WHERE id=?", (collection_id,)).fetchone()
if not row:
@@ -545,13 +513,9 @@ def delete_collection_v2(collection_id: int, request: Request, authorization: st
return {"id": collection_id, "status": "deleted"}
@router.post("/collections/{collection_id}/linked")
async def create_linked_db(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
def create_linked_db(collection_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
name = (body.get("name") or "").strip() or f"Linked DB {collection_id}"
with get_conn() as conn:
src = conn.execute("SELECT * FROM collections WHERE id=?", (collection_id,)).fetchone()
@@ -607,13 +571,9 @@ def list_sources(collection_id: int, request: Request, authorization: str | None
return {"sources": [dict(r) for r in rows]}
@router.post("/collections/{collection_id}/sources")
async def add_source(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
def add_source(collection_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
src_id = body.get("source_collection_id") or body.get("source_id")
if not src_id:
raise HTTPException(400, "source_collection_id required")
@@ -862,13 +822,9 @@ async def restore_page_v2(page_id: int, request: Request, authorization: str | N
raise HTTPException(404, "Page not found or not deleted")
@router.post("/pages/{page_id}/move")
async def move_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
def move_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
with get_conn() as conn:
row = conn.execute("SELECT * FROM collection_pages WHERE id=?", (page_id,)).fetchone()
if not row:
@@ -891,13 +847,9 @@ def list_sub_items_v2(page_id: int, request: Request, authorization: str | None
return {"sub_items": [row_to_dict(r) for r in rows]}
@router.post("/pages/{page_id}/sub-items")
async def create_sub_item_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
def create_sub_item_v2(page_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
with get_conn() as conn:
parent = conn.execute("SELECT collection_id FROM collection_pages WHERE id=?", (page_id,)).fetchone()
if not parent:
@@ -920,13 +872,9 @@ def list_dependencies_v2(page_id: int, request: Request, authorization: str | No
return {"dependencies": [dict(r) for r in rows]}
@router.post("/pages/{page_id}/dependencies")
async def add_dependency_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
def add_dependency_v2(page_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
dep_id = body.get("dependency_id") or body.get("depends_on")
dtype = body.get("dependency_type") or "blocks"
if not dep_id:
@@ -967,13 +915,9 @@ def list_properties_v2(collection_id: int, request: Request, authorization: str
return {"properties": [row_to_dict(r) for r in rows]}
@router.post("/collections/{collection_id}/properties")
async def create_property_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
def create_property_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
name = (body.get("name") or "").strip()
if not name:
raise HTTPException(400, "name is required")
@@ -992,13 +936,9 @@ async def create_property_v2(collection_id: int, request: Request, authorization
return {"id": pid, "name": name, "prop_type": ptype, "status": "created"}
@router.patch("/properties/{prop_id}")
async def patch_property_v2(prop_id: int, request: Request, authorization: str | None = Header(default=None)):
def patch_property_v2(prop_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
with get_conn() as conn:
row = conn.execute("SELECT * FROM collection_properties WHERE id=?", (prop_id,)).fetchone()
if not row:
@@ -1028,13 +968,9 @@ def delete_property_v2(prop_id: int, request: Request, authorization: str | None
return {"id": prop_id, "status": "deleted"}
@router.post("/properties/{prop_id}/relation")
async def create_relation_v2(prop_id: int, request: Request, authorization: str | None = Header(default=None)):
def create_relation_v2(prop_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
related_id = body.get("related_collection_id")
reverse = (body.get("reverse_name") or "").strip()
if not related_id:
@@ -1054,13 +990,9 @@ async def create_relation_v2(prop_id: int, request: Request, authorization: str
return {"id": prop_id, "status": "updated"}
@router.post("/properties/evaluate-formula")
async def evaluate_formula_v2(request: Request, authorization: str | None = Header(default=None)):
def evaluate_formula_v2(request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = get_bearer_user(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
expr = body.get("expression") or body.get("formula")
if not expr:
raise HTTPException(400, "expression required")
@@ -1073,13 +1005,9 @@ async def evaluate_formula_v2(request: Request, authorization: str | None = Head
return {"result": res, "expression": expr}
@router.post("/properties/compute-rollup")
async def compute_rollup_v2(request: Request, authorization: str | None = Header(default=None)):
def compute_rollup_v2(request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = get_bearer_user(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
for k in ("collection_id", "relation_property_id", "target_property_id", "page_id"):
if k not in body:
raise HTTPException(400, f"{k} required")
@@ -1126,13 +1054,9 @@ async def create_view_v2(collection_id: int, request: Request, authorization: st
return {"id": vid, "name": name, "view_type": vtype, "status": "created"}
@router.patch("/views/{view_id}")
async def patch_view_v2(view_id: int, request: Request, authorization: str | None = Header(default=None)):
def patch_view_v2(view_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
with get_conn() as conn:
row = conn.execute("SELECT * FROM collection_views WHERE id=?", (view_id,)).fetchone()
if not row:
@@ -1169,13 +1093,9 @@ def delete_view_v2(view_id: int, request: Request, authorization: str | None = H
return {"id": view_id, "status": "deleted"}
@router.post("/views/{view_id}/save-as")
async def save_as_view_v2(view_id: int, request: Request, authorization: str | None = Header(default=None)):
def save_as_view_v2(view_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
name = (body.get("name") or "").strip() or "Copy"
with get_conn() as conn:
row = conn.execute("SELECT * FROM collection_views WHERE id=?", (view_id,)).fetchone()
@@ -1196,13 +1116,9 @@ def list_dashboards_v2(collection_id: int, request: Request, authorization: str
return {"dashboards": [row_to_dict(r) for r in rows]}
@router.post("/collections/{collection_id}/dashboards")
async def create_dashboard_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
def create_dashboard_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
name = (body.get("name") or "Dashboard").strip()
layout = body.get("layout") or body.get("layout_json") or {"columns": 1, "widgets": []}
with get_conn() as conn:
@@ -1212,13 +1128,9 @@ async def create_dashboard_v2(collection_id: int, request: Request, authorizatio
return {"id": did, "name": name, "status": "created"}
@router.patch("/dashboards/{dashboard_id}")
async def patch_dashboard_v2(dashboard_id: int, request: Request, authorization: str | None = Header(default=None)):
def patch_dashboard_v2(dashboard_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
with get_conn() as conn:
row = conn.execute("SELECT * FROM collection_dashboards WHERE id=?", (dashboard_id,)).fetchone()
if not row:
@@ -1390,13 +1302,9 @@ def mark_all_read(request: Request, authorization: str | None = Header(default=N
return {"status": "all read"}
@router.patch("/users/me/preferences")
async def patch_prefs(request: Request, authorization: str | None = Header(default=None)):
def patch_prefs(request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = get_bearer_user(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
with get_conn() as conn:
row = conn.execute("SELECT notification_prefs FROM users WHERE id=?", (user["id"],)).fetchone()
try:
@@ -1467,13 +1375,9 @@ def list_tags_v2(request: Request, authorization: str | None = Header(default=No
return {"tags": [dict(r) for r in rows]}
@router.post("/tags")
async def create_tag_v2(request: Request, authorization: str | None = Header(default=None)):
def create_tag_v2(request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
name = (body.get("name") or "").strip()
if not name:
raise HTTPException(400, "name required")
@@ -1488,13 +1392,9 @@ async def create_tag_v2(request: Request, authorization: str | None = Header(def
return {"id": tid, "name": name, "color": color, "status": "created"}
@router.patch("/tags/{tag_id}")
async def patch_tag_v2(tag_id: int, request: Request, authorization: str | None = Header(default=None)):
def patch_tag_v2(tag_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
with get_conn() as conn:
row = conn.execute("SELECT * FROM tags WHERE id=? AND user_id=?", (tag_id, user["id"])).fetchone()
if not row:
@@ -1515,13 +1415,9 @@ def delete_tag_v2(tag_id: int, request: Request, authorization: str | None = Hea
return {"id": tag_id, "status": "deleted"}
@router.post("/pages/{page_id}/tags")
async def attach_tag_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
def attach_tag_v2(page_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
tag_id = body.get("tag_id")
if not tag_id:
raise HTTPException(400, "tag_id required")
@@ -1593,13 +1489,9 @@ async def create_share_v2(page_id: int, request: Request, authorization: str | N
return {"id": nid, "page_id": page_id, "status": "shared"}
@router.patch("/shares/{share_id}")
async def patch_share_v2(share_id: int, request: Request, authorization: str | None = Header(default=None)):
def patch_share_v2(share_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
perm = (body.get("permission") or "").strip().lower()
if perm not in ("view", "comment", "edit"):
raise HTTPException(400, "Invalid permission")
@@ -1661,13 +1553,9 @@ def list_history_v2(page_id: int, request: Request, authorization: str | None =
return {"history": [row_to_dict(r) for r in rows], "versions": [row_to_dict(r) for r in vrows]}
@router.post("/pages/{page_id}/history/restore")
async def restore_history_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
def restore_history_v2(page_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
hid = body.get("history_id") or body.get("id") or body.get("version_id")
if not hid:
raise HTTPException(400, "history_id required")
@@ -1760,13 +1648,9 @@ def delete_sprint_v2(sprint_id: int, request: Request, authorization: str | None
return {"id": sprint_id, "status": "deleted"}
@router.post("/sprints/{sprint_id}/assign")
async def assign_sprint_v2(sprint_id: int, request: Request, authorization: str | None = Header(default=None)):
def assign_sprint_v2(sprint_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
pid = body.get("page_id")
if not pid:
raise HTTPException(400, "page_id required")
@@ -1824,13 +1708,9 @@ def list_templates_v2(collection_id: int, request: Request, authorization: str |
return {"templates": [row_to_dict(r) for r in rows]}
@router.post("/collections/{collection_id}/templates")
async def create_template_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
def create_template_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
name = (body.get("name") or "Template").strip()
pv = json.dumps(body.get("property_values") or body.get("property_values_json") or {})
cj = json.dumps(body.get("content") or body.get("content_json") or [])
@@ -1841,13 +1721,9 @@ async def create_template_v2(collection_id: int, request: Request, authorization
return {"id": tid, "name": name, "status": "created"}
@router.patch("/templates/{template_id}")
async def patch_template_v2(template_id: int, request: Request, authorization: str | None = Header(default=None)):
def patch_template_v2(template_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
with get_conn() as conn:
row = conn.execute("SELECT * FROM page_templates WHERE id=?", (template_id,)).fetchone()
if not row:
@@ -1892,13 +1768,9 @@ def list_db_templates_v2(request: Request, authorization: str | None = Header(de
return {"templates": [row_to_dict(r) for r in rows]}
@router.post("/templates/database/{template_id}/apply")
async def apply_db_template_v2(template_id: int, request: Request, authorization: str | None = Header(default=None)):
def apply_db_template_v2(template_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
with get_conn() as conn:
tpl = conn.execute("SELECT * FROM database_templates WHERE id=?", (template_id,)).fetchone()
if not tpl:
@@ -2065,15 +1937,11 @@ def admin_list_users_v2(request: Request, limit: int = 30, offset: int = 0, auth
return {"users": [row_to_dict(r) for r in rows], "total": total, "limit": limit, "offset": offset}
@router.patch("/admin/users/{uid}")
async def admin_patch_user_v2(uid: int, request: Request, authorization: str | None = Header(default=None)):
def admin_patch_user_v2(uid: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = get_bearer_user(request, authorization)
_v2_rate_check(request, user)
if not user.get("is_admin") and not has_scope(user.get("_token_scopes"), "admin"):
raise HTTPException(403, "Admin scope required")
try:
body = await request.json()
except Exception:
body = {}
with get_conn() as conn:
if not conn.execute("SELECT id FROM users WHERE id=?", (uid,)).fetchone():
raise HTTPException(404, "User not found")
@@ -2144,13 +2012,9 @@ def list_webhooks_v2(request: Request, authorization: str | None = Header(defaul
)
@router.post("/webhooks")
async def create_webhook_v2(request: Request, authorization: str | None = Header(default=None)):
def create_webhook_v2(request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
from app.services.webhook_outbound import EVENTS, _event_matches
url = (body.get("url") or "").strip()
event = (body.get("event") or "page.created").strip()
@@ -2172,13 +2036,9 @@ async def create_webhook_v2(request: Request, authorization: str | None = Header
"signature_header": "X-FlowDeck-Signature (HMAC-SHA256, sha256=<hex>)" if secret else None}
@router.patch("/webhooks/{webhook_id}")
async def patch_webhook_v2(webhook_id: int, request: Request, authorization: str | None = Header(default=None)):
def patch_webhook_v2(webhook_id: int, request: Request, authorization: str | None = Header(default=None), body: dict = Body(default={})):
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
try:
body = await request.json()
except Exception:
body = {}
with get_conn() as conn:
row = conn.execute("SELECT * FROM webhook_subscriptions WHERE id=?", (webhook_id,)).fetchone()
if not row:
@@ -2274,19 +2134,15 @@ async def retry_webhook_deliveries(webhook_id: int, request: Request,
@router.post("/webhooks/verify-signature")
async def verify_webhook_signature(request: Request,
authorization: str | None = Header(default=None)):
def verify_webhook_signature(request: Request,
authorization: str | None = Header(default=None),
body: dict = Body(default={})):
"""Verify a webhook signature (for debugging/testing)."""
user = get_bearer_user(request, authorization)
_v2_rate_check(request, user)
from app.services.webhook_outbound import verify_signature
try:
body = await request.json()
except Exception:
body = {}
secret = body.get("secret", "")
payload = body.get("payload", "{}")
signature = body.get("signature", "")