diff --git a/packages/common/src/services/access/permissions.ts b/packages/common/src/services/access/permissions.ts index 7dfe107aa..7083aa0fb 100644 --- a/packages/common/src/services/access/permissions.ts +++ b/packages/common/src/services/access/permissions.ts @@ -1,8 +1,11 @@ +import { invalidateMultiple } from '@op/cache'; import { db } from '@op/db/client'; import { accessRolePermissionsOnAccessZones, accessRoles, organizationUserToAccessRoles, + profileUserToAccessRoles, + profileUsers, } from '@op/db/schema'; import { permission, toBitField } from 'access-zones'; import { and, eq } from 'drizzle-orm'; @@ -10,6 +13,27 @@ import { and, eq } from 'drizzle-orm'; import { CommonError, NotFoundError } from '../../utils'; import { assertProfileAdmin } from '../assert'; +export async function invalidateProfileUserCacheForRole(roleId: string) { + const affectedUsers = await db + .select({ + profileId: profileUsers.profileId, + authUserId: profileUsers.authUserId, + }) + .from(profileUserToAccessRoles) + .innerJoin( + profileUsers, + eq(profileUserToAccessRoles.profileUserId, profileUsers.id), + ) + .where(eq(profileUserToAccessRoles.accessRoleId, roleId)); + + if (affectedUsers.length > 0) { + await invalidateMultiple({ + type: 'profileUser', + paramsList: affectedUsers.map((u) => [u.profileId, u.authUserId]), + }); + } +} + export type Permissions = { admin: boolean; create: boolean; @@ -152,6 +176,8 @@ export async function updateRolePermissions({ }); } + await invalidateProfileUserCacheForRole(roleId); + return role; } @@ -180,6 +206,9 @@ export async function deleteRole({ await assertProfileAdmin(user, role.profileId); + // Invalidate before delete (cascade will remove the join rows we query) + await invalidateProfileUserCacheForRole(roleId); + // Delete the role (cascade will handle permissions) await db.delete(accessRoles).where(eq(accessRoles.id, roleId)); diff --git a/packages/common/src/services/decision/decisionRoles.ts b/packages/common/src/services/decision/decisionRoles.ts index 51f837ba1..a4e93edc1 100644 --- a/packages/common/src/services/decision/decisionRoles.ts +++ b/packages/common/src/services/decision/decisionRoles.ts @@ -4,6 +4,7 @@ import { permission, toBitField } from 'access-zones'; import { eq } from 'drizzle-orm'; import { CommonError, NotFoundError } from '../../utils'; +import { invalidateProfileUserCacheForRole } from '../access/permissions'; import { assertProfileAdmin } from '../assert'; import { type DecisionRolePermissions, @@ -191,5 +192,7 @@ export async function updateDecisionRoles({ }); } + await invalidateProfileUserCacheForRole(roleId); + return { roleId, decisionPermissions }; } diff --git a/packages/common/src/services/profile/updateProfileUserRole.ts b/packages/common/src/services/profile/updateProfileUserRole.ts index e487d234b..b41308ce0 100644 --- a/packages/common/src/services/profile/updateProfileUserRole.ts +++ b/packages/common/src/services/profile/updateProfileUserRole.ts @@ -1,3 +1,4 @@ +import { invalidate } from '@op/cache'; import { and, db, eq, inArray } from '@op/db/client'; import { profileUserToAccessRoles } from '@op/db/schema'; import type { User } from '@op/supabase/lib'; @@ -102,6 +103,11 @@ export const updateProfileUserRoles = async ({ }); } + await invalidate({ + type: 'profileUser', + params: [targetProfileId, targetProfileUser.authUserId], + }); + // Fetch and return the updated profile user with full relations const updatedProfileUser = await getProfileUserWithRelations(profileUserId); if (!updatedProfileUser) {