diff --git a/src/tracker/init_db.py b/src/tracker/init_db.py index 1acd22e..7f0654e 100644 --- a/src/tracker/init_db.py +++ b/src/tracker/init_db.py @@ -36,6 +36,18 @@ async def init_db(): ) session.add(admin) + # Create BFF service member with TRACKER_TOKEN + bff_service = Member( + name="BFF Service", + slug="bff", + type="bridge", + role="bridge", + auth_method="token", + token="tb-tracker-dev-token", + status="offline", + ) + session.add(bff_service) + # Create lobby chat lobby = Chat( kind="lobby", @@ -44,7 +56,7 @@ async def init_db(): session.add(lobby) await session.commit() - logger.info("Seed data created: admin user + lobby chat (id=%s)", lobby.id) + logger.info("Seed data created: admin user, BFF service, lobby chat (id=%s)", lobby.id) if __name__ == "__main__": diff --git a/src/tracker/ws/handler.py b/src/tracker/ws/handler.py index 2e8e388..07aa893 100644 --- a/src/tracker/ws/handler.py +++ b/src/tracker/ws/handler.py @@ -29,7 +29,8 @@ async def websocket_endpoint(ws: WebSocket): return token = auth_msg.get("token", "") - slug = await _authenticate(ws, token) + on_behalf_of = auth_msg.get("on_behalf_of") + slug = await _authenticate(ws, token, on_behalf_of=on_behalf_of) if not slug: return @@ -77,8 +78,12 @@ async def websocket_endpoint(ws: WebSocket): ) -async def _authenticate(ws: WebSocket, token: str) -> str | None: - """Authenticate by token, return slug or None.""" +async def _authenticate(ws: WebSocket, token: str, on_behalf_of: str | None = None) -> str | None: + """Authenticate by token, return slug or None. + + If on_behalf_of is set and the token belongs to a bridge member, + use on_behalf_of slug instead (BFF proxying for web users). + """ async with async_session() as db: result = await db.execute( select(Member).where(Member.token == token).options(selectinload(Member.agent_config)) @@ -103,6 +108,26 @@ async def _authenticate(ws: WebSocket, token: str) -> str | None: await ws.close() return None + # BFF proxy: bridge member can act on behalf of a user + effective_slug = member.slug + effective_type = member.type + if on_behalf_of and member.type == "bridge": + # Look up the actual user + user_result = await db.execute( + select(Member).where(Member.slug == on_behalf_of) + .options(selectinload(Member.agent_config)) + ) + user_member = user_result.scalar_one_or_none() + if user_member: + effective_slug = user_member.slug + effective_type = user_member.type + member = user_member # use user's settings + logger.info("Bridge %s acting on behalf of %s", member.slug, effective_slug) + else: + # User not found, use a synthetic slug to avoid collisions + effective_slug = on_behalf_of + logger.info("Bridge acting on behalf of unknown user %s", effective_slug) + # Get listen modes chat_listen = "all" task_listen = "all" @@ -113,8 +138,8 @@ async def _authenticate(ws: WebSocket, token: str) -> str | None: # Register connection client = ConnectedClient( ws=ws, - member_slug=member.slug, - member_type=member.type, + member_slug=effective_slug, + member_type=effective_type, chat_listen=chat_listen, task_listen=task_listen, ) @@ -148,7 +173,7 @@ async def _authenticate(ws: WebSocket, token: str) -> str | None: await ws.send_json({ "type": "auth.ok", "data": { - "slug": member.slug, + "slug": effective_slug, "lobby_chat_id": str(lobby_chat.id) if lobby_chat else None, "projects": project_list, "online": online, diff --git a/src/tracker/ws/manager.py b/src/tracker/ws/manager.py index 734fd26..d87f5cc 100644 --- a/src/tracker/ws/manager.py +++ b/src/tracker/ws/manager.py @@ -44,6 +44,9 @@ class ConnectionManager: async def broadcast_message(self, project_id: str, message: dict, author_slug: str): """Broadcast message.new — filtered by chat_listen.""" + logger.info("broadcast_message: project=%s, author=%s, clients=%s", project_id, author_slug, list(self.clients.keys())) + for slug, client in list(self.clients.items()): + logger.info(" checking %s: subscribed=%s, chat_listen=%s", slug, client.subscribed_projects, client.chat_listen) mentions = message.get("mentions", []) for slug, client in list(self.clients.items()): if slug == author_slug: