mirror of
https://github.com/bleak-ai/gcontext.git
synced 2026-08-11 13:19:23 +02:00
Add admin CRUD endpoints for the marketplace dashboard (task 11)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a055826477
commit
061972e91a
@@ -1,8 +1,10 @@
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from .db import init_db
|
||||
from .routes_admin import router as admin_router
|
||||
from .routes_moderation import router as moderation_router
|
||||
from .routes_public import router as public_router
|
||||
|
||||
@@ -14,8 +16,20 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
|
||||
app = FastAPI(title="gcontext workflows API", lifespan=lifespan)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[
|
||||
"https://gcontext.ai",
|
||||
"https://www.gcontext.ai",
|
||||
"http://localhost:3000",
|
||||
"http://localhost:3001",
|
||||
],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
app.include_router(public_router)
|
||||
app.include_router(moderation_router)
|
||||
app.include_router(admin_router)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
|
||||
from .db import get_session
|
||||
from .models import Template
|
||||
from .routes_moderation import require_admin
|
||||
from .schemas import AdminUpdateIn, AdminWorkflowOut
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/admin", tags=["admin"], dependencies=[Depends(require_admin)]
|
||||
)
|
||||
|
||||
|
||||
def _to_out(t: Template) -> AdminWorkflowOut:
|
||||
return AdminWorkflowOut(
|
||||
id=t.id,
|
||||
name=t.name,
|
||||
description=t.description,
|
||||
tags=t.tags,
|
||||
status=t.status,
|
||||
submitted_at=t.submitted_at,
|
||||
reviewed_at=t.reviewed_at,
|
||||
file_count=len(t.files),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/workflows", response_model=list[AdminWorkflowOut])
|
||||
def list_all(session: Session = Depends(get_session)):
|
||||
rows = session.scalars(
|
||||
select(Template).options(selectinload(Template.files)).order_by(Template.id)
|
||||
).all()
|
||||
return [_to_out(t) for t in rows]
|
||||
|
||||
|
||||
@router.patch("/workflows/{workflow_id}", response_model=AdminWorkflowOut)
|
||||
def update_metadata(
|
||||
workflow_id: str, body: AdminUpdateIn, session: Session = Depends(get_session)
|
||||
):
|
||||
template = session.scalars(
|
||||
select(Template)
|
||||
.where(Template.id == workflow_id)
|
||||
.options(selectinload(Template.files))
|
||||
).first()
|
||||
if template is None:
|
||||
raise HTTPException(status_code=404, detail="workflow not found")
|
||||
if body.name is not None:
|
||||
template.name = body.name
|
||||
if body.description is not None:
|
||||
template.description = body.description
|
||||
if body.tags is not None:
|
||||
template.tags = body.tags
|
||||
session.commit()
|
||||
session.refresh(template)
|
||||
return _to_out(template)
|
||||
|
||||
|
||||
@router.delete("/workflows/{workflow_id}")
|
||||
def delete_workflow(workflow_id: str, session: Session = Depends(get_session)):
|
||||
template = session.scalars(
|
||||
select(Template).where(Template.id == workflow_id)
|
||||
).first()
|
||||
if template is None:
|
||||
raise HTTPException(status_code=404, detail="workflow not found")
|
||||
session.delete(template)
|
||||
session.commit()
|
||||
return {"deleted": workflow_id}
|
||||
+26
-1
@@ -1,4 +1,6 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, model_validator
|
||||
|
||||
|
||||
class FileIn(BaseModel):
|
||||
@@ -23,3 +25,26 @@ class SubmitOut(ManifestOut):
|
||||
|
||||
class TemplateOut(ManifestOut):
|
||||
files: list[FileIn]
|
||||
|
||||
|
||||
class AdminWorkflowOut(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
tags: list[str]
|
||||
status: str
|
||||
submitted_at: datetime
|
||||
reviewed_at: datetime | None
|
||||
file_count: int
|
||||
|
||||
|
||||
class AdminUpdateIn(BaseModel):
|
||||
name: str | None = None
|
||||
description: str | None = None
|
||||
tags: list[str] | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def at_least_one_field(self):
|
||||
if self.name is None and self.description is None and self.tags is None:
|
||||
raise ValueError("at least one field must be provided")
|
||||
return self
|
||||
|
||||
@@ -141,6 +141,100 @@ def test_invalid_bundles_rejected(client):
|
||||
assert bad_slug.status_code == 422
|
||||
|
||||
|
||||
def test_admin_list_all_statuses(client, admin):
|
||||
submit(client)
|
||||
client.post("/api/moderation/workflows/demo-flow/approve", headers=admin)
|
||||
alt = INDEX_MD.replace("demo-flow", "alt-flow").replace("Demo Flow", "Alt Flow")
|
||||
client.post("/api/workflows", json=bundle(index_content=alt))
|
||||
client.post("/api/moderation/workflows/alt-flow/reject", headers=admin)
|
||||
|
||||
resp = client.get("/api/admin/workflows", headers=admin)
|
||||
assert resp.status_code == 200
|
||||
items = resp.json()
|
||||
by_id = {w["id"]: w for w in items}
|
||||
assert by_id["demo-flow"]["status"] == "approved"
|
||||
assert by_id["demo-flow"]["file_count"] == 5
|
||||
assert by_id["alt-flow"]["status"] == "rejected"
|
||||
|
||||
|
||||
def test_admin_list_requires_token(client):
|
||||
assert client.get("/api/admin/workflows").status_code == 401
|
||||
bad = {"Authorization": "Bearer wrong"}
|
||||
assert client.get("/api/admin/workflows", headers=bad).status_code == 401
|
||||
|
||||
|
||||
def test_admin_update_metadata(client, admin):
|
||||
submit(client)
|
||||
client.post("/api/moderation/workflows/demo-flow/approve", headers=admin)
|
||||
|
||||
resp = client.patch(
|
||||
"/api/admin/workflows/demo-flow",
|
||||
json={"name": "Updated Name", "tags": ["new"]},
|
||||
headers=admin,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["name"] == "Updated Name"
|
||||
assert resp.json()["tags"] == ["new"]
|
||||
|
||||
public = client.get("/api/workflows/demo-flow").json()
|
||||
assert public["name"] == "Updated Name"
|
||||
assert public["tags"] == ["new"]
|
||||
|
||||
|
||||
def test_admin_update_partial(client, admin):
|
||||
submit(client)
|
||||
client.post("/api/moderation/workflows/demo-flow/approve", headers=admin)
|
||||
|
||||
resp = client.patch(
|
||||
"/api/admin/workflows/demo-flow",
|
||||
json={"name": "New Name"},
|
||||
headers=admin,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["name"] == "New Name"
|
||||
assert resp.json()["description"] == "A demo workflow used by the tests."
|
||||
assert resp.json()["tags"] == ["demo", "testing"]
|
||||
|
||||
|
||||
def test_admin_update_not_found(client, admin):
|
||||
resp = client.patch(
|
||||
"/api/admin/workflows/nonexistent",
|
||||
json={"name": "X"},
|
||||
headers=admin,
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_admin_delete(client, admin):
|
||||
submit(client)
|
||||
client.post("/api/moderation/workflows/demo-flow/approve", headers=admin)
|
||||
|
||||
resp = client.delete("/api/admin/workflows/demo-flow", headers=admin)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["deleted"] == "demo-flow"
|
||||
|
||||
assert client.get("/api/workflows/demo-flow").status_code == 404
|
||||
assert client.get("/api/admin/workflows", headers=admin).json() == []
|
||||
|
||||
|
||||
def test_admin_delete_rejected(client, admin):
|
||||
submit(client)
|
||||
client.post("/api/moderation/workflows/demo-flow/reject", headers=admin)
|
||||
|
||||
resp = client.delete("/api/admin/workflows/demo-flow", headers=admin)
|
||||
assert resp.status_code == 200
|
||||
assert client.get("/api/admin/workflows", headers=admin).json() == []
|
||||
|
||||
|
||||
def test_admin_delete_not_found(client, admin):
|
||||
resp = client.delete("/api/admin/workflows/nonexistent", headers=admin)
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_admin_delete_requires_token(client):
|
||||
assert client.delete("/api/admin/workflows/x").status_code == 401
|
||||
|
||||
|
||||
def test_path_traversal_rejected(client):
|
||||
evil = bundle(extra=[{"path": "../outside.md", "content": "x"}])
|
||||
assert client.post("/api/workflows", json=evil).status_code == 422
|
||||
|
||||
Reference in New Issue
Block a user