From 73dcf539ed632a9f96cf82caf7642e9e34c65f6c Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 10 Apr 2025 00:16:20 +0530 Subject: [PATCH] feat: allow role based filtering on the frontend (#11246) This pull request introduces frontend role filtering to allStatusChat getter. The key changes include the addition of a new helper function to get the user's role, updates to the conversation filtering logic to incorporate role and permissions, and the addition of unit tests for the new filtering logic. --------- Co-authored-by: Muhsin Keloth --- .../dashboard/helper/permissionsHelper.js | 9 + .../store/modules/conversations/getters.js | 24 +- .../store/modules/conversations/helpers.js | 45 +++ .../conversations/specs/helpers.spec.js | 276 ++++++++++++++++++ 4 files changed, 351 insertions(+), 3 deletions(-) create mode 100644 app/javascript/dashboard/store/modules/conversations/specs/helpers.spec.js diff --git a/app/javascript/dashboard/helper/permissionsHelper.js b/app/javascript/dashboard/helper/permissionsHelper.js index 9200677bc..25f97c038 100644 --- a/app/javascript/dashboard/helper/permissionsHelper.js +++ b/app/javascript/dashboard/helper/permissionsHelper.js @@ -16,6 +16,15 @@ export const getUserPermissions = (user, accountId) => { return currentAccount.permissions || []; }; +export const getUserRole = (user, accountId) => { + const currentAccount = getCurrentAccount(user, accountId) || {}; + if (currentAccount.custom_role_id) { + return 'custom_role'; + } + + return currentAccount.role || 'agent'; +}; + const isPermissionsPresentInRoute = route => route.meta && route.meta.permissions; diff --git a/app/javascript/dashboard/store/modules/conversations/getters.js b/app/javascript/dashboard/store/modules/conversations/getters.js index 33f140fad..f5b83e546 100644 --- a/app/javascript/dashboard/store/modules/conversations/getters.js +++ b/app/javascript/dashboard/store/modules/conversations/getters.js @@ -1,7 +1,11 @@ import { MESSAGE_TYPE } from 'shared/constants/messages'; -import { applyPageFilters, sortComparator } from './helpers'; +import { applyPageFilters, applyRoleFilter, sortComparator } from './helpers'; import filterQueryGenerator from 'dashboard/helper/filterQueryGenerator'; import { matchesFilters } from './helpers/filterHelpers'; +import { + getUserPermissions, + getUserRole, +} from '../../../helper/permissionsHelper'; import camelcaseKeys from 'camelcase-keys'; export const getSelectedChatConversation = ({ @@ -77,10 +81,24 @@ const getters = { return isUnAssigned && shouldFilter; }); }, - getAllStatusChats: _state => activeFilters => { + getAllStatusChats: (_state, _, __, rootGetters) => activeFilters => { + const currentUser = rootGetters.getCurrentUser; + const currentUserId = rootGetters.getCurrentUser.id; + const currentAccountId = rootGetters.getCurrentAccountId; + + const permissions = getUserPermissions(currentUser, currentAccountId); + const userRole = getUserRole(currentUser, currentAccountId); + return _state.allConversations.filter(conversation => { const shouldFilter = applyPageFilters(conversation, activeFilters); - return shouldFilter; + const allowedForRole = applyRoleFilter( + conversation, + userRole, + permissions, + currentUserId + ); + + return shouldFilter && allowedForRole; }); }, getChatListLoadingStatus: ({ listLoadingStatus }) => listLoadingStatus, diff --git a/app/javascript/dashboard/store/modules/conversations/helpers.js b/app/javascript/dashboard/store/modules/conversations/helpers.js index 0063c8cfc..ebbdcbe64 100644 --- a/app/javascript/dashboard/store/modules/conversations/helpers.js +++ b/app/javascript/dashboard/store/modules/conversations/helpers.js @@ -62,6 +62,51 @@ export const applyPageFilters = (conversation, filters) => { return shouldFilter; }; +/** + * Filters conversations based on user role and permissions + * + * @param {Object} conversation - The conversation object to check permissions for + * @param {string} role - The user's role (administrator, agent, etc.) + * @param {Array} permissions - List of permission strings the user has + * @param {number|string} currentUserId - The ID of the current user + * @returns {boolean} - Whether the user has permissions to access this conversation + */ +export const applyRoleFilter = ( + conversation, + role, + permissions, + currentUserId +) => { + // the role === "agent" check is typically not correct on it's own + // the backend handles this by checking the custom_role_id at the user model + // here however, the `getUserRole` returns "custom_role" if the id is present, + // so we can check the role === "agent" directly + if (['administrator', 'agent'].includes(role)) { + return true; + } + + // Check for full conversation management permission + if (permissions.includes('conversation_manage')) { + return true; + } + + const conversationAssignee = conversation.meta.assignee; + const isUnassigned = !conversationAssignee; + const isAssignedToUser = conversationAssignee?.id === currentUserId; + + // Check unassigned management permission + if (permissions.includes('conversation_unassigned_manage')) { + return isUnassigned || isAssignedToUser; + } + + // Check participating conversation management permission + if (permissions.includes('conversation_participating_manage')) { + return isAssignedToUser; + } + + return false; +}; + const SORT_OPTIONS = { last_activity_at_asc: ['sortOnLastActivityAt', 'asc'], last_activity_at_desc: ['sortOnLastActivityAt', 'desc'], diff --git a/app/javascript/dashboard/store/modules/conversations/specs/helpers.spec.js b/app/javascript/dashboard/store/modules/conversations/specs/helpers.spec.js new file mode 100644 index 000000000..f9118281e --- /dev/null +++ b/app/javascript/dashboard/store/modules/conversations/specs/helpers.spec.js @@ -0,0 +1,276 @@ +import { describe, it, expect } from 'vitest'; +import { applyRoleFilter } from '../helpers'; + +describe('Conversation Helpers', () => { + describe('#applyRoleFilter', () => { + // Test data for conversations + const conversationWithAssignee = { + meta: { + assignee: { + id: 1, + }, + }, + }; + + const conversationWithDifferentAssignee = { + meta: { + assignee: { + id: 2, + }, + }, + }; + + const conversationWithoutAssignee = { + meta: { + assignee: null, + }, + }; + + // Test for administrator role + it('always returns true for administrator role regardless of permissions', () => { + const role = 'administrator'; + const permissions = []; + const currentUserId = 1; + + expect( + applyRoleFilter( + conversationWithAssignee, + role, + permissions, + currentUserId + ) + ).toBe(true); + expect( + applyRoleFilter( + conversationWithDifferentAssignee, + role, + permissions, + currentUserId + ) + ).toBe(true); + expect( + applyRoleFilter( + conversationWithoutAssignee, + role, + permissions, + currentUserId + ) + ).toBe(true); + }); + + // Test for agent role + it('always returns true for agent role regardless of permissions', () => { + const role = 'agent'; + const permissions = []; + const currentUserId = 1; + + expect( + applyRoleFilter( + conversationWithAssignee, + role, + permissions, + currentUserId + ) + ).toBe(true); + expect( + applyRoleFilter( + conversationWithDifferentAssignee, + role, + permissions, + currentUserId + ) + ).toBe(true); + expect( + applyRoleFilter( + conversationWithoutAssignee, + role, + permissions, + currentUserId + ) + ).toBe(true); + }); + + // Test for custom role with 'conversation_manage' permission + it('returns true for any user with conversation_manage permission', () => { + const role = 'custom_role'; + const permissions = ['conversation_manage']; + const currentUserId = 1; + + expect( + applyRoleFilter( + conversationWithAssignee, + role, + permissions, + currentUserId + ) + ).toBe(true); + expect( + applyRoleFilter( + conversationWithDifferentAssignee, + role, + permissions, + currentUserId + ) + ).toBe(true); + expect( + applyRoleFilter( + conversationWithoutAssignee, + role, + permissions, + currentUserId + ) + ).toBe(true); + }); + + // Test for custom role with 'conversation_unassigned_manage' permission + describe('with conversation_unassigned_manage permission', () => { + const role = 'custom_role'; + const permissions = ['conversation_unassigned_manage']; + const currentUserId = 1; + + it('returns true for conversations assigned to the user', () => { + expect( + applyRoleFilter( + conversationWithAssignee, + role, + permissions, + currentUserId + ) + ).toBe(true); + }); + + it('returns true for unassigned conversations', () => { + expect( + applyRoleFilter( + conversationWithoutAssignee, + role, + permissions, + currentUserId + ) + ).toBe(true); + }); + + it('returns false for conversations assigned to other users', () => { + expect( + applyRoleFilter( + conversationWithDifferentAssignee, + role, + permissions, + currentUserId + ) + ).toBe(false); + }); + }); + + // Test for custom role with 'conversation_participating_manage' permission + describe('with conversation_participating_manage permission', () => { + const role = 'custom_role'; + const permissions = ['conversation_participating_manage']; + const currentUserId = 1; + + it('returns true for conversations assigned to the user', () => { + expect( + applyRoleFilter( + conversationWithAssignee, + role, + permissions, + currentUserId + ) + ).toBe(true); + }); + + it('returns false for unassigned conversations', () => { + expect( + applyRoleFilter( + conversationWithoutAssignee, + role, + permissions, + currentUserId + ) + ).toBe(false); + }); + + it('returns false for conversations assigned to other users', () => { + expect( + applyRoleFilter( + conversationWithDifferentAssignee, + role, + permissions, + currentUserId + ) + ).toBe(false); + }); + }); + + // Test for user with no relevant permissions + it('returns false for custom role without any relevant permissions', () => { + const role = 'custom_role'; + const permissions = ['some_other_permission']; + const currentUserId = 1; + + expect( + applyRoleFilter( + conversationWithAssignee, + role, + permissions, + currentUserId + ) + ).toBe(false); + expect( + applyRoleFilter( + conversationWithDifferentAssignee, + role, + permissions, + currentUserId + ) + ).toBe(false); + expect( + applyRoleFilter( + conversationWithoutAssignee, + role, + permissions, + currentUserId + ) + ).toBe(false); + }); + + // Test edge cases for meta.assignee + describe('handles edge cases with meta.assignee', () => { + const role = 'custom_role'; + const permissions = ['conversation_unassigned_manage']; + const currentUserId = 1; + + it('treats undefined assignee as unassigned', () => { + const conversationWithUndefinedAssignee = { + meta: { + assignee: undefined, + }, + }; + + expect( + applyRoleFilter( + conversationWithUndefinedAssignee, + role, + permissions, + currentUserId + ) + ).toBe(true); + }); + + it('handles empty meta object', () => { + const conversationWithEmptyMeta = { + meta: {}, + }; + + expect( + applyRoleFilter( + conversationWithEmptyMeta, + role, + permissions, + currentUserId + ) + ).toBe(true); + }); + }); + }); +});