feat(admin): Token-Reset-Endpunkt + Doku-Klarstellung zu Token-Verwaltung
- POST /api/admin/users/{user_id}/token: neues Bearer-Token ausstellen
(alter Token sofort ungültig, Nutzerdaten bleiben erhalten)
- Store ABC + SQLiteStore: reset_token() implementiert
- BEDIENUNGSANLEITUNG §7.2: erklärt warum Tokens nicht abrufbar sind (nur
SHA256-Hash gespeichert), wo ADMIN_API_KEY nachzuschauen ist (.env),
wie Token-Reset genutzt wird; praktisches Tipp zu ~/.bashrc
- Anhang B.5: neuen Endpunkt in REST-Referenz eingetragen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8ef982f297
commit
b76ec76c45
3 changed files with 66 additions and 3 deletions
|
|
@ -958,16 +958,49 @@ Beispiel-Antwort:
|
||||||
```
|
```
|
||||||
|
|
||||||
> ⚠️ Das Token erscheint **nur einmal** — sofort sicher aufbewahren (z. B. in einem
|
> ⚠️ Das Token erscheint **nur einmal** — sofort sicher aufbewahren (z. B. in einem
|
||||||
> Passwort-Manager). Es kann danach nicht mehr abgerufen werden. Bei Verlust muss
|
> Passwort-Manager oder `~/.bashrc`). Es wird nur als SHA256-Hash in der Datenbank
|
||||||
> der Nutzer gelöscht und neu angelegt werden.
|
> 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:
|
Das Token dem Nutzer mitteilen. Er gibt es bei jedem Aufruf im `Authorization`-Header an:
|
||||||
```bash
|
```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
|
curl -s $URL/api/me -H "Authorization: Bearer $TOKEN" | jq
|
||||||
# → {"user_id":"a3f8c1d2e4b7…","display_name":"Oma Anna","prefs":{}}
|
# → {"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
|
#### Alle Nutzer anzeigen
|
||||||
|
|
||||||
```bash
|
```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 |
|
| `POST` | `/api/admin/users` | `X-Admin-Key` | Nutzer anlegen → Token einmalig |
|
||||||
| `GET` | `/api/admin/users` | `X-Admin-Key` | Alle Nutzer auflisten |
|
| `GET` | `/api/admin/users` | `X-Admin-Key` | Alle Nutzer auflisten |
|
||||||
| `DELETE` | `/api/admin/users/{user_id}` | `X-Admin-Key` | Nutzer + alle Daten löschen |
|
| `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
|
## B.6 WebSocket
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,17 @@ async def delete_user(user_id: str):
|
||||||
return {"deleted": user_id}
|
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)])
|
@router.get("/admin/users", dependencies=[Depends(require_admin_or_user)])
|
||||||
async def list_users():
|
async def list_users():
|
||||||
"""Listet die Nutzer (ohne Secrets). Fuer Admins (SSO/ADMIN_USERS) oder ADMIN_API_KEY."""
|
"""Listet die Nutzer (ohne Secrets). Fuer Admins (SSO/ADMIN_USERS) oder ADMIN_API_KEY."""
|
||||||
|
|
|
||||||
18
app/store.py
18
app/store.py
|
|
@ -124,6 +124,11 @@ class Store(ABC):
|
||||||
Nutzungsdaten). Anonymer Nutzer kann nicht geloescht werden.
|
Nutzungsdaten). Anonymer Nutzer kann nicht geloescht werden.
|
||||||
Liefert True, wenn der Nutzer existierte und geloescht wurde."""
|
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
|
@abstractmethod
|
||||||
def log_emergency(self, user_id: str, category: str, snippet: str) -> None:
|
def log_emergency(self, user_id: str, category: str, snippet: str) -> None:
|
||||||
"""Protokolliert ein erkanntes Notfall-Signal (sensibel!)."""
|
"""Protokolliert ein erkanntes Notfall-Signal (sensibel!)."""
|
||||||
|
|
@ -445,6 +450,19 @@ class SQLiteStore(Store):
|
||||||
conn.execute("DELETE FROM users WHERE id = ?", (user_id,))
|
conn.execute("DELETE FROM users WHERE id = ?", (user_id,))
|
||||||
return True
|
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 ------------------------------------------------
|
# ----- Notfall-Protokoll ------------------------------------------------
|
||||||
def log_emergency(self, user_id: str, category: str, snippet: str) -> None:
|
def log_emergency(self, user_id: str, category: str, snippet: str) -> None:
|
||||||
with self._connect() as conn:
|
with self._connect() as conn:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue