- GitHub integration row ajoutée (🦎 Gitea + 🐙 GitHub) - Badge 'Primary' sur le provider principal (auth_method) - Disconnect masqué pour le provider principal - authMethod, githubLinked, giteaLinked dans Alpine data - loadGithubStatus() + disconnectGithub() methods - Matrice respectée: local→déco OK, gitea→déco github OK, github→déco gitea OK
250 lines
7.4 KiB
Python
250 lines
7.4 KiB
Python
"""FlowDeck — Library API: recents, favorites, shared, published, private, workspace."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Request, Query
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from app.db import get_conn
|
|
from app.auth.session import SessionManager
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(tags=["library"], prefix="/api/library")
|
|
|
|
SOURCE_TYPES = {"all", "local", "gitea", "github"}
|
|
|
|
|
|
def _get_user_id(request: Request) -> int:
|
|
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
|
return user["id"] if user and user.get("id") else 1
|
|
|
|
|
|
def _detect_source_type(workspace: str) -> str:
|
|
"""Detect source type from workspace field.
|
|
|
|
- '' or plain name (no slash) → local
|
|
- 'owner/repo' format → gitea
|
|
- 'github:owner/repo' format → github
|
|
"""
|
|
ws = (workspace or "").strip()
|
|
if not ws:
|
|
return "local"
|
|
if ws.startswith("github:"):
|
|
return "github"
|
|
if "/" in ws:
|
|
return "gitea"
|
|
return "local"
|
|
|
|
|
|
def _source_badge(source_type: str) -> str:
|
|
return {"local": "Local", "gitea": "Gitea", "github": "GitHub"}.get(source_type, "Local")
|
|
|
|
|
|
def _build_item(db_row: dict) -> dict:
|
|
"""Convert a DB row into a library item dict."""
|
|
workspace = db_row.get("workspace", "") or ""
|
|
source_type = _detect_source_type(workspace)
|
|
# Extract workspace display name
|
|
if source_type == "gitea":
|
|
workspace_name = workspace # "owner/repo"
|
|
elif source_type == "github":
|
|
workspace_name = workspace.replace("github:", "", 1)
|
|
else:
|
|
workspace_name = workspace or "Private"
|
|
|
|
# URL construction
|
|
page_id = db_row["id"]
|
|
if source_type == "gitea":
|
|
parts = workspace.split("/", 1)
|
|
owner = parts[0] if len(parts) > 0 else ""
|
|
repo = parts[1] if len(parts) > 1 else ""
|
|
url = f"/gitea-workspace?owner={owner}&repo={repo}"
|
|
elif source_type == "github":
|
|
gh_ws = workspace.replace("github:", "", 1)
|
|
parts = gh_ws.split("/", 1)
|
|
owner = parts[0] if len(parts) > 0 else ""
|
|
repo = parts[1] if len(parts) > 1 else ""
|
|
url = f"/gitea-workspace?owner={owner}&repo={repo}"
|
|
else:
|
|
url = f"/pages/{page_id}"
|
|
|
|
return {
|
|
"id": page_id,
|
|
"title": db_row.get("title") or "Untitled",
|
|
"icon": "📄",
|
|
"source_type": source_type,
|
|
"source_badge": _source_badge(source_type),
|
|
"workspace_name": workspace_name,
|
|
"updated_at": db_row.get("updated_at", ""),
|
|
"url": url,
|
|
}
|
|
|
|
|
|
def _apply_source_filter(query: str, params: list, source_type: str) -> tuple[str, list]:
|
|
"""Add WHERE clause for source_type filter. Mutates params in place."""
|
|
if source_type == "local":
|
|
query += " AND (workspace = '' OR workspace NOT LIKE '%/%' AND workspace NOT LIKE 'github:%')"
|
|
elif source_type == "gitea":
|
|
query += " AND workspace LIKE '%/%' AND workspace NOT LIKE 'github:%'"
|
|
elif source_type == "github":
|
|
query += " AND workspace LIKE 'github:%'"
|
|
# 'all' → no filter
|
|
return query, params
|
|
|
|
|
|
# ── Tab endpoints ──
|
|
|
|
|
|
@router.get("/recents")
|
|
async def library_recents(
|
|
request: Request,
|
|
source_type: str = Query(default="all"),
|
|
):
|
|
"""Recent pages ordered by updated_at DESC."""
|
|
if source_type not in SOURCE_TYPES:
|
|
source_type = "all"
|
|
|
|
uid = _get_user_id(request)
|
|
query = (
|
|
"SELECT id, title, workspace, updated_at FROM pages "
|
|
"WHERE parent_section != 'Trash' AND deleted_at IS NULL"
|
|
)
|
|
params: list = []
|
|
|
|
query, params = _apply_source_filter(query, params, source_type)
|
|
query += " ORDER BY updated_at DESC LIMIT 50"
|
|
|
|
with get_conn() as conn:
|
|
rows = conn.execute(query, params).fetchall()
|
|
|
|
items = [_build_item(dict(r)) for r in rows]
|
|
return {"items": items}
|
|
|
|
|
|
@router.get("/favorites")
|
|
async def library_favorites(
|
|
request: Request,
|
|
source_type: str = Query(default="all"),
|
|
):
|
|
"""Favorite pages for the current user."""
|
|
if source_type not in SOURCE_TYPES:
|
|
source_type = "all"
|
|
|
|
uid = _get_user_id(request)
|
|
query = (
|
|
"SELECT p.id, p.title, p.workspace, p.updated_at FROM favorites f "
|
|
"JOIN pages p ON p.id = f.page_id "
|
|
"WHERE f.user_id = ? AND p.parent_section != 'Trash' AND p.deleted_at IS NULL"
|
|
)
|
|
params: list = [uid]
|
|
|
|
query, params = _apply_source_filter(query, params, source_type)
|
|
query += " ORDER BY f.position"
|
|
|
|
with get_conn() as conn:
|
|
rows = conn.execute(query, params).fetchall()
|
|
|
|
items = [_build_item(dict(r)) for r in rows]
|
|
return {"items": items}
|
|
|
|
|
|
@router.get("/shared")
|
|
async def library_shared(
|
|
request: Request,
|
|
source_type: str = Query(default="all"),
|
|
):
|
|
"""Pages shared with others (share_mode != 'private')."""
|
|
if source_type not in SOURCE_TYPES:
|
|
source_type = "all"
|
|
|
|
query = (
|
|
"SELECT id, title, workspace, updated_at FROM pages "
|
|
"WHERE share_mode != 'private' AND parent_section != 'Trash' AND deleted_at IS NULL"
|
|
)
|
|
params: list = []
|
|
|
|
query, params = _apply_source_filter(query, params, source_type)
|
|
query += " ORDER BY updated_at DESC LIMIT 50"
|
|
|
|
with get_conn() as conn:
|
|
rows = conn.execute(query, params).fetchall()
|
|
|
|
items = [_build_item(dict(r)) for r in rows]
|
|
return {"items": items}
|
|
|
|
|
|
@router.get("/published")
|
|
async def library_published(
|
|
request: Request,
|
|
source_type: str = Query(default="all"),
|
|
):
|
|
"""Published pages (published = 1)."""
|
|
if source_type not in SOURCE_TYPES:
|
|
source_type = "all"
|
|
|
|
query = (
|
|
"SELECT id, title, workspace, updated_at FROM pages "
|
|
"WHERE published = 1 AND parent_section != 'Trash' AND deleted_at IS NULL"
|
|
)
|
|
params: list = []
|
|
|
|
query, params = _apply_source_filter(query, params, source_type)
|
|
query += " ORDER BY updated_at DESC LIMIT 50"
|
|
|
|
with get_conn() as conn:
|
|
rows = conn.execute(query, params).fetchall()
|
|
|
|
items = [_build_item(dict(r)) for r in rows]
|
|
return {"items": items}
|
|
|
|
|
|
@router.get("/private")
|
|
async def library_private(
|
|
request: Request,
|
|
source_type: str = Query(default="all"),
|
|
):
|
|
"""Private pages (parent_section = 'Private')."""
|
|
if source_type not in SOURCE_TYPES:
|
|
source_type = "all"
|
|
|
|
query = (
|
|
"SELECT id, title, workspace, updated_at FROM pages "
|
|
"WHERE parent_section = 'Private' AND deleted_at IS NULL"
|
|
)
|
|
params: list = []
|
|
|
|
query, params = _apply_source_filter(query, params, source_type)
|
|
query += " ORDER BY updated_at DESC LIMIT 50"
|
|
|
|
with get_conn() as conn:
|
|
rows = conn.execute(query, params).fetchall()
|
|
|
|
items = [_build_item(dict(r)) for r in rows]
|
|
return {"items": items}
|
|
|
|
|
|
@router.get("/workspace")
|
|
async def library_workspace(
|
|
request: Request,
|
|
source_type: str = Query(default="all"),
|
|
):
|
|
"""Pages from workspaces (non-empty workspace field)."""
|
|
if source_type not in SOURCE_TYPES:
|
|
source_type = "all"
|
|
|
|
query = (
|
|
"SELECT id, title, workspace, updated_at FROM pages "
|
|
"WHERE workspace != '' AND parent_section != 'Trash' AND deleted_at IS NULL"
|
|
)
|
|
params: list = []
|
|
|
|
query, params = _apply_source_filter(query, params, source_type)
|
|
query += " ORDER BY updated_at DESC LIMIT 50"
|
|
|
|
with get_conn() as conn:
|
|
rows = conn.execute(query, params).fetchall()
|
|
|
|
items = [_build_item(dict(r)) for r in rows]
|
|
return {"items": items}
|