mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 03:57:52 +00:00
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 <muhsinkeramam@gmail.com>
This commit is contained in:
@@ -16,6 +16,15 @@ export const getUserPermissions = (user, accountId) => {
|
|||||||
return currentAccount.permissions || [];
|
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 =>
|
const isPermissionsPresentInRoute = route =>
|
||||||
route.meta && route.meta.permissions;
|
route.meta && route.meta.permissions;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { MESSAGE_TYPE } from 'shared/constants/messages';
|
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 filterQueryGenerator from 'dashboard/helper/filterQueryGenerator';
|
||||||
import { matchesFilters } from './helpers/filterHelpers';
|
import { matchesFilters } from './helpers/filterHelpers';
|
||||||
|
import {
|
||||||
|
getUserPermissions,
|
||||||
|
getUserRole,
|
||||||
|
} from '../../../helper/permissionsHelper';
|
||||||
import camelcaseKeys from 'camelcase-keys';
|
import camelcaseKeys from 'camelcase-keys';
|
||||||
|
|
||||||
export const getSelectedChatConversation = ({
|
export const getSelectedChatConversation = ({
|
||||||
@@ -77,10 +81,24 @@ const getters = {
|
|||||||
return isUnAssigned && shouldFilter;
|
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 => {
|
return _state.allConversations.filter(conversation => {
|
||||||
const shouldFilter = applyPageFilters(conversation, activeFilters);
|
const shouldFilter = applyPageFilters(conversation, activeFilters);
|
||||||
return shouldFilter;
|
const allowedForRole = applyRoleFilter(
|
||||||
|
conversation,
|
||||||
|
userRole,
|
||||||
|
permissions,
|
||||||
|
currentUserId
|
||||||
|
);
|
||||||
|
|
||||||
|
return shouldFilter && allowedForRole;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getChatListLoadingStatus: ({ listLoadingStatus }) => listLoadingStatus,
|
getChatListLoadingStatus: ({ listLoadingStatus }) => listLoadingStatus,
|
||||||
|
|||||||
@@ -62,6 +62,51 @@ export const applyPageFilters = (conversation, filters) => {
|
|||||||
return shouldFilter;
|
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<string>} 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 = {
|
const SORT_OPTIONS = {
|
||||||
last_activity_at_asc: ['sortOnLastActivityAt', 'asc'],
|
last_activity_at_asc: ['sortOnLastActivityAt', 'asc'],
|
||||||
last_activity_at_desc: ['sortOnLastActivityAt', 'desc'],
|
last_activity_at_desc: ['sortOnLastActivityAt', 'desc'],
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user