259 lines
8.5 KiB
Python
259 lines
8.5 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)
|
|
|
|
@app.post("/api/v1/members/{slug}/regenerate-token")
|
|
async def regenerate_token(slug: str, user: dict = Depends(get_current_user)):
|
|
return await tracker_post(f"/api/v1/members/{slug}/regenerate-token")
|
|
|
|
@app.post("/api/v1/members/{slug}/revoke-token")
|
|
async def revoke_token(slug: str, user: dict = Depends(get_current_user)):
|
|
return await tracker_post(f"/api/v1/members/{slug}/revoke-token")
|
|
|
|
|
|
# --- 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()
|