diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-14 22:41:02 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-14 22:41:02 +0200 |
| commit | 8d8f85b4bf976249266a89f692408027216711b7 (patch) | |
| tree | 79cdc211a7791e5f25f96879606329eee03ddb11 /packages/meshbay-hub/src/meshbay_hub/api/admin.py | |
| parent | 38f91818f876c51dcd7eb7911b65fc7bf5154c83 (diff) | |
| download | meshbay-8d8f85b4bf976249266a89f692408027216711b7.tar.gz | |
feat(account): a user can delete their own account, an admin can delete one
Both go through the same erasure, so there is one description of what happens
rather than two that drift.
Gone: credentials, email, node key, group memberships, notifications, refresh
tokens, node registrations. The username is released.
Kept, on purpose and stated in the UI: the row itself, emptied, and the IP log
that points at it. Those logs exist for a year to answer legal requests, and a
log that can no longer say whose connection it recorded keeps the data while
losing the only thing it is for. So the account becomes a tombstone rather than a
hole in the table.
Out of reach, also stated: files uploaded to nodes, and the identity keys nodes
pinned. Those are on machines the hub does not command, and only their operators
can remove them — `member unpin` and a delete on their own disk. Saying so in the
confirmation matters more than the button.
Owning groups blocks deletion, with the list. Cascading would delete other
people's groups out from under them; the account holder can hand them over or
delete them first, deliberately.
Self-deletion re-checks the passphrase. A live token may be a borrowed laptop or
a tab left open, and it is not consent to something irreversible. Admin deletion
requires admin rather than moderator: suspension is the reversible moderation
tool and stays one click away.
A deleted account's access token stops working at once — the status check already
refuses anything but "active", which the tests now pin down, because refresh
tokens being gone would otherwise leave up to an hour of usable session.
Tests: 8 covering what survives and what does not, plus a db_session fixture for
assertions that cannot honestly be made through the API.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api/admin.py')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/api/admin.py | 33 |
1 files changed, 32 insertions, 1 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/admin.py b/packages/meshbay-hub/src/meshbay_hub/api/admin.py index 164885d..efebb75 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/admin.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/admin.py @@ -14,7 +14,7 @@ from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from meshbay_hub.auth import decrypt_email -from meshbay_hub.api.deps import require_moderator +from meshbay_hub.api.deps import require_admin, require_moderator from meshbay_hub.api.revocation import get_connected_node_count from meshbay_hub.db.engine import get_db from meshbay_hub.db.models import Group, GroupMember, IPLog, Node, User @@ -181,6 +181,37 @@ async def admin_patch_user( # ── Groups ─────────────────────────────────────────────────────────────────── +@router.delete("/users/{user_id}") +async def admin_delete_user( + user_id: str, + current_user: User = Depends(require_admin), + db: AsyncSession = Depends(get_db), +): + """ + Erase an account. Same erasure a user performs on themselves. + + Admin rather than moderator: suspension is reversible and is the moderation + tool; this is not. Refused for one's own account — an administrator locking + themselves out is a support incident, and there is `DELETE /v1/users/me` for + someone who means it. + """ + from meshbay_hub.api.users import erase_account + + user = await db.get(User, user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + if user.id == current_user.id: + raise HTTPException( + status_code=400, + detail="Use your own account settings to delete your account") + if user.status == "deleted": + raise HTTPException(status_code=410, detail="Account already deleted") + + result = await erase_account(db, user) + log.info("Account %s erased by admin %s", result["username"], current_user.username) + return result + + @router.get("/groups") async def admin_list_groups( current_user: User = Depends(require_moderator), |