feat(admin): "Verlauf löschen" pro Nutzer (Konto bleibt)

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 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-30 09:08:43 +02:00
commit 69894b1e7b
3 changed files with 36 additions and 0 deletions

View file

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

View file

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

View file

@ -1193,6 +1193,7 @@ function buildUserCard(u) {
<button class="btn-rename text-xs rounded-lg border border-slate-300 dark:border-slate-600 px-2.5 py-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">Umbenennen</button>
<button class="btn-admin text-xs rounded-lg border px-2.5 py-1.5 transition-colors"></button>
<button class="btn-token text-xs rounded-lg border border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-300 px-2.5 py-1.5 hover:bg-amber-50 dark:hover:bg-amber-900/20 transition-colors">Link erneuern</button>
<button class="btn-history text-xs rounded-lg border border-orange-300 dark:border-orange-700 text-orange-700 dark:text-orange-300 px-2.5 py-1.5 hover:bg-orange-50 dark:hover:bg-orange-900/20 transition-colors">Verlauf löschen</button>
<button class="btn-delete text-xs rounded-lg border border-red-300 dark:border-red-800 text-red-600 dark:text-red-400 px-2.5 py-1.5 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors">Löschen</button>
</div>
</div>
@ -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;