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:
parent
8fb5e42e1b
commit
c337ea4309
4 changed files with 46 additions and 1 deletions
|
|
@ -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.
|
||||
|
|
|
|||
20
app/store.py
20
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()
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue