"""FlowDeck — Public API v2 : projects. 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, 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.get("/projects") def list_projects_v2(request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: rows = conn.execute("SELECT * FROM projects ORDER BY proj_type, owner, name").fetchall() return {"projects": [row_to_dict(r) for r in rows]} @router.get("/projects/{owner}/{repo}/tree") async def project_tree_v2(owner: str, repo: str, request: Request, path: str = "", authorization: str | None = Header(default=None)): get_bearer_user(request, authorization) # proxy to gitea client? Return placeholder listing from projects table with get_conn() as conn: proj = conn.execute("SELECT * FROM projects WHERE owner=? AND name=?", (owner, repo)).fetchone() if not proj: raise HTTPException(404, "Project not found") # delegate to gitea API if available (best-effort) try: from app.services.gitea_client import gitea tree = await gitea.list_repo_files(owner, repo, path or "") return {"owner": owner, "repo": repo, "path": path, "tree": tree} except Exception: return {"owner": owner, "repo": repo, "path": path, "tree": []} @router.get("/search") def search_v2(request: Request, query: str = "", workspace_id: int | None = None, type: str = "all", authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) q = (query or request.query_params.get("query") or "").strip() if not q: return {"results": [], "query": q} like = f"%{q}%" with get_conn() as conn: pages = [] # try FTS5 try: rows = conn.execute("SELECT p.id, p.title, p.content, p.workspace_id, snippet(pages_fts, -1, '', '', '...', 32) as snippet FROM pages_fts f JOIN pages p ON p.id=f.rowid WHERE pages_fts MATCH ? LIMIT 20", (q,)).fetchall() pages = [{"type": "page", "id": r["id"], "title": r["title"], "snippet": r["snippet"]} for r in rows] except Exception: rows = conn.execute("SELECT id, title FROM pages WHERE title LIKE ? OR content LIKE ? LIMIT 20", (like, like)).fetchall() pages = [{"type": "page", "id": r["id"], "title": r["title"]} for r in rows] # collections colls = conn.execute("SELECT id, name FROM collections WHERE name LIKE ? LIMIT 10", (like,)).fetchall() results = pages + [{"type": "collection", "id": r["id"], "title": r["name"]} for r in colls] return {"query": q, "results": results} @router.get("/admin/users") def admin_list_users_v2(request: Request, limit: int = 30, offset: int = 0, 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, 100)) with get_conn() as conn: total = conn.execute("SELECT COUNT(*) FROM users").fetchone()[0] rows = conn.execute("SELECT id, login, full_name, email, is_admin, is_active, created_at FROM users ORDER BY id LIMIT ? OFFSET ?", (limit, offset)).fetchall() return {"users": [row_to_dict(r) for r in rows], "total": total, "limit": limit, "offset": offset}