web-client/bff/app.py
Markov 32f9b26072
All checks were successful
Deploy Web Client / deploy (push) Successful in 36s
feat: use backend task.key, dev environment (dev.team.uix.su), env-only config
- Task key from backend (TE-1 format)
- BFF config: all from env vars, no defaults for secrets
- CORS: dev.team.uix.su added
- STORAGE_ROOT for file storage
- Build with NEXT_PUBLIC_API_URL=https://dev.team.uix.su
2026-02-22 19:40:00 +01:00

251 lines
8.1 KiB
Python

"""BFF — Backend for Frontend. Proxies to Tracker with user auth."""
import logging
import time
from fastapi import FastAPI, Request, Depends, Query
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.2.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["https://team.uix.su", "https://dev.team.uix.su", "http://localhost:3100"],
allow_methods=["*"],
allow_headers=["*"],
)
@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, "slug": "admin"}}
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"], "slug": user.get("sub", "admin")}}
# --- 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/{slug}")
async def get_project(slug: str, user: dict = Depends(get_current_user)):
return await tracker_get(f"/api/v1/projects/{slug}")
@app.patch("/api/v1/projects/{slug}")
async def update_project(slug: str, request: Request, user: dict = Depends(get_current_user)):
body = await request.json()
return await tracker_patch(f"/api/v1/projects/{slug}", json=body)
@app.delete("/api/v1/projects/{slug}")
async def delete_project(slug: str, user: dict = Depends(get_current_user)):
return await tracker_delete(f"/api/v1/projects/{slug}")
# --- Proxy: Tasks ---
@app.get("/api/v1/tasks")
async def list_tasks(
project_id: str = None, status: str = None,
assignee: str = None, label: str = None,
user: dict = Depends(get_current_user),
):
params = {}
if project_id:
params["project_id"] = project_id
if status:
params["status"] = status
if assignee:
params["assignee"] = assignee
if label:
params["label"] = label
return await tracker_get("/api/v1/tasks", params=params)
@app.post("/api/v1/tasks")
async def create_task(request: Request, project_slug: str = Query(...), user: dict = Depends(get_current_user)):
body = await request.json()
return await tracker_post(f"/api/v1/tasks?project_slug={project_slug}", 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)):
return await tracker_delete(f"/api/v1/tasks/{task_id}")
@app.post("/api/v1/tasks/{task_id}/take")
async def take_task(task_id: str, slug: str = Query(...), user: dict = Depends(get_current_user)):
return await tracker_post(f"/api/v1/tasks/{task_id}/take?slug={slug}")
@app.post("/api/v1/tasks/{task_id}/reject")
async def reject_task(task_id: str, request: Request, user: dict = Depends(get_current_user)):
body = await request.json()
return await tracker_post(f"/api/v1/tasks/{task_id}/reject", json=body)
@app.post("/api/v1/tasks/{task_id}/assign")
async def assign_task(task_id: str, request: Request, user: dict = Depends(get_current_user)):
body = await request.json()
return await tracker_post(f"/api/v1/tasks/{task_id}/assign", json=body)
@app.post("/api/v1/tasks/{task_id}/watch")
async def watch_task(task_id: str, slug: str = Query(...), user: dict = Depends(get_current_user)):
return await tracker_post(f"/api/v1/tasks/{task_id}/watch?slug={slug}")
@app.delete("/api/v1/tasks/{task_id}/watch")
async def unwatch_task(task_id: str, slug: str = Query(...), user: dict = Depends(get_current_user)):
return await tracker_delete(f"/api/v1/tasks/{task_id}/watch?slug={slug}")
# --- Proxy: Steps ---
@app.get("/api/v1/tasks/{task_id}/steps")
async def list_steps(task_id: str, user: dict = Depends(get_current_user)):
return await tracker_get(f"/api/v1/tasks/{task_id}/steps")
@app.post("/api/v1/tasks/{task_id}/steps")
async def create_step(task_id: str, request: Request, user: dict = Depends(get_current_user)):
body = await request.json()
return await tracker_post(f"/api/v1/tasks/{task_id}/steps", json=body)
@app.patch("/api/v1/tasks/{task_id}/steps/{step_id}")
async def update_step(task_id: str, step_id: str, request: Request, user: dict = Depends(get_current_user)):
body = await request.json()
return await tracker_patch(f"/api/v1/tasks/{task_id}/steps/{step_id}", json=body)
@app.delete("/api/v1/tasks/{task_id}/steps/{step_id}")
async def delete_step(task_id: str, step_id: str, user: dict = Depends(get_current_user)):
return await tracker_delete(f"/api/v1/tasks/{task_id}/steps/{step_id}")
# --- Proxy: Messages (unified) ---
@app.get("/api/v1/messages")
async def list_messages(
chat_id: str = None, task_id: str = None,
limit: int = 50, offset: int = 0,
user: dict = Depends(get_current_user),
):
params = {"limit": limit, "offset": offset}
if chat_id:
params["chat_id"] = chat_id
if task_id:
params["task_id"] = task_id
return await tracker_get("/api/v1/messages", params=params)
@app.post("/api/v1/messages")
async def create_message(request: Request, user: dict = Depends(get_current_user)):
body = await request.json()
# Inject author from auth
body.setdefault("author_type", "human")
body.setdefault("author_slug", user.get("sub", "admin"))
return await tracker_post("/api/v1/messages", json=body)
# --- Proxy: Members ---
@app.get("/api/v1/members")
async def list_members(user: dict = Depends(get_current_user)):
return await tracker_get("/api/v1/members")
@app.get("/api/v1/members/{slug}")
async def get_member(slug: str, user: dict = Depends(get_current_user)):
return await tracker_get(f"/api/v1/members/{slug}")
@app.post("/api/v1/members")
async def create_member(request: Request, user: dict = Depends(get_current_user)):
body = await request.json()
return await tracker_post("/api/v1/members", json=body)
@app.patch("/api/v1/members/{slug}")
async def update_member(slug: str, request: Request, user: dict = Depends(get_current_user)):
body = await request.json()
return await tracker_patch(f"/api/v1/members/{slug}", json=body)
# --- Health ---
@app.get("/health")
async def health():
return {"status": "ok", "service": "bff", "version": "0.2.0"}
# WebSocket proxy
app.include_router(ws_router)
@app.on_event("shutdown")
async def shutdown():
await close_client()