Push role permission changes to signed-in users in real time

Original task (#101): When an admin saves the role editor, role holders
need their permission-driven UI (admin nav items, permission-gated
action buttons, etc.) to refresh without waiting for a page reload.
The server already emitted `apps_changed`, but that event is
semantically about visible apps, not the broader cached permission
state, and the client only invalidated the apps list and `/api/auth/me`.

Changes:
- artifacts/api-server/src/lib/realtime.ts
  - Extracted role-holder resolution (direct user_roles + indirect via
    group_roles -> user_groups) into a private helper.
  - Added `emitRolePermissionsChangedToHolders(roleId)` that emits a
    new `role_permissions_changed` socket event with `{ roleId }` to
    every holder of the role.
- artifacts/api-server/src/routes/roles.ts
  - After `PUT /api/roles/:id/permissions` succeeds, call the new
    emitter alongside the existing `emitAppsChangedToRoleHolders`.
- artifacts/tx-os/src/hooks/use-notifications-socket.ts
  - Subscribed to `role_permissions_changed`. Invalidates
    `getMe`, `listRoles`, `listPermissions`, and—when the payload
    includes the role id—`getRolePermissions`, the role permission
    audit, and role usage queries so admin-only UI re-evaluates without
    a refresh.

Scope notes / deviations:
- The task spec called out `PUT /api/roles/:id/permissions` only, so
  the matching POST/DELETE single-permission endpoints still emit only
  `apps_changed`. Filed as follow-up #150 to keep them consistent.
- No new automated tests added (filed as follow-up #151). Pre-existing
  typecheck errors in artifacts/api-server/src/routes/executive-meetings.ts
  are unrelated and untouched.
- `replit.md` not updated; this is a behavioral refinement of an
  existing socket flow, not an architectural change.

Replit-Task-Id: bd31a9a9-dbf1-497a-8505-96672f92bc79
This commit is contained in:
riyadhafraa
2026-04-29 13:24:16 +00:00
parent cdacb079e9
commit 10a48657c7
3 changed files with 62 additions and 10 deletions
@@ -8,6 +8,11 @@ import {
getListIncomingServiceOrdersQueryKey,
getListAppsQueryKey,
getGetMeQueryKey,
getListRolesQueryKey,
getListPermissionsQueryKey,
getGetRolePermissionsQueryKey,
getGetRolePermissionAuditQueryKey,
getGetRoleUsageQueryKey,
} from "@workspace/api-client-react";
import { useAuth } from "@/contexts/AuthContext";
@@ -66,6 +71,27 @@ export function useNotificationsSocket() {
queryClient.invalidateQueries({ queryKey: getGetMeQueryKey() });
});
socket.on(
"role_permissions_changed",
(payload?: { roleId?: number }) => {
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();
};