"""FlowDeck — Public API router (v2.1.0).""" from __future__ import annotations import json import logging from secrets import token_urlsafe from fastapi import APIRouter, Request, HTTPException, Header, Depends from app.db import get_conn logger = logging.getLogger(__name__) router = APIRouter(tags=["public-api"], prefix="/api/v1") DEFAULT_TOKEN = "fd-public-key" def verify_token(authorization: str | None = Header(None)): if not authorization or not authorization.startswith("Bearer "): raise HTTPException(401, "API token required. Generate one via POST /api/v1/token.") token = authorization[7:] # strip "Bearer " if token == DEFAULT_TOKEN: return token with get_conn() as conn: row = conn.execute("SELECT 1 FROM user_tokens WHERE gitea_token=?", (token,)).fetchone() if not row: raise HTTPException(403, "Invalid API token") return token @router.post("/token") async def generate_token(request: Request): """Generate a public API access token.""" token = f"fd_{token_urlsafe(24)}" with get_conn() as conn: conn.execute( "INSERT OR REPLACE INTO user_tokens (gitea_user_id, gitea_token, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)", (0, token), ) conn.commit() return {"token": token, "note": "Use as: Authorization: Bearer "} @router.get("/collections", dependencies=[Depends(verify_token)]) async def public_list_collections(request: Request): with get_conn() as conn: rows = conn.execute("SELECT id, name, description, icon, created_at FROM collections ORDER BY name").fetchall() return {"collections": [dict(r) for r in rows]} @router.get("/collections/{collection_id}", dependencies=[Depends(verify_token)]) async def public_get_collection(request: Request, collection_id: int): with get_conn() as conn: coll = conn.execute("SELECT * FROM collections WHERE id=?", (collection_id,)).fetchone() if not coll: raise HTTPException(404, "Not found") pages = conn.execute( "SELECT id, title, icon, position, property_values_json, created_at, updated_at FROM collection_pages WHERE collection_id=? ORDER BY position", (collection_id,), ).fetchall() return {"collection": dict(coll), "pages": [dict(p) for p in pages]} @router.get("/collections/{collection_id}/pages", dependencies=[Depends(verify_token)]) async def public_list_pages(request: Request, collection_id: int): with get_conn() as conn: pages = conn.execute( "SELECT id, title, icon, position, property_values_json, created_at FROM collection_pages WHERE collection_id=? AND parent_id IS NULL ORDER BY position", (collection_id,), ).fetchall() return {"pages": [dict(p) for p in pages]} @router.get("/pages/{page_id}", dependencies=[Depends(verify_token)]) async def public_get_page(request: Request, page_id: int): with get_conn() as conn: p = conn.execute("SELECT * FROM collection_pages WHERE id=?", (page_id,)).fetchone() if not p: raise HTTPException(404, "Not found") return dict(p) @router.get("/my-tasks", dependencies=[Depends(verify_token)]) async def public_my_tasks(request: Request): """Public API: list tasks (requires valid token).""" with get_conn() as conn: pages = conn.execute( "SELECT id, title, icon, property_values_json FROM collection_pages WHERE parent_id IS NULL ORDER BY created_at DESC LIMIT 50" ).fetchall() return {"tasks": [dict(p) for p in pages]}