From e3c68b3416f8ea2c033d41ac263552833370ba18 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Fri, 11 Sep 2026 11:25:38 +0200 Subject: fix(hub): account deletion left device keys and swarm sources behind erase_account cleared memberships, notifications, tokens and node registrations, but not user_devices or swarm_sources. A device key left on the tombstone still belonged to it, so an account created later from the same desktop installation - which keeps its private half - was refused that device with a 409 that only reached the console. swarm_sources is keyed by the user id despite its column name and carries the node's ip:port. Both are now erased, which is what the privacy statement promises: every account row goes except the one-year IP log. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01D9MCBBWSm9GhBESmqzJxNy --- packages/meshbay-hub/src/meshbay_hub/api/users.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/api') diff --git a/packages/meshbay-hub/src/meshbay_hub/api/users.py b/packages/meshbay-hub/src/meshbay_hub/api/users.py index 56208a0..f291f59 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/users.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/users.py @@ -33,7 +33,7 @@ from meshbay_hub.config import HubConfig from meshbay_hub.db.engine import get_db from meshbay_hub.db.models import ( EmailVerification, Group, GroupMember, IPLog, Node, Notification, - RefreshToken, User, UserDevice, UserPreference, + RefreshToken, SwarmSource, User, UserDevice, UserPreference, ) log = logging.getLogger(__name__) @@ -1084,7 +1084,14 @@ async def erase_account(db: AsyncSession, user: User) -> dict: Erase an account, keeping only what the law asked us to keep. Gone: credentials, email, node key, group memberships, notifications, refresh - tokens, node registrations. The username is released. + tokens, node registrations, device keys, public-swarm sources. The username + is released. + + Device keys go even though the desktop client keeps its private half: left + behind, the key still belongs to this tombstone, so an account created later + from the same installation is refused that device ("belongs to another + account"). Swarm sources are keyed by the *user* id and carry the node's + ip:port. Kept: the row itself, emptied, and the IP log that points at it. Those logs exist for one year to answer legal requests, and a log that cannot say whose @@ -1112,6 +1119,8 @@ async def erase_account(db: AsyncSession, user: User) -> dict: await db.execute(delete(Notification).where(Notification.user_id == user.id)) await db.execute(delete(RefreshToken).where(RefreshToken.user_id == user.id)) await db.execute(delete(Node).where(Node.user_id == user.id)) + await db.execute(delete(UserDevice).where(UserDevice.user_id == user.id)) + await db.execute(delete(SwarmSource).where(SwarmSource.node_id == user.id)) await db.execute(delete(EmailVerification).where(EmailVerification.user_id == user.id)) username = user.username -- cgit v1.2.3