Push Token Cleanup on Logout All
Business view
Every phone that has the Dentolize app installed and logged in registers itself for push notifications — appointment reminders, new invoices, task assignments, treasury transfers, lab order updates, and more all arrive this way. That registration (a "push token") is saved against the staff member's account and is separate from being logged in; it doesn't automatically expire just because a session ends.
"Logout All Users" is the tool an owner reaches for when something's wrong enough to justify signing out the entire clinic at once — a departing employee whose access needs to be revoked immediately, a lost or stolen device, or a suspected breach. In all of these cases, the expectation is total: nobody should be able to see anything from that clinic anymore, including a stray notification popping up on a screen that's supposed to be locked out.
Before this fix, "Logout All Users" delivered on the session part (nobody could log back in without a password) but quietly missed the notification part. A previously-registered phone would keep buzzing with clinic activity — "New appointment booked," "Invoice #4521 created" — with no way to open the app and see the context, because the session was gone. For a lost-device or compromised-account scenario, that's a real trust gap: leftover notifications can leak sensitive activity summaries to whoever is holding that phone.
This PR closes that gap. Now, when an owner logs everyone out, every staff member's saved push tokens are wiped in the same action — so no device keeps listening after the company-wide logout.
Technical view
Where it lives
packages/server/src/resolvers/mutations/authMutations.js:1206 — the logoutOutAllUsers mutation. It's gated by the DO_ALL permission (packages/server/src/permissions/permissions.js:2787), i.e. only an owner-level account can call it.
What changed
// packages/server/src/resolvers/mutations/authMutations.js:1225-1228
await prisma.user.updateMany({
where: { company: { id: request.session.user.company.id } },
data: { pushNotifications: { set: [] } }
})
This updateMany was inserted between the existing deleteSessions(...) call (which invalidates the Redis-backed session for every active device) and the prisma.userSession.deleteMany(...) call (which removes the DB rows tracking those sessions). The full sequence in logoutOutAllUsers is now:
prisma.user.findMany— fetch every user in
request.session.user.company.id, along with their sessions and multiSessions (each holds a session string, i.e. an Express session ID).
deleteSessions(...)(packages/server/src/utils/helpers.js:1988) — runs
redisClient.del on every sess:<id> key collected from those users. This is what actually kills a live session; Express-session reads/writes sessions from Redis on every request, so removing the key ends the session immediately.
- New:
prisma.user.updateMany(...)— sets every one of those users'
pushNotifications array to empty (String[] field on User, packages/prisma/schema.prisma:430). This is a full company-wide reset of the field, not a per-token removal.
prisma.userSession.deleteMany(...)— deletes theUserSessionrows so
the sessions no longer show up in whatever session-tracking UI reads that table.
Why a blanket set: [] and not a targeted filter
Unlike the single-user logout mutation just above it in the same file (authMutations.js:614-642), which only removes one specific push token (the token belonging to the device that's logging out, looked up via pushNotifications: { has: args.pushNotification }), logoutOutAllUsers has no per-device token to target — it's logging out the whole company at once, so it clears every affected user's entire array. This matches the blast radius of the rest of the mutation: it already unconditionally deletes every session for every user in the company, so wiping every push token for the same set of users is consistent, not broader.
Why this matters for delivery, not just correctness
Push sends are keyed purely off user.pushNotifications, independent of session state. Every notification call site in packages/server/src/resolvers/mutations/mutationUtils/notificationUtils.js and operationUtils.js (appointments, invoices, treasury, tasks, operations, lab orders, free forms, prescriptions, etc.) reads user.pushNotifications.slice(-20) and fires an Expo push to each token in that list, with no session check. Leaving stale tokens in place after a company-wide logout meant those code paths would keep sending real business data to a device the owner explicitly meant to cut off.
What this does not touch
- Patient push tokens. The
Patientmodel has its own
pushNotifications field (packages/prisma/schema.prisma:966) used by the patient-facing app/portal. logoutOutAllUsers only operates on User records (staff), so patient notification tokens are untouched — correctly, since this action doesn't log out patients.
- Self-logout (
logoutmutation). That path already cleaned up its own
device's token before this PR and needed no change.
- A one-off maintenance script,
packages/server/src/generateServerData/deleteAllPushTokens.js, already existed as a manual, unscoped (all companies, not just one) way to wipe every User.pushNotifications array. It's a standalone ops script, not wired into any mutation — this PR is what makes the equivalent cleanup happen automatically, scoped to one company, whenever that company's owner clicks "Logout All Users."