Files
flowdeck/app/routers/external_refs.py
T
Spark d8548eede9
FlowDeck CI / lint (push) Failing after 2m33s
FlowDeck CI / test (push) Failing after 30m3s
FlowDeck CI / docker (push) Skipped
feat: Fusion bloc 1 — barre d'apps commune (ACC-1.1, #17) et références croisées external_ref (ACC-1.3, #18)
2026-10-20 23:17:18 -04:00

119 lines
4.4 KiB
Python

"""Endpoints des références croisées — Fusion ACC-1.3 (Flowdeck).
Création par collage d'une URL profonde d'une autre application
(ObsiGate, TikTak, Kanbante, Flowdeck) — zéro couplage serveur :
l'URL est reconnue par les formes figées du contrat
``dracodev-commons/contrats/liens-profonds.md`` (voir
``app.services.external_refs``). La route ``GET /collections/{id}``
honore la forme canonique du même contrat en redirigeant vers la vue
native de la base (``/db/{id}``).
"""
from __future__ import annotations
import logging
from fastapi import APIRouter, Body, HTTPException, Request
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from app.auth.session import SessionManager
from app.db import get_conn
from app.services import external_refs as refs_svc
from app.services.permission_manager import PermissionManager
logger = logging.getLogger("flowdeck.external_refs")
router = APIRouter(tags=["ExternalRefs"])
_FORMES_ACCEPTEES = (
"URL non reconnue — formes acceptées : TikTak ?task= ou ?list=, "
"ObsiGate #file=, Flowdeck /pages/ ou /collections/, "
"issue Gitea (carte Kanbante)"
)
def _user(request: Request) -> dict:
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
if not user or not user.get("id"):
raise HTTPException(401, "Session requise")
return user
def _check(owner_type: str, owner_id: int, user: dict, edit: bool) -> None:
pm = PermissionManager(user["id"])
if owner_type == "flowdeck.page":
ok = pm.can_edit_page(owner_id) if edit else pm.can_view_page(owner_id)
elif owner_type == "flowdeck.project":
ok = pm.can_edit_collection(owner_id) if edit else pm.can_view_collection(owner_id)
else:
raise HTTPException(400, "Type de propriétaire non géré ici")
if not ok:
raise HTTPException(403, "Accès refusé")
def _list(owner_type: str, owner_id: int, request: Request):
user = _user(request)
_check(owner_type, owner_id, user, edit=False)
return {"refs": refs_svc.list_refs(owner_type, owner_id)}
def _add(owner_type: str, owner_id: int, request: Request, payload: dict):
user = _user(request)
_check(owner_type, owner_id, user, edit=True)
parsed = refs_svc.parse_deep_link(payload.get("url") or "")
if parsed is None:
raise HTTPException(422, _FORMES_ACCEPTEES)
target_app, target_type, target_id, default_label = parsed
label = (payload.get("label") or "").strip() or default_label
ref = refs_svc.add_ref(
user["id"], owner_type, owner_id, target_app, target_type, target_id,
(payload.get("url") or "").strip(), label,
)
return JSONResponse(ref, status_code=201)
@router.get("/api/pages/{page_id}/refs")
def list_page_refs(page_id: int, request: Request):
return _list("flowdeck.page", page_id, request)
@router.post("/api/pages/{page_id}/refs")
def add_page_ref(page_id: int, request: Request, payload: dict = Body(...)):
return _add("flowdeck.page", page_id, request, payload)
@router.get("/api/collections/{collection_id}/refs")
def list_collection_refs(collection_id: int, request: Request):
return _list("flowdeck.project", collection_id, request)
@router.post("/api/collections/{collection_id}/refs")
def add_collection_ref(collection_id: int, request: Request, payload: dict = Body(...)):
return _add("flowdeck.project", collection_id, request, payload)
@router.delete("/api/refs/{ref_id}")
def delete_ref(ref_id: int, request: Request):
user = _user(request)
ref = refs_svc.get_ref(ref_id)
if ref is None:
raise HTTPException(404, "Lien introuvable")
_check(ref["owner_type"], int(ref["owner_id"]), user, edit=True)
refs_svc.delete_ref(ref_id)
return {"deleted": ref_id}
@router.get("/collections/{collection_id}")
def view_collection_deep_link(collection_id: int, request: Request):
"""Forme canonique Fusion (ACC-0.1) : /collections/<id> → /db/<id>."""
_user(request)
with get_conn() as conn:
row = conn.execute(
"SELECT id FROM collections WHERE id = ?", (collection_id,)
).fetchone()
if not row:
# Réponse directe, comme view_page : le gestionnaire global de
# l'app convertirait une HTTPException 404 en redirection 302.
return HTMLResponse("<h2>Collection introuvable</h2>", status_code=404)
return RedirectResponse(f"/db/{collection_id}", status_code=303)