feat(B-WS): WebSocket Helpers + Redis Pub/Sub + Error-Handling
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
B-WS: app/core/ws_helpers.py (NEU) — gemeinsame WebSocket Helpers - authenticate_ws: Session-Auth für WebSocket (Cookie/Token → User/Tenant) - check_ws_origin: Origin-Check (delegiert auf verify_ws_origin) - check_ws_tenant: User-Tenant-Membership-Check - cleanup_ws_connection: Connection aus Registry entfernen + WS schließen - start_heartbeat: Background Ping-Task - send_ws_error: strukturierte Error-Message an Client - handle_ws_message: Message-Dispatch mit Error-Handling B-WS: app/core/ws_pubsub.py (NEU) — Redis Pub/Sub für Multi-Worker-Fanout - publish_to_channel / subscribe_to_channel - get_tenant_channel / broadcast_to_tenants B-WS: WebSocketManager + AIUIControlWSManager angepasst - connect() nutzt authenticate_ws + check_ws_origin + check_ws_tenant - disconnect() nutzt cleanup_ws_connection + cancelt Heartbeat/PubSub - broadcast() unterstützt Redis Pub/Sub Fanout B-ERR-WS: WS Error-Handling in ws_helpers integriert - send_ws_error für strukturierte Errors - handle_ws_message fängt Handler-Exceptions B-WS-TEST: 24 Tests in test_ws_helpers.py — alle grün - Auth, Origin, Error, Dispatch, Cleanup, Heartbeat, Pub/Sub Roundtrip - Keine Regression: 47/47 Resilience+Hooks Tests grün
This commit is contained in:
@@ -205,33 +205,28 @@ async def ai_ui_control_ws(websocket: WebSocket):
|
||||
|
||||
Authentication: via session cookie (same pattern as kommunikation plugin).
|
||||
"""
|
||||
from app.config import get_settings
|
||||
from app.core.auth import get_session_data, get_redis, verify_ws_origin
|
||||
from app.core.db import async_session_maker
|
||||
from app.core.service_container import get_container
|
||||
|
||||
settings = get_settings()
|
||||
if not await verify_ws_origin(websocket):
|
||||
await websocket.close(code=4003, reason="Origin not allowed")
|
||||
container = get_container()
|
||||
if not container.has("ai_ui_control_ws"):
|
||||
await websocket.close(code=4003, reason="AI UI Control not available")
|
||||
return
|
||||
|
||||
session_id = websocket.cookies.get(settings.session_cookie_name)
|
||||
if not session_id:
|
||||
await websocket.close(code=4001, reason="Not authenticated")
|
||||
return
|
||||
ws_manager = container.get("ai_ui_control_ws")
|
||||
|
||||
redis = get_redis()
|
||||
session_data = await get_session_data(redis, session_id)
|
||||
if session_data is None:
|
||||
await websocket.close(code=4001, reason="Session expired")
|
||||
return
|
||||
# connect() performs origin check, session auth, tenant check
|
||||
async with async_session_maker() as db:
|
||||
auth = await ws_manager.connect(websocket, db)
|
||||
if auth is None:
|
||||
return # connection was rejected and closed by ws_helpers
|
||||
|
||||
user_id = session_data["user_id"]
|
||||
tenant_id = session_data["tenant_id"]
|
||||
user_id = auth["user_id"]
|
||||
tenant_id = auth["tenant_id"]
|
||||
|
||||
# Plugin-Gate: check if ai_ui_control plugin is active (global + tenant)
|
||||
from app.core.permission_registry import get_permission_registry
|
||||
from sqlalchemy import text as sa_text
|
||||
from app.core.db import async_session_maker
|
||||
import uuid as _uuid
|
||||
try:
|
||||
registry = get_permission_registry()
|
||||
@@ -251,14 +246,6 @@ async def ai_ui_control_ws(websocket: WebSocket):
|
||||
await websocket.close(code=4003, reason="Plugin check failed")
|
||||
return
|
||||
|
||||
container = get_container()
|
||||
if not container.has("ai_ui_control_ws"):
|
||||
await websocket.close(code=4003, reason="AI UI Control not available")
|
||||
return
|
||||
|
||||
ws_manager = container.get("ai_ui_control_ws")
|
||||
await ws_manager.connect(websocket, user_id)
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
|
||||
Reference in New Issue
Block a user