feat(admin): DELETE /api/admin/users/{id} — Nutzer und alle Daten löschen
- Store.delete_user() löscht User + Sessions + Nachrichten + Erinnerungen + Nutzung
(atomic, anonymer Nutzer geschützt)
- DELETE /api/admin/users/{user_id} (Admin-Key erforderlich)
→ 200 {"deleted":"..."} | 404 | 400 (anonymous)
- BEDIENUNGSANLEITUNG: $URL-Erklärung am Anfang, § 7.2 vollständig mit
Anlegen/Anzeigen/Löschen-Beispielen inkl. realer Beispiel-Antworten
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5b9b9bbdcf
commit
4970bdc85a
3 changed files with 127 additions and 11 deletions
|
|
@ -6,12 +6,26 @@
|
||||||
> Remote-Deployment: [deploy/README.md](deploy/README.md) ·
|
> Remote-Deployment: [deploy/README.md](deploy/README.md) ·
|
||||||
> Kurzübersicht: [README.md](README.md)
|
> Kurzübersicht: [README.md](README.md)
|
||||||
|
|
||||||
In den Shell-Beispielen steht die Gateway-Adresse als Variable — einmal setzen,
|
### Lesehilfe: `$URL` und `| jq`
|
||||||
dann überall einsetzbar (Port aus deiner `.env`, hier `8003`):
|
|
||||||
|
In allen Shell-Beispielen dieses Handbuchs steht `$URL` als Platzhalter für die
|
||||||
|
Gateway-Adresse. Einmal setzen, dann überall einsetzbar:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export URL=http://localhost:8003
|
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`.
|
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
|
```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 \
|
curl -s -X POST $URL/api/admin/users \
|
||||||
-H "X-Admin-Key: $ADMIN_API_KEY" \
|
-H "X-Admin-Key: $ADMIN_API_KEY" \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d '{"display_name":"Oma Anna"}' | jq
|
-d '{"display_name":"Oma Anna"}' | jq
|
||||||
# → {"user_id":"…","display_name":"Oma Anna","token":"…"}
|
```
|
||||||
|
|
||||||
# Mit Token aufrufen:
|
Beispiel-Antwort:
|
||||||
TOKEN=<token-von-oben>
|
```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
|
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
|
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
|
```bash
|
||||||
curl -s -X PUT $URL/api/me/prefs \
|
curl -s -X PUT $URL/api/me/prefs \
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
-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 |
|
| `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 |
|
||||||
|
|
||||||
## B.6 WebSocket
|
## B.6 WebSocket
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,19 @@ async def create_user(payload: UserCreate):
|
||||||
return UserCreated(user_id=user.id, display_name=user.display_name, token=token)
|
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)])
|
@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."""
|
||||||
|
|
|
||||||
23
app/store.py
23
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:
|
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."""
|
"""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
|
@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!)."""
|
||||||
|
|
@ -422,6 +428,23 @@ class SQLiteStore(Store):
|
||||||
).fetchone()
|
).fetchone()
|
||||||
return int(row["requests"])
|
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 ------------------------------------------------
|
# ----- 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