Files
flowdeck/app/routers/api_v2/admin.py
T
bruno 6a5fe0524a
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
refactor: A28 lot 1 — api_v2.py (2 110 L) → package 14 fichiers (v7.29.0)
Découpe par concern de l'ancien app/routers/api_v2.py (2 110 lignes,
115 routes) en package `app/routers/api_v2/` :

- 12 modules de routes : collections 566 L (23 r.), engagement 338 (21),
  workspaces 230 (9), templates_io 205 (9), webhooks 195 (8),
  identity 195 (7), views 164 (8), sharing 160 (8), properties 151 (7),
  planning 148 (7), projects 93 (4), admin 91 (4)
- `_common.py` : helpers partagés (_hash, _v2_rate_check)
- `__init__.py` : router = APIRouter(prefix="/api/v2") + include_router
  sur les routers de sections (sans prefix, tags « api-v2 »)

Preuve contractuelle : `docs/openapi-v2.json` régénéré = IDENTIQUE
byte-à-byte (0 changement de chemin/tag/operation_id). Seul importateur
(app/main.py : from app.routers.api_v2 import router) fonctionne via le
package. En-tête d'imports copié par module puis émondé par ruff --fix
(143 imports morts), I001 réordonnés.

Reste A28 : dashboard.py 2 735 L, collections.py 2 622 L, board.py 2 101 L
(même recette, lots suivants).

suite **1091/1091** · ruff OK · OpenAPI 509 identique · docs à jour
2026-10-01 23:26:45 -04:00

92 lines
3.6 KiB
Python

"""FlowDeck — Public API v2 : admin.
Découpe A28 de l'ancien app/routers/api_v2.py (2 110 lignes, 115 routes) — un module par concern, contrat inchangé (Bearer+scopes, pagination, RFC7807, audit + idempotency).
"""
from __future__ import annotations
import logging
from fastapi import APIRouter, Body, Header, HTTPException, Request
from app.db import get_conn
from app.services.api_v2_helpers import ( # noqa: F401 — require_scope est utilisé par les handlers
audit_log,
check_idempotency,
check_v2_rate_limit,
get_bearer_user,
has_scope,
paginate_headers,
parse_pagination,
require_scope,
row_to_dict,
store_idempotency,
to_iso8601,
validate_scopes_input,
)
from ._common import _v2_rate_check
logger = logging.getLogger(__name__)
router = APIRouter(tags=["api-v2"])
@router.patch("/admin/users/{uid}")
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")
with get_conn() as conn:
if not conn.execute("SELECT id FROM users WHERE id=?", (uid,)).fetchone():
raise HTTPException(404, "User not found")
sets = []
params: list = []
for k in ("is_active", "is_admin", "full_name", "email"):
if k in body:
sets.append(f"{k}=?")
params.append(int(body[k]) if k in ("is_active", "is_admin") else body[k])
if not sets:
raise HTTPException(400, "No fields")
params.append(uid)
conn.execute(f"UPDATE users SET {', '.join(sets)} WHERE id=?", params)
conn.commit()
audit_log(user, "admin.user_update", "user", uid, "", request)
return {"id": uid, "status": "updated"}
@router.delete("/admin/users/{uid}")
def admin_delete_user_v2(uid: int, request: Request, authorization: str | None = Header(default=None)):
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")
if uid == user["id"]:
raise HTTPException(400, "Cannot delete yourself")
with get_conn() as conn:
conn.execute("DELETE FROM users WHERE id=?", (uid,))
conn.commit()
audit_log(user, "admin.user_delete", "user", uid, "", request)
return {"id": uid, "status": "deleted"}
@router.get("/admin/audit-logs")
def admin_audit_logs_v2(request: Request, limit: int = 50, authorization: str | None = Header(default=None)):
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")
limit = max(1, min(limit, 200))
with get_conn() as conn:
rows = conn.execute("SELECT * FROM api_audit_log ORDER BY created_at DESC LIMIT ?", (limit,)).fetchall()
return {"logs": [row_to_dict(r) for r in rows]}
@router.get("/webhooks/events")
def list_webhook_events_v2(request: Request, authorization: str | None = Header(default=None)):
"""Catalogue of deliverable events (+ wildcard syntax)."""
user = get_bearer_user(request, authorization)
_v2_rate_check(request, user)
from app.services.webhook_outbound import EVENTS
return {"events": EVENTS, "wildcards": ["*", "page.*", "collection.*"]}