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 it | Action | What it's really for |
|---|---|---|
| Self-service | Change your own password | Routine security hygiene |
| Self-service | Turn on two-factor authentication | Routine security hygiene |
| Self-service | Turn off two-factor authentication | Routine security hygiene |
| Anyone with a reset link | Complete a "forgot password" reset | Account recovery |
| Manager / owner | Edit a staff member's profile | Day-to-day HR/admin upkeep |
| Manager / owner | Change what a permission group can do | Role/permission management |
| Manager / owner | Delete a permission group | Role/permission cleanup |
| Dentolize support | Force-disable a user's 2FA | Support unlocking a locked-out user |
| Dentolize support | Suspend a clinic's account | Billing/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.