diff --git a/bff/app.py b/bff/app.py
index f454ffe..21ec1b2 100644
--- a/bff/app.py
+++ b/bff/app.py
@@ -3,7 +3,7 @@
import logging
import time
-from fastapi import FastAPI, Request, Depends
+from fastapi import FastAPI, Request, Depends, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from pydantic import BaseModel
@@ -19,9 +19,8 @@ logging.basicConfig(
)
logger = logging.getLogger("bff")
-app = FastAPI(title="Team Board BFF", version="0.1.0")
+app = FastAPI(title="Team Board BFF", version="0.2.0")
-# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["https://team.uix.su", "http://localhost:3100"],
@@ -30,8 +29,6 @@ app.add_middleware(
)
-# --- Request logging ---
-
@app.middleware("http")
async def log_requests(request: Request, call_next):
start = time.time()
@@ -57,61 +54,68 @@ class LoginRequest(BaseModel):
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 {"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"], "provider": user["provider"]}}
+ return {"user": {"name": user["name"], "slug": user.get("sub", "admin")}}
# --- Proxy: Projects ---
-@app.get("/api/v1/projects/")
+@app.get("/api/v1/projects")
async def list_projects(user: dict = Depends(get_current_user)):
- return await tracker_get("/api/v1/projects/")
+ return await tracker_get("/api/v1/projects")
-@app.post("/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)
+ 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.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/{project_id}")
-async def update_project(project_id: str, request: Request, user: dict = Depends(get_current_user)):
+@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/{project_id}", json=body)
+ return await tracker_patch(f"/api/v1/projects/{slug}", 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)
+@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, user: dict = Depends(get_current_user)):
+@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
- return await tracker_get("/api/v1/tasks/", params=params)
+ 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, user: dict = Depends(get_current_user)):
+@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("/api/v1/tasks/", json=body)
+ return await tracker_post(f"/api/v1/tasks?project_slug={project_slug}", json=body)
@app.get("/api/v1/tasks/{task_id}")
@@ -127,73 +131,120 @@ async def update_task(task_id: str, request: Request, user: dict = Depends(get_c
@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)
+ return await tracker_delete(f"/api/v1/tasks/{task_id}")
-# --- 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.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}")
-# --- 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)):
+@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()
- # 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)
+ return await tracker_post(f"/api/v1/tasks/{task_id}/reject", 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)):
+@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("/api/v1/labels/", json=body)
+ 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.1.0"}
+ return {"status": "ok", "service": "bff", "version": "0.2.0"}
-# --- WebSocket ---
-
+# WebSocket proxy
app.include_router(ws_router)
-# --- Shutdown ---
-
@app.on_event("shutdown")
async def shutdown():
await close_client()
diff --git a/bff/ws_proxy.py b/bff/ws_proxy.py
index 7288eb0..a245f79 100644
--- a/bff/ws_proxy.py
+++ b/bff/ws_proxy.py
@@ -1,8 +1,8 @@
-"""WebSocket proxy: authenticated users ↔ Tracker."""
+"""WebSocket proxy: authenticated web users ↔ Tracker WS."""
import asyncio
-import logging
import json
+import logging
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
import websockets
@@ -26,12 +26,30 @@ async def ws_proxy(ws: WebSocket, token: str = ""):
return
await ws.accept()
- logger.info("WS connected: %s", user.get("name"))
+ user_name = user.get("name", "unknown")
+ user_slug = user.get("sub", "admin")
+ logger.info("WS connected: %s (%s)", user_name, user_slug)
- # Connect to tracker
- tracker_url = f"{TRACKER_WS_URL}?client_type=human&client_id={user['sub']}&token={TRACKER_TOKEN}"
try:
- async with websockets.connect(tracker_url) as tracker_ws:
+ async with websockets.connect(TRACKER_WS_URL) as tracker_ws:
+ # Send auth to tracker on behalf of user
+ auth_msg = json.dumps({
+ "type": "auth",
+ "token": TRACKER_TOKEN,
+ })
+ await tracker_ws.send(auth_msg)
+
+ # Wait for auth.ok
+ auth_resp = await tracker_ws.recv()
+ auth_data = json.loads(auth_resp)
+ if auth_data.get("type") != "auth.ok":
+ logger.error("Tracker auth failed: %s", auth_data)
+ await ws.close(code=4002, reason="Tracker auth failed")
+ return
+
+ # Forward auth.ok to client
+ await ws.send_text(auth_resp)
+
async def forward_to_tracker():
try:
while True:
@@ -51,4 +69,4 @@ async def ws_proxy(ws: WebSocket, token: str = ""):
except Exception as e:
logger.error("WS proxy error: %s", e)
finally:
- logger.info("WS disconnected: %s", user.get("name"))
+ logger.info("WS disconnected: %s", user_name)
diff --git a/src/app/(protected)/projects/[slug]/page.tsx b/src/app/(protected)/projects/[slug]/page.tsx
index b3185a0..e8a0175 100644
--- a/src/app/(protected)/projects/[slug]/page.tsx
+++ b/src/app/(protected)/projects/[slug]/page.tsx
@@ -43,7 +43,7 @@ export default function ProjectPage() {
- Пока пусто. Напишите первое сообщение 💬 -
- )} - {messages.map((msg) => ( -