feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt + Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen), Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue - Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration) - /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated - StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL - WS-Auth liest Identitaet aus dem Handshake-Header - Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery) - Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
855fc71a1e
commit
76df695111
16 changed files with 703 additions and 29 deletions
|
|
@ -31,6 +31,7 @@ from app.store import SessionOwnershipError
|
|||
from app.audio.vad import EnergyVAD
|
||||
from app.core.memory_extractor import maybe_schedule_extraction
|
||||
from app.quota import enforce_quota, record_usage, QuotaExceededError
|
||||
from app.auth import authenticate
|
||||
from app.safety.emergency import handle_emergency, schedule_llm_emergency_check
|
||||
|
||||
router = APIRouter()
|
||||
|
|
@ -45,13 +46,11 @@ _OVERRIDE_KEYS = (
|
|||
)
|
||||
|
||||
|
||||
def _authenticate(token: str | None):
|
||||
store = get_store()
|
||||
if not settings.auth_enabled:
|
||||
return store.ensure_anonymous_user()
|
||||
if not token:
|
||||
return None
|
||||
return store.get_user_by_token(token)
|
||||
def _authenticate(websocket: WebSocket, token: str | None):
|
||||
# Forward-Auth (SSO) greift auch beim WS-Handshake: SSOwat injiziert den
|
||||
# Identitaets-Header in den Upgrade-Request -> aus websocket.headers lesbar.
|
||||
client_host = websocket.client.host if websocket.client else ""
|
||||
return authenticate(websocket.headers, client_host, token)
|
||||
|
||||
|
||||
async def _resolve(user, session_id, options):
|
||||
|
|
@ -213,7 +212,7 @@ async def _voice_turn(websocket, store, user, session_id, audio, fmt, options):
|
|||
|
||||
@router.websocket("/ws/chat")
|
||||
async def ws_chat(websocket: WebSocket, session_id: str | None = None, token: str | None = None):
|
||||
user = _authenticate(token)
|
||||
user = _authenticate(websocket, token)
|
||||
if user is None:
|
||||
await websocket.close(code=1008)
|
||||
return
|
||||
|
|
@ -244,7 +243,7 @@ async def ws_chat(websocket: WebSocket, session_id: str | None = None, token: st
|
|||
|
||||
@router.websocket("/ws/voice")
|
||||
async def ws_voice(websocket: WebSocket, session_id: str | None = None, token: str | None = None):
|
||||
user = _authenticate(token)
|
||||
user = _authenticate(websocket, token)
|
||||
if user is None:
|
||||
await websocket.close(code=1008)
|
||||
return
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue