From 69894b1e7be1b52ca0de7f12e5b6b7799cbd844c Mon Sep 17 00:00:00 2001 From: dschlueter Date: Tue, 30 Jun 2026 09:08:43 +0200 Subject: [PATCH] =?UTF-8?q?feat(admin):=20"Verlauf=20l=C3=B6schen"=20pro?= =?UTF-8?q?=20Nutzer=20(Konto=20bleibt)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Store.delete_user_history (loescht Sessions + Nachrichten eines Nutzers, ohne Konto/Erinnerungen/Usage) + DELETE /api/admin/users/{id}/history (audit-geloggt) + Button im Admin-Nutzer-Bereich. Co-Authored-By: Claude Opus 4.8 --- app/api/admin.py | 12 ++++++++++++ app/store.py | 16 ++++++++++++++++ app/web/app.js | 8 ++++++++ 3 files changed, 36 insertions(+) diff --git a/app/api/admin.py b/app/api/admin.py index 51642d4..cab3535 100644 --- a/app/api/admin.py +++ b/app/api/admin.py @@ -131,6 +131,18 @@ async def delete_user(user_id: str): return {"deleted": user_id} +@router.delete("/admin/users/{user_id}/history", dependencies=[Depends(require_admin)]) +async def delete_user_history(user_id: str, request: Request): + """Loescht NUR den Gespraechsverlauf (Sessions + Nachrichten) eines Nutzers; + Konto, Erinnerungen und Nutzungsdaten bleiben erhalten.""" + store = get_store() + if store.get_user(user_id) is None: + raise HTTPException(status_code=404, detail=f"Nutzer {user_id!r} nicht gefunden.") + deleted = store.delete_user_history(user_id) + log_admin_action(request, "user_history_deleted", user_id=user_id) + return {"user_id": user_id, "deleted_messages": deleted} + + @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. diff --git a/app/store.py b/app/store.py index c3252e7..5194ef3 100644 --- a/app/store.py +++ b/app/store.py @@ -126,6 +126,11 @@ class Store(ABC): Nutzungsdaten). Anonymer Nutzer kann nicht geloescht werden. Liefert True, wenn der Nutzer existierte und geloescht wurde.""" + @abstractmethod + def delete_user_history(self, user_id: str) -> int: + """Loescht NUR den Gespraechsverlauf (Sessions + Nachrichten) eines Nutzers; + Konto, Erinnerungen und Nutzungsdaten bleiben. Liefert die Zahl geloeschter Nachrichten.""" + @abstractmethod def reset_token(self, user_id: str) -> tuple[User, str] | None: """Generiert einen neuen Token fuer den Nutzer; der alte wird sofort ungueltig. @@ -492,6 +497,17 @@ class SQLiteStore(Store): conn.execute("DELETE FROM users WHERE id = ?", (user_id,)) return True + def delete_user_history(self, user_id: str) -> int: + with self._connect() as conn: + cur = conn.execute( + "DELETE FROM messages WHERE session_id IN" + " (SELECT id FROM sessions WHERE user_id = ?)", + (user_id,), + ) + n = cur.rowcount + conn.execute("DELETE FROM sessions WHERE user_id = ?", (user_id,)) + return n + 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() diff --git a/app/web/app.js b/app/web/app.js index f449547..016288d 100644 --- a/app/web/app.js +++ b/app/web/app.js @@ -1193,6 +1193,7 @@ function buildUserCard(u) { + @@ -1409,6 +1410,13 @@ function buildUserCard(u) { } }); + // Verlauf löschen (Konto + Erinnerungen bleiben) + card.querySelector(".btn-history").addEventListener("click", async () => { + if (!confirm(`Gesprächsverlauf von „${u.display_name}" löschen?\nKonto und Erinnerungen bleiben erhalten.`)) return; + const res = await adminFetch(`/api/admin/users/${uid}/history`, "DELETE"); + if (res) alert(`Verlauf gelöscht (${res.deleted_messages ?? 0} Nachrichten).`); + }); + // Löschen card.querySelector(".btn-delete").addEventListener("click", async () => { if (!confirm(`Nutzer „${u.display_name}" und alle Daten unwiderruflich löschen?`)) return;