From 4897bfd2ef397a16408347f1fe1448b2a0a7e9b8 Mon Sep 17 00:00:00 2001 From: Pranav Date: Thu, 23 May 2024 12:12:07 -0700 Subject: [PATCH] Update permissions on the frontend --- .../dashboard/components/layout/Sidebar.vue | 2 +- .../layout/sidebarComponents/Secondary.vue | 21 ++++---- .../sidebarComponents/SecondaryNavItem.vue | 47 +++++++++--------- .../dashboard/components/policy.vue | 24 +++++++++ .../dashboard/constants/permissions.js | 10 ++++ .../dashboard/helper/routeHelpers.js | 23 ++++----- .../conversation/conversation.routes.js | 49 +++++++++++++------ app/javascript/dashboard/routes/index.js | 29 +---------- app/views/api/v1/models/_user.json.jbuilder | 1 + 9 files changed, 116 insertions(+), 90 deletions(-) create mode 100644 app/javascript/dashboard/components/policy.vue create mode 100644 app/javascript/dashboard/constants/permissions.js diff --git a/app/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue index 0f3a1a857..ff87835b4 100644 --- a/app/javascript/dashboard/components/layout/Sidebar.vue +++ b/app/javascript/dashboard/components/layout/Sidebar.vue @@ -20,7 +20,7 @@ :teams="teams" :custom-views="customViews" :menu-config="activeSecondaryMenu" - :current-role="currentRole" + :current-user="currentUser" :is-on-chatwoot-cloud="isOnChatwootCloud" @add-label="showAddLabelPopup" @toggle-accounts="toggleAccountModal" diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue b/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue index f291d221e..6d00bd4c2 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue @@ -60,9 +60,9 @@ export default { type: Object, default: () => {}, }, - currentRole: { - type: String, - default: '', + currentUser: { + type: Object, + default: () => {}, }, isOnChatwootCloud: { type: Boolean, @@ -83,13 +83,16 @@ export default { if (!this.currentRole) { return []; } - const menuItemsFilteredByRole = this.menuConfig.menuItems.filter( - menuItem => - window.roleWiseRoutes[this.currentRole].indexOf( - menuItem.toStateName - ) > -1 + const menuItemsFilteredByPermissions = this.menuConfig.menuItems.filter( + menuItem => { + const { meta: { menuPermissions = [] } = {} } = menuItem; + const { permissions: userPermissions = [] } = this.currentUser; + return menuPermissions.some(permission => + userPermissions.includes(permission) + ); + } ); - return menuItemsFilteredByRole.filter(item => { + return menuItemsFilteredByPermissions.filter(item => { if (item.showOnlyOnCloud) { return this.isOnChatwootCloud; } diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue index a7009523a..df4981f83 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue @@ -65,27 +65,29 @@ :show-child-count="showChildCount(child.count)" :child-item-count="child.count" /> - -
  • - - - {{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }} - - -
  • -
    + + +
  • + + + {{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }} + + +
  • +
    +
    @@ -105,9 +107,10 @@ import { isOnMentionsView, isOnUnattendedView, } from '../../../store/modules/conversations/helpers/actionHelpers'; +import Policy from '../../policy.vue'; export default { - components: { SecondaryChildNavItem }, + components: { SecondaryChildNavItem, Policy }, mixins: [adminMixin, configMixin], props: { menuItem: { diff --git a/app/javascript/dashboard/components/policy.vue b/app/javascript/dashboard/components/policy.vue new file mode 100644 index 000000000..5520fdcc6 --- /dev/null +++ b/app/javascript/dashboard/components/policy.vue @@ -0,0 +1,24 @@ + + + diff --git a/app/javascript/dashboard/constants/permissions.js b/app/javascript/dashboard/constants/permissions.js new file mode 100644 index 000000000..2705a3887 --- /dev/null +++ b/app/javascript/dashboard/constants/permissions.js @@ -0,0 +1,10 @@ +export const ACCOUNT_PERMISSIONS = { + ACCOUNT_MANAGE: 'account_manage', + PORTAL_MANAGE: 'portal_manage', + CONVERSATION_MANAGE: 'conversation_manage', + CONVERSATION_UNASSIGNED_MANAGE: 'conversation_unassigned_manage', + CONVERSATION_PARTICIPATING_MANAGE: 'conversation_participating_manage', + MACROS_MANAGE: 'macros_manage', + REPORTS_MANAGE: 'reports_manage', + CONTACTS_MANAGE: 'contacts_manage', +}; diff --git a/app/javascript/dashboard/helper/routeHelpers.js b/app/javascript/dashboard/helper/routeHelpers.js index 86c286368..a63df4e9d 100644 --- a/app/javascript/dashboard/helper/routeHelpers.js +++ b/app/javascript/dashboard/helper/routeHelpers.js @@ -3,17 +3,14 @@ export const getCurrentAccount = ({ accounts } = {}, accountId) => { return accounts.find(account => account.id === accountId); }; -// eslint-disable-next-line default-param-last -export const getUserRole = ({ accounts } = {}, accountId) => { - const currentAccount = getCurrentAccount({ accounts }, accountId) || {}; - return currentAccount.role || null; +export const routeIsAccessibleFor = (route, userPermissions = []) => { + const { meta: { permissions: routePermissions = [] } = {} } = route; + return routePermissions.some(permissions => + userPermissions.includes(permissions) + ); }; -export const routeIsAccessibleFor = (route, role, roleWiseRoutes) => { - return roleWiseRoutes[role].includes(route); -}; - -const validateActiveAccountRoutes = (to, user, roleWiseRoutes) => { +const validateActiveAccountRoutes = (to, user) => { // If the current account is active, then check for the route permissions const accountDashboardURL = `accounts/${to.params.accountId}/dashboard`; @@ -22,15 +19,13 @@ const validateActiveAccountRoutes = (to, user, roleWiseRoutes) => { return accountDashboardURL; } - const userRole = getUserRole(user, Number(to.params.accountId)); - const isAccessible = routeIsAccessibleFor(to.name, userRole, roleWiseRoutes); + const isAccessible = routeIsAccessibleFor(to, user.permissions); // If the route is not accessible for the user, return to dashboard screen return isAccessible ? null : accountDashboardURL; }; -export const validateLoggedInRoutes = (to, user, roleWiseRoutes) => { +export const validateLoggedInRoutes = (to, user) => { const currentAccount = getCurrentAccount(user, Number(to.params.accountId)); - // If current account is missing, either user does not have // access to the account or the account is deleted, return to login screen if (!currentAccount) { @@ -40,7 +35,7 @@ export const validateLoggedInRoutes = (to, user, roleWiseRoutes) => { const isCurrentAccountActive = currentAccount.status === 'active'; if (isCurrentAccountActive) { - return validateActiveAccountRoutes(to, user, roleWiseRoutes); + return validateActiveAccountRoutes(to, user); } // If the current account is not active, then redirect the user to the suspended screen diff --git a/app/javascript/dashboard/routes/dashboard/conversation/conversation.routes.js b/app/javascript/dashboard/routes/dashboard/conversation/conversation.routes.js index 14487682e..6d39fb00d 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/conversation.routes.js +++ b/app/javascript/dashboard/routes/dashboard/conversation/conversation.routes.js @@ -1,13 +1,20 @@ /* eslint arrow-body-style: 0 */ +import { ACCOUNT_PERMISSIONS } from '../../../constants/permissions'; import { frontendURL } from '../../../helper/URLHelper'; const ConversationView = () => import('./ConversationView'); +const GENERIC_CONVERSATION_PERMISSIONS = [ + ACCOUNT_PERMISSIONS.CONVERSATION_MANAGE, + ACCOUNT_PERMISSIONS.CONVERSATION_PARTICIPATING_MANAGE, + ACCOUNT_PERMISSIONS.CONVERSATION_UNASSIGNED_MANAGE, +]; + export default { routes: [ { path: frontendURL('accounts/:accountId/dashboard'), name: 'home', - roles: ['administrator', 'agent'], + meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS }, component: ConversationView, props: () => { return { inboxId: 0 }; @@ -16,7 +23,7 @@ export default { { path: frontendURL('accounts/:accountId/conversations/:conversation_id'), name: 'inbox_conversation', - roles: ['administrator', 'agent'], + meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS }, component: ConversationView, props: route => { return { inboxId: 0, conversationId: route.params.conversation_id }; @@ -25,7 +32,7 @@ export default { { path: frontendURL('accounts/:accountId/inbox/:inbox_id'), name: 'inbox_dashboard', - roles: ['administrator', 'agent'], + meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS }, component: ConversationView, props: route => { return { inboxId: route.params.inbox_id }; @@ -36,7 +43,7 @@ export default { 'accounts/:accountId/inbox/:inbox_id/conversations/:conversation_id' ), name: 'conversation_through_inbox', - roles: ['administrator', 'agent'], + meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS }, component: ConversationView, props: route => { return { @@ -48,7 +55,7 @@ export default { { path: frontendURL('accounts/:accountId/label/:label'), name: 'label_conversations', - roles: ['administrator', 'agent'], + meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS }, component: ConversationView, props: route => ({ label: route.params.label }), }, @@ -57,7 +64,7 @@ export default { 'accounts/:accountId/label/:label/conversations/:conversation_id' ), name: 'conversations_through_label', - roles: ['administrator', 'agent'], + meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS }, component: ConversationView, props: route => ({ conversationId: route.params.conversation_id, @@ -67,7 +74,7 @@ export default { { path: frontendURL('accounts/:accountId/team/:teamId'), name: 'team_conversations', - roles: ['administrator', 'agent'], + meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS }, component: ConversationView, props: route => ({ teamId: route.params.teamId }), }, @@ -76,7 +83,7 @@ export default { 'accounts/:accountId/team/:teamId/conversations/:conversationId' ), name: 'conversations_through_team', - roles: ['administrator', 'agent'], + meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS }, component: ConversationView, props: route => ({ conversationId: route.params.conversationId, @@ -86,7 +93,7 @@ export default { { path: frontendURL('accounts/:accountId/custom_view/:id'), name: 'folder_conversations', - roles: ['administrator', 'agent'], + meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS }, component: ConversationView, props: route => ({ foldersId: route.params.id }), }, @@ -95,7 +102,7 @@ export default { 'accounts/:accountId/custom_view/:id/conversations/:conversation_id' ), name: 'conversations_through_folders', - roles: ['administrator', 'agent'], + meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS }, component: ConversationView, props: route => ({ conversationId: route.params.conversation_id, @@ -105,7 +112,7 @@ export default { { path: frontendURL('accounts/:accountId/mentions/conversations'), name: 'conversation_mentions', - roles: ['administrator', 'agent'], + meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS }, component: ConversationView, props: () => ({ conversationType: 'mention' }), }, @@ -114,7 +121,7 @@ export default { 'accounts/:accountId/mentions/conversations/:conversationId' ), name: 'conversation_through_mentions', - roles: ['administrator', 'agent'], + meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS }, component: ConversationView, props: route => ({ conversationId: route.params.conversationId, @@ -124,7 +131,7 @@ export default { { path: frontendURL('accounts/:accountId/unattended/conversations'), name: 'conversation_unattended', - roles: ['administrator', 'agent'], + meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS }, component: ConversationView, props: () => ({ conversationType: 'unattended' }), }, @@ -133,7 +140,7 @@ export default { 'accounts/:accountId/unattended/conversations/:conversationId' ), name: 'conversation_through_unattended', - roles: ['administrator', 'agent'], + meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS }, component: ConversationView, props: route => ({ conversationId: route.params.conversationId, @@ -143,8 +150,13 @@ export default { { path: frontendURL('accounts/:accountId/participating/conversations'), name: 'conversation_participating', - roles: ['administrator', 'agent'], component: ConversationView, + meta: { + permissions: [ + ACCOUNT_PERMISSIONS.CONVERSATION_MANAGE, + ACCOUNT_PERMISSIONS.CONVERSATION_PARTICIPATING_MANAGE, + ], + }, props: () => ({ conversationType: 'participating' }), }, { @@ -152,8 +164,13 @@ export default { 'accounts/:accountId/participating/conversations/:conversationId' ), name: 'conversation_through_participating', - roles: ['administrator', 'agent'], component: ConversationView, + meta: { + permissions: [ + ACCOUNT_PERMISSIONS.CONVERSATION_MANAGE, + ACCOUNT_PERMISSIONS.CONVERSATION_PARTICIPATING_MANAGE, + ], + }, props: route => ({ conversationId: route.params.conversationId, conversationType: 'participating', diff --git a/app/javascript/dashboard/routes/index.js b/app/javascript/dashboard/routes/index.js index e2245e20f..dc6528d93 100644 --- a/app/javascript/dashboard/routes/index.js +++ b/app/javascript/dashboard/routes/index.js @@ -8,29 +8,6 @@ import AnalyticsHelper from '../helper/AnalyticsHelper'; const routes = [...dashboard.routes]; -window.roleWiseRoutes = { - agent: [], - administrator: [], -}; - -// generateRoleWiseRoute - updates window object with agent/admin route -const generateRoleWiseRoute = route => { - route.forEach(element => { - if (element.children) { - generateRoleWiseRoute(element.children); - } - if (element.roles) { - element.roles.forEach(roleEl => { - window.roleWiseRoutes[roleEl].push(element.name); - }); - } - }); -}; -// Create a object of routes -// accessible by each role. -// returns an object with roles as keys and routeArr as values -generateRoleWiseRoute(routes); - export const router = new VueRouter({ mode: 'history', routes }); export const validateAuthenticateRoutePermission = (to, next, { getters }) => { @@ -45,11 +22,7 @@ export const validateAuthenticateRoutePermission = (to, next, { getters }) => { return next(frontendURL(`accounts/${user.account_id}/dashboard`)); } - const nextRoute = validateLoggedInRoutes( - to, - getters.getCurrentUser, - window.roleWiseRoutes - ); + const nextRoute = validateLoggedInRoutes(to, getters.getCurrentUser); return nextRoute ? next(frontendURL(nextRoute)) : next(); }; diff --git a/app/views/api/v1/models/_user.json.jbuilder b/app/views/api/v1/models/_user.json.jbuilder index 3d8131f60..0e8c95adb 100644 --- a/app/views/api/v1/models/_user.json.jbuilder +++ b/app/views/api/v1/models/_user.json.jbuilder @@ -14,6 +14,7 @@ json.provider resource.provider json.pubsub_token resource.pubsub_token json.custom_attributes resource.custom_attributes if resource.custom_attributes.present? json.role resource.active_account_user&.role +json.permissions resource.active_account_user&.permissions json.ui_settings resource.ui_settings json.uid resource.uid json.type resource.type