Some checks failed
Deploy Web Client / deploy (push) Has been cancelled
- BFF on port 8200: auth + proxy to tracker - All /api/* routes go through BFF - WebSocket proxy with JWT auth - Tracker no longer exposed to internet - Logging on all requests - Removed Next.js API routes for auth (BFF handles it)
176 lines
5.2 KiB
Python
176 lines
5.2 KiB
Python
"""BFF — Backend for Frontend. Proxies to Tracker with user auth."""
|
|
|
|
import logging
|
|
import time
|
|
|
|
from fastapi import FastAPI, Request, Depends
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import JSONResponse
|
|
from pydantic import BaseModel
|
|
|
|
from auth import create_token, get_current_user
|
|
from config import AUTH_USER, AUTH_PASS, ENV
|
|
from tracker_client import tracker_get, tracker_post, tracker_patch, tracker_delete, close_client
|
|
from ws_proxy import router as ws_router
|
|
|
|
logging.basicConfig(
|
|
level=logging.DEBUG if ENV == "dev" else logging.INFO,
|
|
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
)
|
|
logger = logging.getLogger("bff")
|
|
|
|
app = FastAPI(title="Team Board BFF", version="0.1.0")
|
|
|
|
# CORS
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["https://team.uix.su", "http://localhost:3100"],
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
|
|
# --- Request logging ---
|
|
|
|
@app.middleware("http")
|
|
async def log_requests(request: Request, call_next):
|
|
start = time.time()
|
|
logger.info("[REQ] %s %s", request.method, request.url.path)
|
|
try:
|
|
response = await call_next(request)
|
|
elapsed = (time.time() - start) * 1000
|
|
logger.info("[RES] %s %s → %d (%.0fms)", request.method, request.url.path, response.status_code, elapsed)
|
|
return response
|
|
except Exception as e:
|
|
logger.error("[ERR] %s %s → %s", request.method, request.url.path, str(e))
|
|
return JSONResponse({"error": str(e)}, status_code=500)
|
|
|
|
|
|
# --- Auth ---
|
|
|
|
class LoginRequest(BaseModel):
|
|
username: str
|
|
password: str
|
|
|
|
|
|
@app.post("/api/auth/login")
|
|
async def login(data: LoginRequest):
|
|
if data.username == AUTH_USER and data.password == AUTH_PASS:
|
|
token = create_token(data.username)
|
|
return {"token": token, "user": {"name": data.username, "provider": "local"}}
|
|
return JSONResponse({"error": "Invalid credentials"}, status_code=401)
|
|
|
|
|
|
@app.get("/api/auth/me")
|
|
async def me(user: dict = Depends(get_current_user)):
|
|
return {"user": {"name": user["name"], "provider": user["provider"]}}
|
|
|
|
|
|
# --- Proxy: Projects ---
|
|
|
|
@app.get("/api/v1/projects/")
|
|
async def list_projects(user: dict = Depends(get_current_user)):
|
|
return await tracker_get("/api/v1/projects/")
|
|
|
|
|
|
@app.post("/api/v1/projects/")
|
|
async def create_project(request: Request, user: dict = Depends(get_current_user)):
|
|
body = await request.json()
|
|
return await tracker_post("/api/v1/projects/", json=body)
|
|
|
|
|
|
@app.get("/api/v1/projects/{project_id}")
|
|
async def get_project(project_id: str, user: dict = Depends(get_current_user)):
|
|
return await tracker_get(f"/api/v1/projects/{project_id}")
|
|
|
|
|
|
@app.patch("/api/v1/projects/{project_id}")
|
|
async def update_project(project_id: str, request: Request, user: dict = Depends(get_current_user)):
|
|
body = await request.json()
|
|
return await tracker_patch(f"/api/v1/projects/{project_id}", json=body)
|
|
|
|
|
|
@app.delete("/api/v1/projects/{project_id}")
|
|
async def delete_project(project_id: str, user: dict = Depends(get_current_user)):
|
|
await tracker_delete(f"/api/v1/projects/{project_id}")
|
|
return JSONResponse(status_code=204)
|
|
|
|
|
|
# --- Proxy: Tasks ---
|
|
|
|
@app.get("/api/v1/tasks/")
|
|
async def list_tasks(project_id: str = None, status: str = None, user: dict = Depends(get_current_user)):
|
|
params = {}
|
|
if project_id:
|
|
params["project_id"] = project_id
|
|
if status:
|
|
params["status"] = status
|
|
return await tracker_get("/api/v1/tasks/", params=params)
|
|
|
|
|
|
@app.post("/api/v1/tasks/")
|
|
async def create_task(request: Request, user: dict = Depends(get_current_user)):
|
|
body = await request.json()
|
|
return await tracker_post("/api/v1/tasks/", json=body)
|
|
|
|
|
|
@app.get("/api/v1/tasks/{task_id}")
|
|
async def get_task(task_id: str, user: dict = Depends(get_current_user)):
|
|
return await tracker_get(f"/api/v1/tasks/{task_id}")
|
|
|
|
|
|
@app.patch("/api/v1/tasks/{task_id}")
|
|
async def update_task(task_id: str, request: Request, user: dict = Depends(get_current_user)):
|
|
body = await request.json()
|
|
return await tracker_patch(f"/api/v1/tasks/{task_id}", json=body)
|
|
|
|
|
|
@app.delete("/api/v1/tasks/{task_id}")
|
|
async def delete_task(task_id: str, user: dict = Depends(get_current_user)):
|
|
await tracker_delete(f"/api/v1/tasks/{task_id}")
|
|
return JSONResponse(status_code=204)
|
|
|
|
|
|
# --- Proxy: Agents ---
|
|
|
|
@app.get("/api/v1/agents/")
|
|
async def list_agents(user: dict = Depends(get_current_user)):
|
|
return await tracker_get("/api/v1/agents/")
|
|
|
|
|
|
@app.get("/api/v1/agents/adapters")
|
|
async def list_adapters(user: dict = Depends(get_current_user)):
|
|
return await tracker_get("/api/v1/agents/adapters")
|
|
|
|
|
|
# --- Proxy: Labels ---
|
|
|
|
@app.get("/api/v1/labels/")
|
|
async def list_labels(user: dict = Depends(get_current_user)):
|
|
return await tracker_get("/api/v1/labels/")
|
|
|
|
|
|
@app.post("/api/v1/labels/")
|
|
async def create_label(request: Request, user: dict = Depends(get_current_user)):
|
|
body = await request.json()
|
|
return await tracker_post("/api/v1/labels/", json=body)
|
|
|
|
|
|
# --- Health ---
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
return {"status": "ok", "service": "bff", "version": "0.1.0"}
|
|
|
|
|
|
# --- WebSocket ---
|
|
|
|
app.include_router(ws_router)
|
|
|
|
|
|
# --- Shutdown ---
|
|
|
|
@app.on_event("shutdown")
|
|
async def shutdown():
|
|
await close_client()
|