"""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()