From c337ea4309b2d3c364da3d69bc92bd876b4b0e02 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Tue, 30 Jun 2026 14:43:56 +0200 Subject: [PATCH] =?UTF-8?q?feat(admin):=20Bulk=20"alle=20Nutzerdaten=20l?= =?UTF-8?q?=C3=B6schen=20(Konten=20behalten)"=20+=20Nutzerliste=20alphabet?= =?UTF-8?q?isch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DELETE /api/admin/user-data: loescht fuer ALLE Nutzer Verlauf, Statistik (usage/web_search_usage/cost_usage) und Erinnerungen; Konten bleiben. Gefahrenzone-Knopf mit doppelter Bestaetigung, audit-geloggt. - list_users jetzt ORDER BY LOWER(display_name) -> Nutzerkarten alphabetisch (ohne Gross-/Kleinschreibung). Co-Authored-By: Claude Opus 4.8 --- app/api/admin.py | 9 +++++++++ app/store.py | 20 +++++++++++++++++++- app/web/app.js | 11 +++++++++++ app/web/index.html | 7 +++++++ 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/app/api/admin.py b/app/api/admin.py index dadea8a..b52d112 100644 --- a/app/api/admin.py +++ b/app/api/admin.py @@ -166,6 +166,15 @@ async def delete_user_memories(user_id: str, request: Request): return {"user_id": user_id, "deleted_memories": deleted} +@router.delete("/admin/user-data", dependencies=[Depends(require_admin)]) +async def wipe_all_user_data(request: Request): + """Loescht fuer ALLE Nutzer Verlauf, Statistik und Erinnerungen; die Konten + selbst bleiben erhalten. Nicht umkehrbar.""" + counts = get_store().wipe_all_user_data() + log_admin_action(request, "all_user_data_wiped") + return counts + + @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 de33432..aeb68d2 100644 --- a/app/store.py +++ b/app/store.py @@ -156,6 +156,11 @@ class Store(ABC): def delete_user_memories(self, user_id: str) -> int: """Loescht alle Erinnerungen eines Nutzers. Liefert die Zahl geloeschter Erinnerungen.""" + @abstractmethod + def wipe_all_user_data(self) -> dict: + """Loescht fuer ALLE Nutzer Verlauf, Statistik und Erinnerungen; Konten bleiben. + Liefert ein Dict mit Anzahlen je Kategorie.""" + @abstractmethod def reset_token(self, user_id: str) -> tuple[User, str] | None: """Generiert einen neuen Token fuer den Nutzer; der alte wird sofort ungueltig. @@ -354,7 +359,7 @@ class SQLiteStore(Store): def list_users(self) -> list[User]: with self._connect() as conn: rows = conn.execute( - "SELECT * FROM users WHERE id != ? ORDER BY created_at", + "SELECT * FROM users WHERE id != ? ORDER BY LOWER(display_name)", (ANONYMOUS_USER_ID,), ).fetchall() return [self._row_to_user(row) for row in rows] @@ -607,6 +612,19 @@ class SQLiteStore(Store): cur = conn.execute("DELETE FROM memories WHERE user_id = ?", (user_id,)) return cur.rowcount + def wipe_all_user_data(self) -> dict: + with self._connect() as conn: + counts = { + "messages": conn.execute("DELETE FROM messages").rowcount, + "sessions": conn.execute("DELETE FROM sessions").rowcount, + "memories": conn.execute("DELETE FROM memories").rowcount, + } + stats = 0 + for tbl in ("usage", "web_search_usage", "cost_usage"): + stats += conn.execute(f"DELETE FROM {tbl}").rowcount + counts["stats"] = stats + return counts + 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 f487297..c36a1ba 100644 --- a/app/web/app.js +++ b/app/web/app.js @@ -1176,6 +1176,17 @@ $("#create-user-form").addEventListener("submit", async (e) => { } }); +// Alle Nutzerdaten löschen (Konten bleiben) — doppelte Sicherheitsabfrage. +$("#btn-wipe-all").addEventListener("click", async () => { + if (!confirm("Wirklich für ALLE Nutzer Verlauf, Statistik und Erinnerungen löschen?\nDie Konten bleiben erhalten. Dies kann nicht rückgängig gemacht werden.")) return; + if (!confirm("Letzte Warnung: Alle Nutzerdaten werden jetzt gelöscht. Fortfahren?")) return; + const res = await adminFetch("/api/admin/user-data", "DELETE"); + if (res) { + alert(`Gelöscht: ${res.messages ?? 0} Nachrichten, ${res.sessions ?? 0} Sessions, ${res.stats ?? 0} Statistik-Zeilen, ${res.memories ?? 0} Erinnerungen.`); + loadAdminUsers(); + } +}); + function buildUserCard(u) { const card = document.createElement("div"); card.className = "bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4"; diff --git a/app/web/index.html b/app/web/index.html index 53aec24..6a0017b 100644 --- a/app/web/index.html +++ b/app/web/index.html @@ -85,6 +85,13 @@ + +
+

Alle Nutzerdaten löschen

+

Löscht für alle Nutzer Verlauf, Statistik (Anfragen/Web-Suchen/Kosten) und Erinnerungen. Die Konten selbst bleiben erhalten. Nicht umkehrbar.

+ +
+

lade …