web-client/bff/app.py
Markov b89969a7a0
All checks were successful
Deploy Web Client / deploy (push) Successful in 36s
feat: chat panel above kanban, WS client, chat API
2026-02-15 23:07:14 +01:00

200 lines
6.1 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/")
# --- Proxy: Chats ---
@app.get("/api/v1/chats/lobby")
async def get_lobby(user: dict = Depends(get_current_user)):
return await tracker_get("/api/v1/chats/lobby")
@app.get("/api/v1/chats/project/{project_id}")
async def get_project_chat(project_id: str, user: dict = Depends(get_current_user)):
return await tracker_get(f"/api/v1/chats/project/{project_id}")
@app.get("/api/v1/chats/{chat_id}/messages")
async def list_messages(chat_id: str, limit: int = 50, before: str = None, user: dict = Depends(get_current_user)):
params = {"limit": limit}
if before:
params["before"] = before
return await tracker_get(f"/api/v1/chats/{chat_id}/messages", params=params)
@app.post("/api/v1/chats/{chat_id}/messages")
async def create_message(chat_id: str, request: Request, user: dict = Depends(get_current_user)):
body = await request.json()
# Inject sender info from auth
body.setdefault("sender_type", "human")
body.setdefault("sender_name", user.get("name", "human"))
return await tracker_post(f"/api/v1/chats/{chat_id}/messages", json=body)
# --- 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()