diff --git a/artifacts/api-server/src/lib/realtime.ts b/artifacts/api-server/src/lib/realtime.ts index d1705902..1dfb2276 100644 --- a/artifacts/api-server/src/lib/realtime.ts +++ b/artifacts/api-server/src/lib/realtime.ts @@ -15,14 +15,7 @@ export async function emitAppsChangedToUsers(userIds: number[]): Promise { } } -/** - * Resolve every user that currently carries `roleId`, either directly via - * `user_roles` or indirectly via `group_roles` -> `user_groups`, and emit - * `apps_changed` to each of them. - */ -export async function emitAppsChangedToRoleHolders(roleId: number): Promise { - if (!Number.isInteger(roleId)) return; - +async function getRoleHolderUserIds(roleId: number): Promise { const directRows = await db .select({ userId: userRolesTable.userId }) .from(userRolesTable) @@ -41,9 +34,38 @@ export async function emitAppsChangedToRoleHolders(roleId: number): Promise r.userId), ...indirectRows.map((r) => r.userId), ]; +} + +/** + * Resolve every user that currently carries `roleId`, either directly via + * `user_roles` or indirectly via `group_roles` -> `user_groups`, and emit + * `apps_changed` to each of them. + */ +export async function emitAppsChangedToRoleHolders(roleId: number): Promise { + if (!Number.isInteger(roleId)) return; + const userIds = await getRoleHolderUserIds(roleId); await emitAppsChangedToUsers(userIds); } + +/** + * Notify every holder of `roleId` that the role's permission set has been + * updated, so the client can invalidate cached `/api/auth/me` and + * role/permission queries and re-evaluate permission-driven UI without a + * page refresh. + */ +export async function emitRolePermissionsChangedToHolders( + roleId: number, +): Promise { + if (!Number.isInteger(roleId)) return; + const userIds = await getRoleHolderUserIds(roleId); + const unique = Array.from(new Set(userIds.filter((id) => Number.isInteger(id)))); + if (unique.length === 0) return; + const { io } = await import("../index.js"); + for (const userId of unique) { + io.to(`user:${userId}`).emit("role_permissions_changed", { roleId }); + } +} diff --git a/artifacts/api-server/src/routes/roles.ts b/artifacts/api-server/src/routes/roles.ts index 5421acc3..d4652915 100644 --- a/artifacts/api-server/src/routes/roles.ts +++ b/artifacts/api-server/src/routes/roles.ts @@ -19,7 +19,10 @@ import { UpdateRoleBody, ReplaceRolePermissionsBody, } from "@workspace/api-zod"; -import { emitAppsChangedToRoleHolders } from "../lib/realtime.js"; +import { + emitAppsChangedToRoleHolders, + emitRolePermissionsChangedToHolders, +} from "../lib/realtime.js"; const router: IRouter = Router(); @@ -566,6 +569,7 @@ router.put("/roles/:id/permissions", requireAdmin, async (req, res): Promise { + queryClient.invalidateQueries({ queryKey: getGetMeQueryKey() }); + queryClient.invalidateQueries({ queryKey: getListPermissionsQueryKey() }); + queryClient.invalidateQueries({ queryKey: getListRolesQueryKey() }); + const roleId = payload?.roleId; + if (typeof roleId === "number") { + queryClient.invalidateQueries({ + queryKey: getGetRolePermissionsQueryKey(roleId), + }); + queryClient.invalidateQueries({ + queryKey: getGetRolePermissionAuditQueryKey(roleId), + }); + queryClient.invalidateQueries({ + queryKey: getGetRoleUsageQueryKey(roleId), + }); + } + }, + ); + return () => { socket.disconnect(); };