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:
Dieter Schlüter 2026-06-18 04:37:16 +02:00
commit 76df695111
16 changed files with 703 additions and 29 deletions

53
deploy/README.md Normal file
View file

@ -0,0 +1,53 @@
# Remote-Betrieb über YunoHost (assistent.linix.de)
Web-UI + Mikrofon vom Handy/Browser, abgesichert per HTTPS und YunoHost-SSO.
Topologie: `https://assistent.linix.de` → nginx@YunoHost (TLS, SSO) → über LAN →
`http://<GPU-Box>:8003` (Gateway). Der interne LAN-Hop ist unverschlüsselt
(vertrautes LAN, wie bei Ollama).
> **Warum HTTPS Pflicht ist:** Browser geben das Mikrofon (`getUserMedia`) nur in
> einem „secure context" frei also HTTPS oder `http://localhost`. Über einfaches
> `http://` vom Handy gibt es **kein** Mikrofon.
## 1. Gateway auf der GPU-Box
`deploy/voice-assistant.env.example``/etc/voice-assistant/voice-assistant.env` anpassen:
- `HOST` = **LAN-IP** der GPU-Box (nicht `0.0.0.0`), `PORT=8003`
- `AUTH_ENABLED=true`
- `TRUSTED_AUTH_HEADER` (nach Discovery, s. u.), `TRUSTED_PROXY_IPS` = LAN-IP von linix.de
- `ADMIN_USERS=atoor,dieterschlueter,dschlueter`, `SSO_LOGOUT_URL=…`
> Kein `--forwarded-allow-ips` setzen: Das Identitäts-Vertrauen prüft die
> **direkte Quell-IP** (= der Proxy). Würde uvicorn den Client aus
> `X-Forwarded-For` überschreiben, schlüge der Proxy-IP-Check fehl.
## 2. Drei Sicherheits-Pflichten
1. **Bind:** Gateway nur an die LAN-IP (Schritt 1).
2. **Firewall:** Port 8003 der GPU-Box **nur** von der linix.de-IP erlauben, z. B.:
```
sudo ufw allow from <linix.de_LAN_IP> to any port 8003 proto tcp
sudo ufw deny 8003
```
3. **Header überschreiben:** nginx setzt den Identitäts-Header selbst; Client-Eingaben
werden verworfen (siehe nginx-Conf). App-seitig zusätzlich der Proxy-IP-Check.
## 3. nginx auf YunoHost
`deploy/assistent.linix.de.nginx.conf` als Vorlage → auf dem YunoHost-Server unter
`/etc/nginx/conf.d/assistent.linix.de.d/assistant.conf` ablegen, `GPU_BOX_LAN_IP`
eintragen, die `map $http_upgrade …` einmalig im http{}-Kontext anlegen, dann
`nginx -t && systemctl reload nginx`. Subdomain `assistent.linix.de` in YunoHost
anlegen (Let's Encrypt) und per SSO schützen (nur erlaubte Tester/Gruppe).
## 4. Discovery: richtigen Identitäts-Header bestimmen
Als Admin **hinter dem SSO** aufrufen:
```
https://assistent.linix.de/api/admin/request-headers
```
Dort den Header finden, der den eingeloggten SSO-Usernamen trägt (z. B. `X-Remote-User`,
`Remote-User`, `Auth-User` …). Diesen Namen in `TRUSTED_AUTH_HEADER` **und** in der
nginx-`proxy_set_header …`-Zeile eintragen, Gateway + nginx neu laden.
## 5. Test
- `https://assistent.linix.de/` lädt die UI, links „Angemeldet als <SSO-User>".
- Text-Chat funktioniert; **Mikrofon-Button** nimmt auf und spielt die Antwort ab.
- Admins (`ADMIN_USERS`) sehen den Admin-Bereich (Nutzerliste).

View file

@ -0,0 +1,42 @@
# Reverse-Proxy fuer die Voice-Assistant-Web-UI auf einem YunoHost-Server.
#
# Ziel: https://assistent.linix.de -> http://<GPU_BOX_LAN_IP>:8003 (Gateway im LAN)
# YunoHost terminiert TLS (Let's Encrypt) und schuetzt die Subdomain per SSO.
#
# Ablage auf dem YunoHost-Server (Beispiel):
# /etc/nginx/conf.d/assistent.linix.de.d/assistant.conf
# danach: nginx -t && systemctl reload nginx
#
# WICHTIG (Unterschied zu Ollama): WebSockets (/ws/voice, /ws/chat) brauchen die
# Upgrade-Header und einen langen read-timeout - sonst bricht der Mikrofon-Button.
# --- einmalig im http{}-Kontext (z. B. /etc/nginx/conf.d/websocket-upgrade.conf) ---
# map $http_upgrade $connection_upgrade {
# default upgrade;
# '' close;
# }
location / {
proxy_pass http://GPU_BOX_LAN_IP:8003; # <-- LAN-IP der GPU-Box eintragen
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket-Upgrade (zwingend fuer das Mikrofon):
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# Identitaets-Header: SSOwat setzt die Nutzeridentitaet. Nach der Discovery
# (GET /api/admin/request-headers hinter dem SSO) den richtigen Namen hier
# FEST setzen und Client-Spoofing verwerfen. Beispiel, wenn SSO $remote_user
# bereitstellt (Name ggf. anpassen):
#
# proxy_set_header X-Remote-User $remote_user;
#
# In der Gateway-Konfiguration dann: TRUSTED_AUTH_HEADER=X-Remote-User
}

View file

@ -1,12 +1,22 @@
HOST=0.0.0.0
PORT=8080
# Nur an die LAN-IP binden (NICHT 0.0.0.0), erreichbar allein fuer den Reverse-Proxy.
HOST=192.168.0.50 # <-- LAN-IP der GPU-Box
PORT=8003
OPENROUTER_API_KEY=
# Profil und Konfiguration
VA_PROFILE=cloud
# Authentifizierung (Produktion: an). ADMIN_API_KEY fuer die Nutzerverwaltung.
# Authentifizierung (Produktion: an). ADMIN_API_KEY fuer die Nutzerverwaltung (Fallback).
AUTH_ENABLED=true
ADMIN_API_KEY=
# --- Forward-/Trusted-Header-Auth via YunoHost-SSO ---------------------------
# Nach der Discovery (GET /api/admin/request-headers hinter dem SSO) den echten
# Header-Namen eintragen. Identitaet wird nur von der Proxy-Quell-IP akzeptiert.
TRUSTED_AUTH_HEADER=X-Remote-User
TRUSTED_PROXY_IPS=192.168.0.10 # <-- LAN-IP des YunoHost-Servers (linix.de)
ADMIN_USERS=atoor,dieterschlueter,dschlueter
SSO_LOGOUT_URL=https://linix.de/yunohost/sso/?action=logout
# Persistente Datenbank (Pfad auf dem Host)
DB_PATH=/var/lib/voice-assistant/voice-assistant.db