feat(admin): Bulk "alle Nutzerdaten löschen (Konten behalten)" + Nutzerliste alphabetisch

- 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 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-30 14:43:56 +02:00
commit c337ea4309
4 changed files with 46 additions and 1 deletions

View file

@ -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.

View file

@ -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()

View file

@ -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";

View file

@ -85,6 +85,13 @@
</div>
</div>
<!-- Wartung: alle Nutzerdaten löschen (Konten bleiben) -->
<div class="bg-white dark:bg-slate-800 rounded-xl border border-red-200 dark:border-red-900 p-4">
<h3 class="text-sm font-medium text-red-700 dark:text-red-400 mb-1">Alle Nutzerdaten löschen</h3>
<p class="text-xs text-slate-500 dark:text-slate-400 mb-3">Löscht für <strong>alle</strong> Nutzer Verlauf, Statistik (Anfragen/Web-Suchen/Kosten) und Erinnerungen. Die Konten selbst bleiben erhalten. Nicht umkehrbar.</p>
<button id="btn-wipe-all" class="text-xs rounded-lg border border-red-300 dark:border-red-800 text-red-600 dark:text-red-400 px-3 py-1.5 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors">Alle Daten löschen (Konten behalten)</button>
</div>
<!-- Nutzerliste -->
<div id="admin-user-cards" class="space-y-3">
<p class="text-sm text-slate-400 text-center py-4">lade …</p>