diff --git a/backend/core/database.py b/backend/core/database.py index 8dcca5b..acf109b 100644 --- a/backend/core/database.py +++ b/backend/core/database.py @@ -233,6 +233,19 @@ async def update_user( return user +async def delete_user( + user_id: uuid.UUID, + session: AsyncSession, +) -> bool: + """Delete a user by their UUID. Returns ``True`` if deleted, ``False`` if not found.""" + user = await session.get(User, user_id) + if user is None: + return False + await session.delete(user) + await session.flush() + return True + + # ── Authentication (Valkey tokens + bcrypt) ─────────────────────────────── _AUTH_TOKEN_KEY = "auth_token:{token}" diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 31bb391..aae876d 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -13,6 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from backend.core.database import ( delete_session, + delete_user as db_delete_user, get_admin_stats, get_session_messages, get_session_user_id, @@ -171,6 +172,18 @@ async def admin_update_user( return _user_to_admin_out(user, session_count=len(sids)) +@router.delete("/users/{user_id}", status_code=204) +async def admin_delete_user( + user_id: uuid.UUID, + _admin_id: str = Depends(require_admin), + session: AsyncSession = Depends(get_db_session), +) -> None: + """Permanently delete a user and all their data.""" + deleted = await db_delete_user(user_id, session=session) + if not deleted: + raise HTTPException(status_code=404, detail="User not found") + + @router.get("/sessions", response_model=list[AdminSessionOut]) async def admin_list_sessions( _admin_id: str = Depends(require_admin), diff --git a/backend/routes/auth.py b/backend/routes/auth.py index de065da..1ca0458 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -143,8 +143,8 @@ async def me_endpoint( Requires ``Authorization: Bearer `` header. """ - from backend.core.database import get_session as _get_db from backend.core.database import get_user + from backend.core.database import open_session as _open_db user_id = await resolve_auth_token(token) if user_id is None: @@ -153,7 +153,7 @@ async def me_endpoint( detail="Invalid or expired token", ) - async with _get_db() as session: + async with _open_db() as session: user = await get_user(uuid_obj(user_id), session=session) if user is None or not user.is_active: diff --git a/frontend/src/Admin.tsx b/frontend/src/Admin.tsx index ea7c027..989f8f8 100644 --- a/frontend/src/Admin.tsx +++ b/frontend/src/Admin.tsx @@ -5,6 +5,7 @@ import { adminUpdateUser, adminListSessions, adminDeleteSession, + adminDeleteUser, type AdminStats, type AdminUserData, type AdminSessionData, @@ -82,6 +83,16 @@ export default function AdminDashboard({ onBack }: AdminProps) { } }; + const handleDeleteUser = async (userId: string) => { + if (!confirm("Permanently delete this user and all their data?")) return; + try { + await adminDeleteUser(userId); + await loadAll(); + } catch (err: unknown) { + alert(err instanceof Error ? err.message : "Delete failed"); + } + }; + const handleDeleteSession = async (sessionId: string) => { if (!confirm("Delete this session and all its messages?")) return; try { @@ -172,6 +183,7 @@ export default function AdminDashboard({ onBack }: AdminProps) { users={users} onRoleChange={handleRoleChange} onToggleActive={handleToggleActive} + onDeleteUser={handleDeleteUser} /> ) : ( @@ -307,10 +319,12 @@ function UsersTab({ users, onRoleChange, onToggleActive, + onDeleteUser, }: { users: AdminUserData[]; onRoleChange: (id: string, role: string) => void; onToggleActive: (id: string, current: boolean) => void; + onDeleteUser: (id: string) => void; }) { return (
@@ -323,6 +337,7 @@ function UsersTab({ Status Sessions Created + @@ -366,13 +381,22 @@ function UsersTab({ {new Date(u.created_at).toLocaleDateString()} - + + + + ))} diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 9928696..d88ca60 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -294,6 +294,17 @@ export async function adminListSessions(): Promise { return res.json(); } +export async function adminDeleteUser(userId: string): Promise { + const res = await fetch(`${API_BASE}/admin/users/${userId}`, { + method: "DELETE", + headers: { ...authHeaders() }, + }); + if (!res.ok) { + const body = await res.json().catch(() => null); + throw new Error(body?.detail ?? `HTTP ${res.status}`); + } +} + export async function adminDeleteSession(sessionId: string): Promise { const res = await fetch(`${API_BASE}/admin/sessions/${sessionId}`, { method: "DELETE",