feat(api): GET /api/deployment + /api/projects/:id (§22.9 runtime config)
Adds two read endpoints that replace the build-time VITE_APP_NAME with a runtime-config surface: /api/deployment returns deployment name/tagline plus the projects visible to the caller (gated filtered by membership, unlisted omitted from enumeration per §22.5); /api/projects/:id returns one project's config and optional theme overlay, gated behind the §22.5 read gate. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
"""§22.9 runtime deployment/project config (replaces VITE_APP_NAME).
|
||||
|
||||
GET /api/deployment — the deployment name/tagline + the projects the caller can
|
||||
see (§22.5: gated filtered by membership, unlisted omitted from enumeration).
|
||||
GET /api/projects/:id — one project's runtime config + optional theme overlay,
|
||||
gated behind the §22.5 read gate (404 for a non-member of a gated project).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from . import auth, db
|
||||
|
||||
|
||||
def make_router() -> APIRouter:
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/api/deployment")
|
||||
async def get_deployment(request: Request) -> dict[str, Any]:
|
||||
viewer = auth.current_user(request)
|
||||
dep = db.conn().execute(
|
||||
"SELECT name, tagline FROM deployment WHERE id = 1"
|
||||
).fetchone()
|
||||
# §22.5: enumerate only public + (member-)gated; unlisted is never listed.
|
||||
visible = set(auth.visible_project_ids(viewer))
|
||||
rows = db.conn().execute(
|
||||
"SELECT id, name, type, visibility FROM projects "
|
||||
"WHERE visibility != 'unlisted' ORDER BY name"
|
||||
).fetchall()
|
||||
projects = [
|
||||
{"id": r["id"], "name": r["name"], "type": r["type"], "visibility": r["visibility"]}
|
||||
for r in rows
|
||||
if r["id"] in visible
|
||||
]
|
||||
return {
|
||||
"name": (dep["name"] if dep else "") or "",
|
||||
"tagline": (dep["tagline"] if dep else "") or "",
|
||||
"projects": projects,
|
||||
}
|
||||
|
||||
@router.get("/api/projects/{project_id}")
|
||||
async def get_project(project_id: str, request: Request) -> dict[str, Any]:
|
||||
viewer = auth.current_user(request)
|
||||
# §22.5 read gate: a gated project 404s to a non-member (shape matches
|
||||
# an unknown id). unlisted is readable by direct id.
|
||||
auth.require_project_readable(viewer, project_id)
|
||||
row = db.conn().execute(
|
||||
"SELECT id, name, type, visibility, initial_state, config_json "
|
||||
"FROM projects WHERE id = ?",
|
||||
(project_id,),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
auth.require_project_readable(viewer, project_id) # raises 404
|
||||
cfg = json.loads(row["config_json"] or "{}")
|
||||
dep = db.conn().execute("SELECT tagline FROM deployment WHERE id = 1").fetchone()
|
||||
return {
|
||||
"id": row["id"],
|
||||
"name": row["name"],
|
||||
"tagline": (dep["tagline"] if dep else "") or "",
|
||||
"type": row["type"],
|
||||
"visibility": row["visibility"],
|
||||
"initial_state": row["initial_state"],
|
||||
"theme": cfg.get("theme") or {},
|
||||
}
|
||||
|
||||
return router
|
||||
Reference in New Issue
Block a user