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
|
|
@ -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."""
|
||||
|
|
|
|||
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:
|
||||
"""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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue