diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 2e4d4f3..7838dd1 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -958,16 +958,49 @@ Beispiel-Antwort: ``` > ⚠️ 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. +> Passwort-Manager oder `~/.bashrc`). Es wird nur als SHA256-Hash in der Datenbank +> gespeichert — der Klartext ist danach **nicht mehr abrufbar**. +> +> Token verloren? → Neues Token ausstellen (s. u.) oder Nutzer löschen und neu anlegen. Das Token dem Nutzer mitteilen. Er gibt es bei jedem Aufruf im `Authorization`-Header an: ```bash -TOKEN=va-tok-AbCdEfGh12345... # einmal setzen +TOKEN=va-tok-AbCdEfGh12345... # einmal setzen, z.B. in ~/.bashrc: +# export TOKEN=va-tok-AbCdEfGh12345... curl -s $URL/api/me -H "Authorization: Bearer $TOKEN" | jq # → {"user_id":"a3f8c1d2e4b7…","display_name":"Oma Anna","prefs":{}} ``` +#### Token neu ausstellen (bei Verlust oder Rotation) + +Falls das Token verloren gegangen ist oder aus Sicherheitsgründen gewechselt werden soll: + +```bash +curl -s -X POST $URL/api/admin/users/$USER_ID/token \ + -H "X-Admin-Key: $ADMIN_API_KEY" | jq +``` + +Beispiel-Antwort: +```json +{ + "user_id": "a3f8c1d2e4b7...", + "display_name": "Oma Anna", + "token": "va-tok-NeuErKlArTeXt..." +} +``` + +Der **alte Token wird sofort ungültig**. Der neue Token erscheint ebenfalls nur einmal — +alle bisherigen Daten (Erinnerungen, Gesprächsverlauf) bleiben erhalten. + +> **Woher kommt `$ADMIN_API_KEY`?** Dieser Key ist in der Datei `.env` auf dem Server +> hinterlegt. Nachschauen mit: +> ```bash +> grep ADMIN_API_KEY .env +> # ADMIN_API_KEY=mein-geheimes-admin-passwort +> ``` +> Du hast ihn beim Setup selbst gewählt. Er ist **kein** auto-generierter Hash — +> du kannst ihn jederzeit in `.env` lesen und bei Bedarf ändern (Gateway neu starten). + #### Alle Nutzer anzeigen ```bash @@ -1524,6 +1557,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 | +| `POST` | `/api/admin/users/{user_id}/token` | `X-Admin-Key` | Neues Token ausstellen (alter Token sofort ungültig) | ## B.6 WebSocket diff --git a/app/api/admin.py b/app/api/admin.py index 99cc805..5c6eb36 100644 --- a/app/api/admin.py +++ b/app/api/admin.py @@ -59,6 +59,17 @@ async def delete_user(user_id: str): return {"deleted": user_id} +@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) + + @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 aae4077..71cbc0e 100644 --- a/app/store.py +++ b/app/store.py @@ -124,6 +124,11 @@ class Store(ABC): Nutzungsdaten). Anonymer Nutzer kann nicht geloescht werden. Liefert True, wenn der Nutzer existierte und geloescht wurde.""" + @abstractmethod + def reset_token(self, user_id: str) -> tuple[User, str] | None: + """Generiert einen neuen Token fuer den Nutzer; der alte wird sofort ungueltig. + Liefert (User, Klartext-Token) oder None, wenn der Nutzer nicht existiert.""" + @abstractmethod def log_emergency(self, user_id: str, category: str, snippet: str) -> None: """Protokolliert ein erkanntes Notfall-Signal (sensibel!).""" @@ -445,6 +450,19 @@ class SQLiteStore(Store): conn.execute("DELETE FROM users WHERE id = ?", (user_id,)) return True + def reset_token(self, user_id: str) -> tuple[User, str] | None: + with self._connect() as conn: + row = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone() + if not row: + return None + raw_token = secrets.token_urlsafe(32) + conn.execute( + "UPDATE users SET token_hash = ? WHERE id = ?", + (hash_token(raw_token), user_id), + ) + user = self._row_to_user(conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()) + return user, raw_token + # ----- Notfall-Protokoll ------------------------------------------------ def log_emergency(self, user_id: str, category: str, snippet: str) -> None: with self._connect() as conn: