diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-15 14:24:35 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-15 14:24:35 +0200 |
| commit | 2dbff20818d171242a15e3567e14292c0d63aea2 (patch) | |
| tree | dcfe9607809c95ff15435ec684f0954a2d777798 /packages/meshbay-hub/src/meshbay_hub/api/users.py | |
| parent | 5949532e6450d1aebc681caa8db2192a43b11f12 (diff) | |
| download | meshbay-2dbff20818d171242a15e3567e14292c0d63aea2.tar.gz | |
fix(hub): a phone's poll secret ends with the account's sessions
The secret a phone polls /v1/push/poll with is a credential of its own,
outside any session, and it outlived the ones that should have ended it:
"sign out everywhere" — what someone who lost a phone presses — a
passphrase change and an admin revocation left every push row in place, so
the lost phone went on reading "X posted in <group>" lines. Each now deletes
the account's rows, as a reset and an erasure already did, and the poll
refuses a revoked or deleted account whatever deleted the row or not. A
suspended one still reads: the line telling it so is one of them.
A phone still in hand is not affected for long: a refused poll makes it
forget the row, and the page registers it again at its next start.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api/users.py')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/api/users.py | 8 |
1 files changed, 7 insertions, 1 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/users.py b/packages/meshbay-hub/src/meshbay_hub/api/users.py index 904d7a0..eb053b7 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/users.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/users.py @@ -1140,13 +1140,17 @@ async def revoke_all_sessions( Access tokens already issued run out on their own, within their lifetime. A desktop device key is not a session and is left alone — removing the - device is what stops it signing back in. + device is what stops it signing back in. A phone's push row is dropped: + its poll secret is a credential of its own, and "everywhere" is what + someone who lost a phone presses. A phone still in hand registers again + at its next start. """ result = await db.execute( update(RefreshToken) .where(RefreshToken.user_id == current_user.id, RefreshToken.revoked.is_(False)) .values(revoked=True)) + await db.execute(delete(PushSubscription).where(PushSubscription.user_id == current_user.id)) db.add(IPLog(user_id=current_user.id, event="sessions_revoked", ip_address=client_ip(request))) await db.commit() @@ -1180,10 +1184,12 @@ async def change_password( # Every existing session goes stale. Revoke all refresh tokens, then hand # this caller a fresh pair so the tab that made the change stays signed in; # other browsers fail their next renewal and drop to the sign-in form. + # Phones' push rows go too — their poll secrets would outlive the sessions. await db.execute( update(RefreshToken) .where(RefreshToken.user_id == current_user.id) .values(revoked=True)) + await db.execute(delete(PushSubscription).where(PushSubscription.user_id == current_user.id)) memberships = await db.execute( select(GroupMember.group_id).where(GroupMember.user_id == current_user.id)) |