diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 28a5290..81f3b1c 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -6,12 +6,26 @@ > Remote-Deployment: [deploy/README.md](deploy/README.md) · > Kurzübersicht: [README.md](README.md) -In den Shell-Beispielen steht die Gateway-Adresse als Variable — einmal setzen, -dann überall einsetzbar (Port aus deiner `.env`, hier `8003`): +### Lesehilfe: `$URL` und `| jq` + +In allen Shell-Beispielen dieses Handbuchs steht `$URL` als Platzhalter für die +Gateway-Adresse. Einmal setzen, dann überall einsetzbar: + ```bash export URL=http://localhost:8003 ``` -Befehle mit JSON-Ausgabe enden auf `| jq` (schöne Formatierung). Installieren: `sudo apt install jq`. + +*(Port aus deiner `.env` — Standard ist `8080`, in dieser Installation `8003`.)* + +Danach kann man z. B. schreiben: +```bash +curl -s $URL/health +# entspricht: curl -s http://localhost:8003/health +``` + +Befehle, die JSON zurückgeben, enden auf `| jq` — das formatiert die Ausgabe lesbar. +Installieren: `sudo apt install jq`. Ohne `jq` einfach weglassen; der Befehl +funktioniert trotzdem, die Ausgabe ist dann unformatiert. --- @@ -916,27 +930,92 @@ AUTH_ENABLED=false # Lokal/Entwicklung: anonymer Standardnutzer, kein Token n Geschützte Endpunkte: `chat`, `speak`, `transcribe`, `sessions`, `me`. -### 7.2 Nutzer anlegen und Tokens +### 7.2 Nutzer anlegen, anzeigen und löschen + +**Voraussetzung:** `ADMIN_API_KEY` muss beim Gateway-Start als Umgebungsvariable gesetzt sein. +Einmal setzen (gilt für alle folgenden Befehle im Terminal): ```bash -export ADMIN_API_KEY=ein-langes-geheimnis # muss beim Gateway-Start gesetzt sein +export ADMIN_API_KEY=mein-langes-geheimnis +``` -# Nutzer anlegen (Token erscheint NUR einmal — sicher aufbewahren!): +#### Nutzer anlegen + +```bash curl -s -X POST $URL/api/admin/users \ -H "X-Admin-Key: $ADMIN_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"display_name":"Oma Anna"}' | jq -# → {"user_id":"…","display_name":"Oma Anna","token":"…"} +``` -# Mit Token aufrufen: -TOKEN= +Beispiel-Antwort: +```json +{ + "user_id": "a3f8c1d2e4b7...", + "display_name": "Oma Anna", + "token": "va-tok-AbCdEfGh12345..." +} +``` + +> ⚠️ Das Token erscheint **nur einmal** — sofort sicher aufbewahren (z. B. in einem +> Passwort-Manager). Es kann danach nicht mehr abgerufen werden. Bei Verlust muss +> der Nutzer gelöscht und neu angelegt werden. + +Das Token dem Nutzer mitteilen. Er gibt es bei jedem Aufruf im `Authorization`-Header an: +```bash +TOKEN=va-tok-AbCdEfGh12345... # einmal setzen curl -s $URL/api/me -H "Authorization: Bearer $TOKEN" | jq +# → {"user_id":"a3f8c1d2e4b7…","display_name":"Oma Anna","prefs":{}} +``` -# Alle Nutzer anzeigen (Admin): +#### Alle Nutzer anzeigen + +```bash curl -s $URL/api/admin/users -H "X-Admin-Key: $ADMIN_API_KEY" | jq ``` -**Dauerhafte Präferenzen** pro Nutzer (Ebene zwischen Profil und Session): +Beispiel-Antwort: +```json +[ + { + "user_id": "a3f8c1d2e4b7...", + "display_name": "Oma Anna", + "external_id": null, + "created_at": "2026-06-18T10:00:00+00:00" + }, + { + "user_id": "b9e2f5a1c6d3...", + "display_name": "Herr Müller", + "external_id": null, + "created_at": "2026-06-18T11:30:00+00:00" + } +] +``` + +#### Nutzer löschen + +Löscht den Nutzer **und alle seine Daten** (Sessions, Gesprächsverlauf, Erinnerungen, +Nutzungsstatistik) unwiderruflich. + +```bash +USER_ID=a3f8c1d2e4b7... # user_id aus der Liste oben + +curl -s -X DELETE $URL/api/admin/users/$USER_ID \ + -H "X-Admin-Key: $ADMIN_API_KEY" | jq +``` + +Beispiel-Antwort bei Erfolg: +```json +{ "deleted": "a3f8c1d2e4b7..." } +``` + +Nutzer nicht gefunden → HTTP 404: +```json +{ "detail": "Nutzer 'xyz' nicht gefunden." } +``` + +#### Dauerhafte Nutzerpräferenzen setzen + ```bash curl -s -X PUT $URL/api/me/prefs \ -H "Authorization: Bearer $TOKEN" \ @@ -1436,6 +1515,7 @@ Body-Felder: `input_endpoint`, `output_endpoint`, `stt_provider`, `llm_provider` |---------|------|------|--------------| | `POST` | `/api/admin/users` | `X-Admin-Key` | Nutzer anlegen → Token einmalig | | `GET` | `/api/admin/users` | `X-Admin-Key` | Alle Nutzer auflisten | +| `DELETE` | `/api/admin/users/{user_id}` | `X-Admin-Key` | Nutzer + alle Daten löschen | ## B.6 WebSocket diff --git a/app/api/admin.py b/app/api/admin.py index 7dac54e..99cc805 100644 --- a/app/api/admin.py +++ b/app/api/admin.py @@ -46,6 +46,19 @@ async def create_user(payload: UserCreate): return UserCreated(user_id=user.id, display_name=user.display_name, token=token) +@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} + + @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.""" diff --git a/app/store.py b/app/store.py index ead900b..aae4077 100644 --- a/app/store.py +++ b/app/store.py @@ -118,6 +118,12 @@ class Store(ABC): def add_usage(self, user_id: str, units: int = 0, day: str | None = None) -> int: """Zaehlt eine Anfrage (+units) und liefert die neue Tages-Anfragezahl.""" + @abstractmethod + def delete_user(self, user_id: str) -> bool: + """Loescht einen Nutzer und alle seine Daten (Sessions, Nachrichten, Erinnerungen, + Nutzungsdaten). Anonymer Nutzer kann nicht geloescht werden. + Liefert True, wenn der Nutzer existierte und geloescht wurde.""" + @abstractmethod def log_emergency(self, user_id: str, category: str, snippet: str) -> None: """Protokolliert ein erkanntes Notfall-Signal (sensibel!).""" @@ -422,6 +428,23 @@ class SQLiteStore(Store): ).fetchone() return int(row["requests"]) + def delete_user(self, user_id: str) -> bool: + if user_id == ANONYMOUS_USER_ID: + raise ValueError("Der anonyme Nutzer kann nicht geloescht werden.") + with self._connect() as conn: + if not conn.execute("SELECT 1 FROM users WHERE id = ?", (user_id,)).fetchone(): + return False + conn.execute( + "DELETE FROM messages WHERE session_id IN" + " (SELECT id FROM sessions WHERE user_id = ?)", + (user_id,), + ) + conn.execute("DELETE FROM sessions WHERE user_id = ?", (user_id,)) + conn.execute("DELETE FROM memories WHERE user_id = ?", (user_id,)) + conn.execute("DELETE FROM usage WHERE user_id = ?", (user_id,)) + conn.execute("DELETE FROM users WHERE id = ?", (user_id,)) + return True + # ----- Notfall-Protokoll ------------------------------------------------ def log_emergency(self, user_id: str, category: str, snippet: str) -> None: with self._connect() as conn: