fix: A30 + A37 + A39 + A40 + A41 — fin du P2/P3 XS/S (v7.4.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s

- A30 — `require_scope()` câblé : 69 sites stricts de api_v2.py passent par la
  factory (Bearer + scope en 1 appel, contrôle manuel supprimé) ; sémantique
  alignée sur celle des handlers (pas de default "read" → 0 changement de
  comportement) ; 12 top-level morts supprimés (0 ref app ET tests) :
  unsync_block, find_referring, _b64url, strip_markdown, format_number,
  get_auto_property_value, get_next_unique_id, local_date_in_tz,
  verify_device_token, _get_dynamic_groups, _require_user_gitea,
  validate_upload_request
- A37 — CORS sans `*` : origines = app_base_url + allow_origin_regex
  (localhost/dev, origines d'extension pour le Web Clipper), méthodes et
  entêtes minutées, allow_credentials explicite + test test_cors_no_star
- A39 — htmx : décision « rien » documentée (32 attributs hx-* réels sur 6
  templates, conversion = refonte du view-switching sans test E2E)
- A40 — version d'assets à source unique : ENV.globals["asset_version"] lu au
  boot depuis le fichier VERSION ; littéraux `?v=` de base.html éliminés ;
  test test_asset_version_single_source
- A41 — app.css : 91 règles mortes purgées (-10 274 octets, 121 618 → 111 344),
  scan templates/JS/CSS/Python à 0 référence

suite **1031/1031** · `ruff check app tests` OK · OpenAPI 511 chemins / 7.4.0
docs (ROADMAP/CHANGELOG/WORKLOAD/VERSION) à jour
This commit is contained in:
2026-10-01 09:30:37 -04:00
parent 998b5c630c
commit 937ecfc2e0
23 changed files with 179 additions and 819 deletions
+71 -208
View File
@@ -16,7 +16,7 @@ from fastapi import APIRouter, Header, HTTPException, Request
from fastapi.responses import JSONResponse, Response
from app.db import get_conn
from app.services.api_v2_helpers import (
from app.services.api_v2_helpers import ( # noqa: F401 — require_scope est utilisé par les handlers
audit_log,
check_idempotency,
check_v2_rate_limit,
@@ -24,6 +24,7 @@ from app.services.api_v2_helpers import (
has_scope,
paginate_headers,
parse_pagination,
require_scope,
row_to_dict,
store_idempotency,
to_iso8601,
@@ -47,10 +48,8 @@ def _v2_rate_check(request: Request, user: dict) -> None:
@router.post("/tokens")
async def create_token(request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -110,10 +109,8 @@ async def list_tokens(request: Request, authorization: str | None = Header(defau
@router.delete("/tokens/{token_id}")
async def revoke_token(token_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
row = conn.execute("SELECT id, user_id FROM api_tokens WHERE id=?", (token_id,)).fetchone()
if not row:
@@ -127,10 +124,8 @@ async def revoke_token(token_id: int, request: Request, authorization: str | Non
@router.post("/tokens/{token_id}/rotate")
async def rotate_token(token_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
row = conn.execute("SELECT id, user_id, name, scopes FROM api_tokens WHERE id=?", (token_id,)).fetchone()
if not row:
@@ -171,10 +166,8 @@ async def get_me(request: Request, authorization: str | None = Header(default=No
@router.patch("/users/me")
async def patch_me(request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -239,10 +232,8 @@ async def list_workspaces(request: Request, authorization: str | None = Header(d
@router.post("/workspaces")
async def create_workspace(request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
idem = check_idempotency(request, user["id"])
if idem:
return JSONResponse(content=idem["data"], status_code=idem["status"])
@@ -291,10 +282,8 @@ async def get_workspace(workspace_id: int, request: Request, authorization: str
@router.patch("/workspaces/{workspace_id}")
async def patch_workspace(workspace_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -321,10 +310,8 @@ async def patch_workspace(workspace_id: int, request: Request, authorization: st
@router.delete("/workspaces/{workspace_id}")
async def delete_workspace(workspace_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
row = conn.execute("SELECT * FROM workspaces WHERE id=?", (workspace_id,)).fetchone()
if not row:
@@ -349,10 +336,8 @@ async def list_workspace_members(workspace_id: int, request: Request, authorizat
@router.post("/workspaces/{workspace_id}/members")
async def invite_member(workspace_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -389,10 +374,8 @@ async def invite_member(workspace_id: int, request: Request, authorization: str
@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)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -417,10 +400,8 @@ async def update_member_role(workspace_id: int, uid: int, request: Request, auth
@router.delete("/workspaces/{workspace_id}/members/{uid}")
async def remove_member(workspace_id: int, uid: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
ws = conn.execute("SELECT owner_id FROM workspaces WHERE id=?", (workspace_id,)).fetchone()
if not ws:
@@ -470,10 +451,8 @@ async def list_collections_v2(request: Request, authorization: str | None = Head
@router.post("/collections")
async def create_collection_v2(request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
idem = check_idempotency(request, user["id"])
if idem:
return JSONResponse(content=idem["data"], status_code=idem["status"])
@@ -528,10 +507,8 @@ async def get_collection_v2(collection_id: int, request: Request, authorization:
@router.patch("/collections/{collection_id}")
async def patch_collection_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -555,10 +532,8 @@ async def patch_collection_v2(collection_id: int, request: Request, authorizatio
@router.delete("/collections/{collection_id}")
async def delete_collection_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
row = conn.execute("SELECT * FROM collections WHERE id=?", (collection_id,)).fetchone()
if not row:
@@ -570,10 +545,8 @@ async def delete_collection_v2(collection_id: int, request: Request, authorizati
@router.post("/collections/{collection_id}/linked")
async def create_linked_db(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -609,10 +582,8 @@ async def create_linked_db(collection_id: int, request: Request, authorization:
@router.post("/collections/{collection_id}/task")
async def toggle_task(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
row = conn.execute("SELECT is_task FROM collections WHERE id=?", (collection_id,)).fetchone()
if not row:
@@ -636,10 +607,8 @@ async def list_sources(collection_id: int, request: Request, authorization: str
@router.post("/collections/{collection_id}/sources")
async def add_source(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -662,10 +631,8 @@ async def add_source(collection_id: int, request: Request, authorization: str |
@router.delete("/collections/{collection_id}/sources/{source_id}")
async def remove_source(collection_id: int, source_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
conn.execute("DELETE FROM collection_data_sources WHERE collection_id=? AND (id=? OR source_collection_id=?)", (collection_id, source_id, source_id))
conn.commit()
@@ -734,10 +701,8 @@ async def list_collection_pages_v2(collection_id: int, request: Request, authori
@router.post("/collections/{collection_id}/pages")
async def create_collection_page_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
idem = check_idempotency(request, user["id"])
if idem:
return JSONResponse(content=idem["data"], status_code=idem["status"])
@@ -816,10 +781,8 @@ async def get_page_v2(page_id: int, request: Request, authorization: str | None
@router.patch("/pages/{page_id}")
async def patch_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -865,10 +828,8 @@ async def patch_page_v2(page_id: int, request: Request, authorization: str | Non
@router.delete("/pages/{page_id}")
async def delete_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
row = conn.execute("SELECT * FROM collection_pages WHERE id=?", (page_id,)).fetchone()
if not row:
@@ -884,10 +845,8 @@ async def delete_page_v2(page_id: int, request: Request, authorization: str | No
@router.post("/pages/{page_id}/restore")
async def restore_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
# For soft-deleted pages (deleted_at) - but collection_pages has no deleted_at; handle pages table
with get_conn() as conn:
row = conn.execute("SELECT deleted_at FROM pages WHERE id=?", (page_id,)).fetchone()
@@ -903,10 +862,8 @@ async def restore_page_v2(page_id: int, request: Request, authorization: str | N
@router.post("/pages/{page_id}/move")
async def move_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -934,10 +891,8 @@ async def list_sub_items_v2(page_id: int, request: Request, authorization: str |
@router.post("/pages/{page_id}/sub-items")
async def create_sub_item_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -965,10 +920,8 @@ async def list_dependencies_v2(page_id: int, request: Request, authorization: st
@router.post("/pages/{page_id}/dependencies")
async def add_dependency_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -992,10 +945,8 @@ async def add_dependency_v2(page_id: int, request: Request, authorization: str |
@router.delete("/pages/{page_id}/dependencies/{dep_id}")
async def remove_dependency_v2(page_id: int, dep_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
conn.execute("DELETE FROM page_dependencies WHERE page_id=? AND dependency_id=?", (page_id, dep_id))
conn.commit()
@@ -1016,10 +967,8 @@ async def list_properties_v2(collection_id: int, request: Request, authorization
@router.post("/collections/{collection_id}/properties")
async def create_property_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1043,10 +992,8 @@ async def create_property_v2(collection_id: int, request: Request, authorization
@router.patch("/properties/{prop_id}")
async def patch_property_v2(prop_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1069,10 +1016,8 @@ async def patch_property_v2(prop_id: int, request: Request, authorization: str |
@router.delete("/properties/{prop_id}")
async def delete_property_v2(prop_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
if not conn.execute("SELECT id FROM collection_properties WHERE id=?", (prop_id,)).fetchone():
raise HTTPException(404, "Property not found")
@@ -1083,10 +1028,8 @@ async def delete_property_v2(prop_id: int, request: Request, authorization: str
@router.post("/properties/{prop_id}/relation")
async def create_relation_v2(prop_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1158,10 +1101,8 @@ async def list_views_v2(collection_id: int, request: Request, authorization: str
@router.post("/collections/{collection_id}/views")
async def create_view_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1185,10 +1126,8 @@ async def create_view_v2(collection_id: int, request: Request, authorization: st
@router.patch("/views/{view_id}")
async def patch_view_v2(view_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1218,10 +1157,8 @@ async def patch_view_v2(view_id: int, request: Request, authorization: str | Non
@router.delete("/views/{view_id}")
async def delete_view_v2(view_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
if not conn.execute("SELECT id FROM collection_views WHERE id=?", (view_id,)).fetchone():
raise HTTPException(404, "View not found")
@@ -1232,10 +1169,8 @@ async def delete_view_v2(view_id: int, request: Request, authorization: str | No
@router.post("/views/{view_id}/save-as")
async def save_as_view_v2(view_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1261,10 +1196,8 @@ async def list_dashboards_v2(collection_id: int, request: Request, authorization
@router.post("/collections/{collection_id}/dashboards")
async def create_dashboard_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1279,10 +1212,8 @@ async def create_dashboard_v2(collection_id: int, request: Request, authorizatio
@router.patch("/dashboards/{dashboard_id}")
async def patch_dashboard_v2(dashboard_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1303,10 +1234,8 @@ async def patch_dashboard_v2(dashboard_id: int, request: Request, authorization:
@router.delete("/dashboards/{dashboard_id}")
async def delete_dashboard_v2(dashboard_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
conn.execute("DELETE FROM collection_dashboards WHERE id=?", (dashboard_id,))
conn.commit()
@@ -1326,10 +1255,8 @@ async def list_comments_v2(page_id: int, request: Request, authorization: str |
@router.post("/pages/{page_id}/comments")
async def create_comment_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1351,10 +1278,8 @@ async def create_comment_v2(page_id: int, request: Request, authorization: str |
@router.patch("/comments/{comment_id}")
async def patch_comment_v2(comment_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1379,10 +1304,8 @@ async def patch_comment_v2(comment_id: int, request: Request, authorization: str
@router.delete("/comments/{comment_id}")
async def delete_comment_v2(comment_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
row = conn.execute("SELECT * FROM comments WHERE id=?", (comment_id,)).fetchone()
if not row:
@@ -1395,10 +1318,8 @@ async def delete_comment_v2(comment_id: int, request: Request, authorization: st
@router.post("/pages/{page_id}/mentions")
async def create_mention_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1498,10 +1419,8 @@ async def list_favorites_v2(request: Request, authorization: str | None = Header
@router.post("/favorites")
async def add_favorite_v2(request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1523,10 +1442,8 @@ async def add_favorite_v2(request: Request, authorization: str | None = Header(d
@router.delete("/favorites/{page_id}")
async def remove_favorite_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
conn.execute("DELETE FROM favorites WHERE user_id=? AND page_id=?", (user["id"], page_id))
conn.commit()
@@ -1550,10 +1467,8 @@ async def list_tags_v2(request: Request, authorization: str | None = Header(defa
@router.post("/tags")
async def create_tag_v2(request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1573,10 +1488,8 @@ async def create_tag_v2(request: Request, authorization: str | None = Header(def
@router.patch("/tags/{tag_id}")
async def patch_tag_v2(tag_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1593,10 +1506,8 @@ async def patch_tag_v2(tag_id: int, request: Request, authorization: str | None
@router.delete("/tags/{tag_id}")
async def delete_tag_v2(tag_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
conn.execute("DELETE FROM tags WHERE id=? AND user_id=?", (tag_id, user["id"]))
conn.commit()
@@ -1604,10 +1515,8 @@ async def delete_tag_v2(tag_id: int, request: Request, authorization: str | None
@router.post("/pages/{page_id}/tags")
async def attach_tag_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1625,10 +1534,8 @@ async def attach_tag_v2(page_id: int, request: Request, authorization: str | Non
@router.delete("/pages/{page_id}/tags/{tag_id}")
async def detach_tag_v2(page_id: int, tag_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
conn.execute("DELETE FROM page_tags WHERE page_id=? AND tag_id=?", (page_id, tag_id))
conn.commit()
@@ -1659,10 +1566,8 @@ async def list_shares_v2(page_id: int, request: Request, authorization: str | No
@router.post("/pages/{page_id}/shares")
async def create_share_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1688,10 +1593,8 @@ async def create_share_v2(page_id: int, request: Request, authorization: str | N
@router.patch("/shares/{share_id}")
async def patch_share_v2(share_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1708,10 +1611,8 @@ async def patch_share_v2(share_id: int, request: Request, authorization: str | N
@router.delete("/shares/{share_id}")
async def delete_share_v2(share_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
row = conn.execute("SELECT page_id FROM page_shares WHERE id=?", (share_id,)).fetchone()
if not row:
@@ -1726,10 +1627,8 @@ async def delete_share_v2(share_id: int, request: Request, authorization: str |
@router.post("/pages/{page_id}/publish")
async def publish_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1749,10 +1648,8 @@ async def publish_page_v2(page_id: int, request: Request, authorization: str | N
@router.delete("/pages/{page_id}/publish")
async def unpublish_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
conn.execute("UPDATE pages SET is_published=0 WHERE id=?", (page_id,))
conn.commit()
@@ -1776,10 +1673,8 @@ async def list_history_v2(page_id: int, request: Request, authorization: str | N
@router.post("/pages/{page_id}/history/restore")
async def restore_history_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1820,10 +1715,8 @@ async def list_sprints_v2(collection_id: int, request: Request, authorization: s
@router.post("/collections/{collection_id}/sprints")
async def create_sprint_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1845,10 +1738,8 @@ async def create_sprint_v2(collection_id: int, request: Request, authorization:
@router.patch("/sprints/{sprint_id}")
async def patch_sprint_v2(sprint_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1872,10 +1763,8 @@ async def patch_sprint_v2(sprint_id: int, request: Request, authorization: str |
@router.delete("/sprints/{sprint_id}")
async def delete_sprint_v2(sprint_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
conn.execute("DELETE FROM sprints WHERE id=?", (sprint_id,))
conn.commit()
@@ -1883,10 +1772,8 @@ async def delete_sprint_v2(sprint_id: int, request: Request, authorization: str
@router.post("/sprints/{sprint_id}/assign")
async def assign_sprint_v2(sprint_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1904,10 +1791,8 @@ async def assign_sprint_v2(sprint_id: int, request: Request, authorization: str
@router.delete("/sprints/{sprint_id}/assign/{page_id}")
async def unassign_sprint_v2(sprint_id: int, page_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
conn.execute("DELETE FROM sprint_pages WHERE sprint_id=? AND page_id=?", (sprint_id, page_id))
conn.commit()
@@ -1951,10 +1836,8 @@ async def list_templates_v2(collection_id: int, request: Request, authorization:
@router.post("/collections/{collection_id}/templates")
async def create_template_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1970,10 +1853,8 @@ async def create_template_v2(collection_id: int, request: Request, authorization
@router.patch("/templates/{template_id}")
async def patch_template_v2(template_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -1991,10 +1872,8 @@ async def patch_template_v2(template_id: int, request: Request, authorization: s
@router.delete("/templates/{template_id}")
async def delete_template_v2(template_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
conn.execute("DELETE FROM page_templates WHERE id=?", (template_id,))
conn.commit()
@@ -2002,10 +1881,8 @@ async def delete_template_v2(template_id: int, request: Request, authorization:
@router.post("/templates/{template_id}/apply")
async def apply_template_v2(template_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
tpl = conn.execute("SELECT * FROM page_templates WHERE id=?", (template_id,)).fetchone()
if not tpl:
@@ -2027,10 +1904,8 @@ async def list_db_templates_v2(request: Request, authorization: str | None = Hea
@router.post("/templates/database/{template_id}/apply")
async def apply_db_template_v2(template_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -2104,10 +1979,8 @@ async def export_csv_v2(collection_id: int, request: Request, authorization: str
@router.post("/collections/{collection_id}/import/csv")
async def import_csv_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
form = await request.form()
file = form.get("file")
@@ -2283,10 +2156,8 @@ async def list_webhooks_v2(request: Request, authorization: str | None = Header(
@router.post("/webhooks")
async def create_webhook_v2(request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -2313,10 +2184,8 @@ async def create_webhook_v2(request: Request, authorization: str | None = Header
@router.patch("/webhooks/{webhook_id}")
async def patch_webhook_v2(webhook_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
try:
body = await request.json()
except Exception:
@@ -2336,10 +2205,8 @@ async def patch_webhook_v2(webhook_id: int, request: Request, authorization: str
@router.delete("/webhooks/{webhook_id}")
async def delete_webhook_v2(webhook_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
conn.execute("DELETE FROM webhook_subscriptions WHERE id=?", (webhook_id,))
conn.commit()
@@ -2348,10 +2215,8 @@ async def delete_webhook_v2(webhook_id: int, request: Request, authorization: st
@router.post("/webhooks/{webhook_id}/test")
async def test_webhook_v2(webhook_id: int, request: Request, authorization: str | None = Header(default=None)):
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
with get_conn() as conn:
row = conn.execute("SELECT * FROM webhook_subscriptions WHERE id=?", (webhook_id,)).fetchone()
if not row:
@@ -2399,10 +2264,8 @@ async def list_deliveries_v2(webhook_id: int, request: Request,
async def retry_webhook_deliveries(webhook_id: int, request: Request,
authorization: str | None = Header(default=None)):
"""Manually retry failed deliveries for a webhook."""
user = get_bearer_user(request, authorization)
user = require_scope("write")(request, authorization)
_v2_rate_check(request, user)
if not has_scope(user.get("_token_scopes"), "write"):
raise HTTPException(403, "Insufficient scope. Required: write")
from app.services.webhook_outbound import retry_due_deliveries