2026-06-18 05:19:41 +02:00
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
2026-06-17 02:14:25 +02:00
|
|
|
|
2026-06-18 05:19:41 +02:00
|
|
|
from app.auth import is_admin_user, require_admin, require_admin_or_user
|
|
|
|
|
from app.config import settings
|
2026-06-17 02:14:25 +02:00
|
|
|
from app.dependencies import get_store
|
2026-06-18 17:55:05 +02:00
|
|
|
from app.schemas import MemoryCreate, MemoryOut, UserCreate, UserCreated, UserUpdate
|
2026-06-17 02:14:25 +02:00
|
|
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
2026-06-18 05:19:41 +02:00
|
|
|
@router.get("/admin/request-headers")
|
|
|
|
|
async def request_headers(request: Request, key: str | None = None):
|
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>
2026-06-18 04:37:16 +02:00
|
|
|
"""Discovery: zeigt die eingehenden HTTP-Header + Quell-IP.
|
|
|
|
|
|
|
|
|
|
Hilft, hinter dem Reverse-Proxy/SSO den richtigen Identitaets-Header
|
2026-06-18 05:19:41 +02:00
|
|
|
(TRUSTED_AUTH_HEADER) festzustellen.
|
|
|
|
|
|
|
|
|
|
Henne-Ei: Beim Einrichten kennt das Gateway den SSO-Admin noch nicht. Daher
|
|
|
|
|
ist der Endpoint auch per Admin-Key aufrufbar - als Query (`?key=...`, im Browser
|
|
|
|
|
durchs SSO bequem) oder X-Admin-Key-Header. Nach dem Setup wieder meiden bzw.
|
|
|
|
|
den Key rotieren (er landet sonst in Proxy-Logs).
|
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>
2026-06-18 04:37:16 +02:00
|
|
|
"""
|
2026-06-18 05:19:41 +02:00
|
|
|
expected = settings.admin_api_key.strip()
|
|
|
|
|
provided = key or request.headers.get("x-admin-key")
|
|
|
|
|
ok = bool(expected) and provided is not None and provided.strip() == expected
|
|
|
|
|
if not ok and not is_admin_user(_current_user_or_none(request)):
|
|
|
|
|
raise HTTPException(status_code=403, detail="Admin privileges required")
|
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>
2026-06-18 04:37:16 +02:00
|
|
|
return {
|
|
|
|
|
"client": request.client.host if request.client else None,
|
|
|
|
|
"headers": dict(request.headers),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-06-18 05:19:41 +02:00
|
|
|
def _current_user_or_none(request: Request):
|
|
|
|
|
from app.auth import authenticate, _bearer_token
|
|
|
|
|
|
|
|
|
|
client_host = request.client.host if request.client else ""
|
|
|
|
|
token = _bearer_token(request.headers.get("authorization"))
|
|
|
|
|
return authenticate(request.headers, client_host, token)
|
|
|
|
|
|
|
|
|
|
|
2026-06-17 02:14:25 +02:00
|
|
|
@router.post("/admin/users", response_model=UserCreated, dependencies=[Depends(require_admin)])
|
|
|
|
|
async def create_user(payload: UserCreate):
|
|
|
|
|
"""Legt einen Nutzer an und gibt das Bearer-Token EINMALIG zurueck."""
|
|
|
|
|
user, token = get_store().create_user(payload.display_name)
|
|
|
|
|
return UserCreated(user_id=user.id, display_name=user.display_name, token=token)
|
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>
2026-06-18 04:37:16 +02:00
|
|
|
|
|
|
|
|
|
2026-06-18 17:23:25 +02:00
|
|
|
@router.delete("/admin/users/{user_id}", dependencies=[Depends(require_admin)])
|
|
|
|
|
async def delete_user(user_id: str):
|
|
|
|
|
"""Loescht einen Nutzer und alle seine Daten (Sessions, Nachrichten, Erinnerungen,
|
|
|
|
|
Nutzungsdaten). Der anonyme Nutzer kann nicht geloescht werden."""
|
|
|
|
|
try:
|
|
|
|
|
deleted = get_store().delete_user(user_id)
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
raise HTTPException(status_code=400, detail=str(exc))
|
|
|
|
|
if not deleted:
|
|
|
|
|
raise HTTPException(status_code=404, detail=f"Nutzer {user_id!r} nicht gefunden.")
|
|
|
|
|
return {"deleted": user_id}
|
|
|
|
|
|
|
|
|
|
|
2026-06-18 17:36:41 +02:00
|
|
|
@router.post("/admin/users/{user_id}/token", response_model=UserCreated, dependencies=[Depends(require_admin)])
|
|
|
|
|
async def reset_token(user_id: str):
|
|
|
|
|
"""Stellt einen neuen Bearer-Token aus; der alte wird sofort ungueltig.
|
|
|
|
|
Der neue Token wird EINMALIG zurueckgegeben und danach nicht mehr angezeigt."""
|
|
|
|
|
result = get_store().reset_token(user_id)
|
|
|
|
|
if result is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail=f"Nutzer {user_id!r} nicht gefunden.")
|
|
|
|
|
user, token = result
|
|
|
|
|
return UserCreated(user_id=user.id, display_name=user.display_name, token=token)
|
|
|
|
|
|
|
|
|
|
|
2026-06-18 17:55:05 +02:00
|
|
|
@router.put("/admin/users/{user_id}", dependencies=[Depends(require_admin)])
|
|
|
|
|
async def update_user(user_id: str, payload: UserUpdate):
|
|
|
|
|
"""Aktualisiert den Anzeigenamen eines Nutzers (z. B. nach erstem SSO-Login)."""
|
|
|
|
|
user = get_store().update_display_name(user_id, payload.display_name)
|
|
|
|
|
if user is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail=f"Nutzer {user_id!r} nicht gefunden.")
|
|
|
|
|
return {"user_id": user.id, "display_name": user.display_name}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/admin/users/{user_id}/memories", response_model=MemoryOut, dependencies=[Depends(require_admin)])
|
|
|
|
|
async def add_user_memory(user_id: str, payload: MemoryCreate):
|
|
|
|
|
"""Legt eine Erinnerung fuer einen Nutzer an (Admin kann Kontext vorbelegen)."""
|
|
|
|
|
store = get_store()
|
|
|
|
|
if store.get_user(user_id) is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail=f"Nutzer {user_id!r} nicht gefunden.")
|
|
|
|
|
memory = store.add_memory(user_id, payload.content)
|
|
|
|
|
return MemoryOut(id=memory.id, content=memory.content, created_at=memory.created_at)
|
|
|
|
|
|
|
|
|
|
|
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>
2026-06-18 04:37:16 +02:00
|
|
|
@router.get("/admin/users", dependencies=[Depends(require_admin_or_user)])
|
|
|
|
|
async def list_users():
|
|
|
|
|
"""Listet die Nutzer (ohne Secrets). Fuer Admins (SSO/ADMIN_USERS) oder ADMIN_API_KEY."""
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
"user_id": u.id,
|
|
|
|
|
"display_name": u.display_name,
|
|
|
|
|
"external_id": u.external_id,
|
|
|
|
|
"created_at": u.created_at,
|
|
|
|
|
}
|
|
|
|
|
for u in get_store().list_users()
|
|
|
|
|
]
|