"""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.*"]}