Dentolize · Clear Push Tokens on Logout Walkthrough
On this pageBusiness viewTechnical view

The mutations this PR changes

Business view

Nine different actions in Dentolize end a user's session as a side effect of doing something else. This PR touches all nine, adding the same one behavior to each: also forget that user's phone(s). The table below groups them by who triggers them and what they're normally used for.

Who does itActionWhat it's really for
Self-serviceChange your own passwordRoutine security hygiene
Self-serviceTurn on two-factor authenticationRoutine security hygiene
Self-serviceTurn off two-factor authenticationRoutine security hygiene
Anyone with a reset linkComplete a "forgot password" resetAccount recovery
Manager / ownerEdit a staff member's profileDay-to-day HR/admin upkeep
Manager / ownerChange what a permission group can doRole/permission management
Manager / ownerDelete a permission groupRole/permission cleanup
Dentolize supportForce-disable a user's 2FASupport unlocking a locked-out user
Dentolize supportSuspend a clinic's accountBilling/compliance enforcement

In every case, the person being signed out no longer has a working session — but until this PR, their phone kept a live push-notification subscription open. This fix makes "signed out" mean "signed out," including on the notification channel.

Technical view

Each mutation lives in packages/server/src/resolvers/mutations/. All line numbers below are from the current branch head.

updateAuthDetails — self-service password change

packages/server/src/resolvers/mutations/authMutations.js:650

prisma.user.update({
  where: { id: request.session.user.id },
  data: { password, shouldResetPassword: false, pushNotifications: { set: [] } },
  ...
}),
prisma.userSession.deleteMany({ ... })

(authMutations.js:687-701, inside one $transaction.) Permission: isAuthenticated (permissions.js:2758).

resetPassword — forgot-password flow

packages/server/src/resolvers/mutations/authMutations.js:843

prisma.user.update({
  where: { id: user.id },
  data: { password, validated: true, resetPasswordToken: null, pushNotifications: { set: [] } }
}),
prisma.userSession.deleteMany({ ... })

(authMutations.js:878-884.) No isAuthenticated check — the caller isn't logged in yet, they're proving identity via the emailed token (permissions.js:2752).

confirm2FAToken — turning 2FA on

packages/server/src/resolvers/mutations/authMutations.js:979

prisma.user.update({
  data: { twoFactorAuth: true, twoFactorSecret: args.secret, twoFactorCodes: [], pushNotifications: { set: [] } },
  ...
}),
prisma.userSession.deleteMany({ ... })

(authMutations.js:1003-1018.)

disable2FA — self-service turning 2FA off

packages/server/src/resolvers/mutations/authMutations.js:1037

prisma.user.update({
  data: { twoFactorAuth: false, twoFactorSecret: null, twoFactorCodes: [], pushNotifications: { set: [] } },
  ...
}),
prisma.userSession.deleteMany({ ... })

(authMutations.js:1074-1092.)

editGroup — changing a permission group's rights

packages/server/src/resolvers/mutations/companyMutations.js:866

await prisma.userSession.deleteMany({
  where: { OR: [{ user: { groupId: args.group } }, { users: { some: { groupId: args.group } } }] }
})

await prisma.user.updateMany({ where: { groupId: args.group }, data: { pushNotifications: { set: [] } } })

return prisma.group.update({ ... })

(companyMutations.js:894-900.) Every member of the group is affected, since a permissions change invalidates all their sessions, not just the one making the edit. Permission: hasPermission('EDIT_GROUPS') (permissions.js:2790).

deleteGroup — deleting a permission group

packages/server/src/resolvers/mutations/companyMutations.js:929

prisma.user.updateMany({
  where: { groupId: args.group },
  data: { groupId: args.newGroup, pushNotifications: { set: [] } }
}),
prisma.group.delete({ where: { id: args.group }, select: { id: true } }),
prisma.userSession.deleteMany({
  where: { OR: [{ user: { groupId: args.group } }, { users: { some: { groupId: args.group } } }] }
})

(companyMutations.js:981-988, all in one $transaction.) The push-token clear is deliberately folded into the same updateMany that reassigns members to args.newGroup, rather than added as a separate statement afterward — a separate statement would run after the reassignment (transaction array order) and match zero users, the same way the pre-existing userSession.deleteMany on this line already does once the group id has changed underneath it. The PR description calls this out explicitly. Permission: hasPermission('DELETE_GROUPS') (permissions.js:2789).

editUser — editing any staff member's profile

packages/server/src/resolvers/mutations/userMutations.js:425

prisma.user.update({
  where: { id: args.user },
  data: { ...data, pushNotifications: { set: [] }, job: args.job, salary: args.salary, name: args.name.trim(), ... }
}),
prisma.userSession.deleteMany({ where: { OR: [{ userId: args.user }, { users: { some: { id: args.user } } }] } })

(userMutations.js:464-551.) Note this fires on every save of the Edit User form — changing a salary or job title force-logs-out the user today, and now also clears their push tokens, exactly the same as changing their permission group would. Permission: hasPermission('EDIT_USERS') plus several ownership/company checks (permissions.js:2901-2909).

adminDisableTwoFactor — support forcing 2FA off

packages/server/src/resolvers/mutations/adminMutations.js:1177

prisma.user.update({
  where: { id: args.user },
  data: { twoFactorAuth: false, twoFactorSecret: null, twoFactorCodes: [], pushNotifications: { set: [] } },
  ...
}),
prisma.userSession.deleteMany({ where: { OR: [{ userId: args.user }, { users: { some: { id: args.user } } }] } })

(adminMutations.js:1196-1210.) Internal-admin only — isAdmin (permissions.js:3791), not reachable from the clinic-facing app.

disableCompany — suspending a clinic

packages/server/src/resolvers/mutations/adminMutations.js:398

args.disabled
  ? prisma.user.updateMany({ where: { companyId: args.company }, data: { pushNotifications: { set: [] } } })
  : null,

(adminMutations.js:422-424, one entry in the transaction array used by disableCompany.) This one only fires when disabling a company (args.disabled === true) — re-enabling a company doesn't touch push tokens, which is correct, since nothing needs cleaning up on the way back in. Reachable either as isAdmin or, for a clinic's own owner, hasPermission('DO_ALL') (permissions.js:3785).

Worth noting as a boundary of this PR: disableCompany invalidates sessions only by clearing the Redis-cached session (deleteSessions, adminMutations.js:405-412); it does not delete the corresponding UserSession database rows the way the other eight mutations do. That's a pre-existing gap in session bookkeeping, unrelated to push tokens, and this PR does not change it.