Compare commits

...
Author SHA1 Message Date
Vishnu Narayanan 25cdb2d265 chore: docker foss arm64 images 2024-07-05 12:46:09 +05:30
31bcdaa3dd chore: Upgrade ruby to 3.3.3 (#9664)
- Upgrade the ruby version to 3.3.3

---------

Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
2024-07-05 12:20:07 +05:30
Sivin VargheseandGitHub 6ae606c981 fix: Custom snooze is not working in mobile view (#9717)
# Pull Request Template

## Description

Currently, when a user navigates to a chat and attempts to access the
custom snooze modal, it is not visible, making it unable to set custom
snooze options. With this fix, the custom snooze modal will correctly
display even when a chat is open in mobile view.

**Cause of this issue**
The `<custom-snooze-modal/>` component is added to the `<chat-list/>`
component. To accommodate small screen views, we are using the expanded
view. However, if we open a chat and select the custom snooze option
from the chat header in the message view, the `<custom-snooze-modal/>`
component is hidden in the `<chat-list/>` component.

**Solution**
So, I moved the `<custom-snooze-modal/>` to the wrapper component
`<conversation-view/>` so we can use in all cases like,
1. Right-click to custom snooze
2. CMD bar custom snooze
3. Small screen custom snooze
2024-07-04 13:13:03 +05:30
aaf47b4c1f chore: [Snyk] Security upgrade sidekiq from 7.2.4 to 7.3.0 (#9710)
Upgrade gems to mitigate vulnerabilities.

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2024-07-03 15:48:57 -07:00
cc4851b19d chore: Move frontend authorization to permission based system (#9709)
We previously relied on user roles to determine whether to render
specific routes in our frontend components. A permissions-based model is replacing this approach.


Follow up: #9695

Co-authored-by: Pranav <pranavrajs@gmail.com>
2024-07-03 15:13:16 -07:00
49 changed files with 690 additions and 329 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ defaults: &defaults
working_directory: ~/build
docker:
# specify the version you desire here
- image: cimg/ruby:3.2.2-browsers
- image: cimg/ruby:3.3.3-browsers
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
+2 -2
View File
@@ -12,7 +12,7 @@ services:
args:
VARIANT: "ubuntu-22.04"
NODE_VERSION: "20.9.0"
RUBY_VERSION: "3.2.2"
RUBY_VERSION: "3.3.3"
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
USER_UID: "1000"
USER_GID: "1000"
@@ -25,7 +25,7 @@ services:
args:
VARIANT: "ubuntu-22.04"
NODE_VERSION: "20.9.0"
RUBY_VERSION: "3.2.2"
RUBY_VERSION: "3.3.3"
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
USER_UID: "1000"
USER_GID: "1000"
+7 -4
View File
@@ -24,10 +24,12 @@ jobs:
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
with:
platforms: 'arm64,amd64'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
- name: Strip enterprise code
run: |
@@ -54,10 +56,11 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
platforms: linux/amd64
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ env.DOCKER_TAG }}
+1 -1
View File
@@ -1 +1 @@
3.2.2
3.3.3
+2 -2
View File
@@ -1,6 +1,6 @@
source 'https://rubygems.org'
ruby '3.2.2'
ruby '3.3.3'
##-- base gems for rails --##
gem 'rack-cors', '2.0.0', require: 'rack/cors'
@@ -116,7 +116,7 @@ gem 'sentry-ruby', require: false
gem 'sentry-sidekiq', '>= 5.18.0', require: false
##-- background job processing --##
gem 'sidekiq', '>= 7.2.4'
gem 'sidekiq', '>= 7.3.0'
# We want cron jobs
gem 'sidekiq-cron', '>= 1.12.0'
+22 -17
View File
@@ -183,13 +183,16 @@ GEM
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.3.4)
ddtrace (1.11.1)
debase-ruby_core_source (>= 0.10.16, <= 3.2.0)
libdatadog (~> 2.0.0.1.0)
libddwaf (~> 1.8.2.0.0)
datadog-ci (0.8.3)
msgpack
debase-ruby_core_source (3.2.0)
date (3.3.4)
ddtrace (1.23.2)
datadog-ci (~> 0.8.1)
debase-ruby_core_source (= 3.3.1)
libdatadog (~> 7.0.0.1.0)
libddwaf (~> 1.14.0.0.0)
msgpack
debase-ruby_core_source (3.3.1)
debug (1.8.0)
irb (>= 1.5.0)
reline (>= 0.3.1)
@@ -416,15 +419,15 @@ GEM
addressable (~> 2.8)
letter_opener (1.8.1)
launchy (>= 2.2, < 3)
libdatadog (2.0.0.1.0)
libdatadog (2.0.0.1.0-x86_64-linux)
libddwaf (1.8.2.0.0)
libdatadog (7.0.0.1.0)
libdatadog (7.0.0.1.0-x86_64-linux)
libddwaf (1.14.0.0.0)
ffi (~> 1.0)
libddwaf (1.8.2.0.0-arm64-darwin)
libddwaf (1.14.0.0.0-arm64-darwin)
ffi (~> 1.0)
libddwaf (1.8.2.0.0-x86_64-darwin)
libddwaf (1.14.0.0.0-x86_64-darwin)
ffi (~> 1.0)
libddwaf (1.8.2.0.0-x86_64-linux)
libddwaf (1.14.0.0.0-x86_64-linux)
ffi (~> 1.0)
line-bot-api (1.28.0)
liquid (5.4.0)
@@ -434,6 +437,7 @@ GEM
llhttp-ffi (0.4.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
logger (1.6.0)
lograge (0.14.0)
actionpack (>= 4)
activesupport (>= 4)
@@ -715,11 +719,12 @@ GEM
sexp_processor (4.17.0)
shoulda-matchers (5.3.0)
activesupport (>= 5.2.0)
sidekiq (7.2.4)
sidekiq (7.3.0)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
logger
rack (>= 2.2.4)
redis-client (>= 0.19.0)
redis-client (>= 0.22.2)
sidekiq-cron (1.12.0)
fugit (~> 1.8)
globalid (>= 1.0.1)
@@ -935,7 +940,7 @@ DEPENDENCIES
sentry-ruby
sentry-sidekiq (>= 5.18.0)
shoulda-matchers
sidekiq (>= 7.2.4)
sidekiq (>= 7.3.0)
sidekiq-cron (>= 1.12.0)
simplecov (= 0.17.1)
slack-ruby-client (~> 2.2.0)
@@ -960,7 +965,7 @@ DEPENDENCIES
working_hours
RUBY VERSION
ruby 3.2.2p185
ruby 3.3.3p89
BUNDLED WITH
2.4.6
2.5.14
@@ -111,15 +111,6 @@
@updateFolder="onUpdateSavedFilter"
/>
</woot-modal>
<woot-modal
:show.sync="showCustomSnoozeModal"
:on-close="hideCustomSnoozeModal"
>
<custom-snooze-modal
@close="hideCustomSnoozeModal"
@choose-time="chooseSnoozeTime"
/>
</woot-modal>
</div>
</template>
@@ -152,10 +143,6 @@ import {
isOnUnattendedView,
} from '../store/modules/conversations/helpers/actionHelpers';
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
import { CMD_SNOOZE_CONVERSATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
import { getUnixTime } from 'date-fns';
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
import IntersectionObserver from './IntersectionObserver.vue';
export default {
@@ -170,7 +157,6 @@ export default {
ConversationBulkActions,
IntersectionObserver,
VirtualList,
CustomSnoozeModal,
},
mixins: [
timeMixin,
@@ -247,7 +233,6 @@ export default {
root: this.$refs.conversationList,
rootMargin: '100px 0px 100px 0px',
},
showCustomSnoozeModal: false,
itemComponent: ConversationItem,
// virtualListExtraProps is to pass the props to the conversationItem component.
@@ -283,7 +268,6 @@ export default {
campaigns: 'campaigns/getAllCampaigns',
labels: 'labels/getLabels',
selectedConversations: 'bulkActions/getSelectedConversationIds',
contextMenuChatId: 'getContextMenuChatId',
}),
hasAppliedFilters() {
return this.appliedFilters.length !== 0;
@@ -517,11 +501,6 @@ export default {
this.$emitter.on('fetch_conversation_stats', () => {
this.$store.dispatch('conversationStats/get', this.conversationFilters);
});
this.$emitter.on(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
},
beforeDestroy() {
this.$emitter.off(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
},
methods: {
updateVirtualListProps(key, value) {
@@ -999,43 +978,6 @@ export default {
onContextMenuToggle(state) {
this.isContextMenuOpen = state;
},
onCmdSnoozeConversation(snoozeType) {
if (snoozeType === wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME) {
this.showCustomSnoozeModal = true;
} else {
this.toggleStatus(
wootConstants.STATUS_TYPE.SNOOZED,
findSnoozeTime(snoozeType) || null
);
}
},
chooseSnoozeTime(customSnoozeTime) {
this.showCustomSnoozeModal = false;
if (customSnoozeTime) {
this.toggleStatus(
wootConstants.STATUS_TYPE.SNOOZED,
getUnixTime(customSnoozeTime)
);
}
},
toggleStatus(status, snoozedUntil) {
this.$store
.dispatch('toggleStatus', {
conversationId: this.currentChat?.id || this.contextMenuChatId,
status,
snoozedUntil,
})
.then(() => {
this.$store.dispatch('setContextMenuChatId', null);
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
});
},
hideCustomSnoozeModal() {
// if we select custom snooze and then the custom snooze modal is open
// Then if the custom snooze modal is closed and set the context menu chat id to null
this.$store.dispatch('setContextMenuChatId', null);
this.showCustomSnoozeModal = false;
},
},
};
</script>
@@ -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"
@@ -37,7 +37,8 @@ import alertMixin from 'shared/mixins/alertMixin';
import PrimarySidebar from './sidebarComponents/Primary.vue';
import SecondarySidebar from './sidebarComponents/Secondary.vue';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import router from '../../routes';
import router, { routesWithPermissions } from '../../routes';
import { hasPermissions } from '../../helper/permissionsHelper';
export default {
components: {
@@ -98,9 +99,13 @@ export default {
return getSidebarItems(this.accountId);
},
primaryMenuItems() {
const userPermissions = this.currentUser.permissions;
const menuItems = this.sideMenuConfig.primaryMenu;
return menuItems.filter(menuItem => {
const isAvailableForTheUser = menuItem.roles.includes(this.currentRole);
const isAvailableForTheUser = hasPermissions(
routesWithPermissions[menuItem.toStateName],
userPermissions
);
if (!isAvailableForTheUser) {
return false;
@@ -9,7 +9,6 @@ const primaryMenuItems = accountId => [
featureFlag: FEATURE_FLAGS.INBOX_VIEW,
toState: frontendURL(`accounts/${accountId}/inbox-view`),
toStateName: 'inbox_view',
roles: ['administrator', 'agent'],
},
{
icon: 'chat',
@@ -17,7 +16,6 @@ const primaryMenuItems = accountId => [
label: 'CONVERSATIONS',
toState: frontendURL(`accounts/${accountId}/dashboard`),
toStateName: 'home',
roles: ['administrator', 'agent'],
},
{
icon: 'book-contacts',
@@ -26,7 +24,6 @@ const primaryMenuItems = accountId => [
featureFlag: FEATURE_FLAGS.CRM,
toState: frontendURL(`accounts/${accountId}/contacts`),
toStateName: 'contacts_dashboard',
roles: ['administrator', 'agent'],
},
{
icon: 'arrow-trending-lines',
@@ -34,8 +31,7 @@ const primaryMenuItems = accountId => [
label: 'REPORTS',
featureFlag: FEATURE_FLAGS.REPORTS,
toState: frontendURL(`accounts/${accountId}/reports`),
toStateName: 'settings_account_reports',
roles: ['administrator'],
toStateName: 'account_overview_reports',
},
{
icon: 'megaphone',
@@ -44,7 +40,6 @@ const primaryMenuItems = accountId => [
featureFlag: FEATURE_FLAGS.CAMPAIGNS,
toState: frontendURL(`accounts/${accountId}/campaigns`),
toStateName: 'ongoing_campaigns',
roles: ['administrator'],
},
{
icon: 'library',
@@ -54,7 +49,6 @@ const primaryMenuItems = accountId => [
alwaysVisibleOnChatwootInstances: true,
toState: frontendURL(`accounts/${accountId}/portals`),
toStateName: 'default_portal_articles',
roles: ['administrator'],
},
{
icon: 'settings',
@@ -62,7 +56,6 @@ const primaryMenuItems = accountId => [
label: 'SETTINGS',
toState: frontendURL(`accounts/${accountId}/settings`),
toStateName: 'settings_home',
roles: ['administrator', 'agent'],
},
];
@@ -24,7 +24,6 @@ const settings = accountId => ({
'settings_inbox_list',
'settings_inbox_new',
'settings_inbox_show',
'settings_inbox',
'settings_inboxes_add_agents',
'settings_inboxes_page_channel',
'settings_integrations_dashboard_apps',
@@ -46,6 +45,9 @@ const settings = accountId => ({
icon: 'briefcase',
label: 'ACCOUNT_SETTINGS',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/general`),
toStateName: 'general_settings_index',
},
@@ -53,6 +55,9 @@ const settings = accountId => ({
icon: 'people',
label: 'AGENTS',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/agents/list`),
toStateName: 'agent_list',
featureFlag: FEATURE_FLAGS.AGENT_MANAGEMENT,
@@ -61,6 +66,9 @@ const settings = accountId => ({
icon: 'people-team',
label: 'TEAMS',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/teams/list`),
toStateName: 'settings_teams_list',
featureFlag: FEATURE_FLAGS.TEAM_MANAGEMENT,
@@ -69,6 +77,9 @@ const settings = accountId => ({
icon: 'mail-inbox-all',
label: 'INBOXES',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`),
toStateName: 'settings_inbox_list',
featureFlag: FEATURE_FLAGS.INBOX_MANAGEMENT,
@@ -77,6 +88,9 @@ const settings = accountId => ({
icon: 'tag',
label: 'LABELS',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/labels/list`),
toStateName: 'labels_list',
featureFlag: FEATURE_FLAGS.LABELS,
@@ -85,6 +99,9 @@ const settings = accountId => ({
icon: 'code',
label: 'CUSTOM_ATTRIBUTES',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(
`accounts/${accountId}/settings/custom-attributes/list`
),
@@ -95,6 +112,9 @@ const settings = accountId => ({
icon: 'automation',
label: 'AUTOMATION',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/automation/list`),
toStateName: 'automation_list',
featureFlag: FEATURE_FLAGS.AUTOMATIONS,
@@ -103,6 +123,9 @@ const settings = accountId => ({
icon: 'bot',
label: 'AGENT_BOTS',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
globalConfigFlag: 'csmlEditorHost',
toState: frontendURL(`accounts/${accountId}/settings/agent-bots`),
toStateName: 'agent_bots',
@@ -112,6 +135,9 @@ const settings = accountId => ({
icon: 'flash-settings',
label: 'MACROS',
hasSubMenu: false,
meta: {
permissions: ['administrator', 'agent'],
},
toState: frontendURL(`accounts/${accountId}/settings/macros`),
toStateName: 'macros_wrapper',
featureFlag: FEATURE_FLAGS.MACROS,
@@ -120,6 +146,9 @@ const settings = accountId => ({
icon: 'chat-multiple',
label: 'CANNED_RESPONSES',
hasSubMenu: false,
meta: {
permissions: ['administrator', 'agent'],
},
toState: frontendURL(
`accounts/${accountId}/settings/canned-response/list`
),
@@ -130,6 +159,9 @@ const settings = accountId => ({
icon: 'flash-on',
label: 'INTEGRATIONS',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
toStateName: 'settings_integrations',
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
@@ -138,6 +170,9 @@ const settings = accountId => ({
icon: 'star-emphasis',
label: 'APPLICATIONS',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/applications`),
toStateName: 'settings_applications',
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
@@ -146,6 +181,9 @@ const settings = accountId => ({
icon: 'key',
label: 'AUDIT_LOGS',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/audit-log/list`),
toStateName: 'auditlogs_list',
isEnterpriseOnly: true,
@@ -156,6 +194,9 @@ const settings = accountId => ({
icon: 'document-list-clock',
label: 'SLA',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/sla/list`),
toStateName: 'sla_list',
isEnterpriseOnly: true,
@@ -166,6 +207,9 @@ const settings = accountId => ({
icon: 'credit-card-person',
label: 'BILLING',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/billing`),
toStateName: 'billing_settings_index',
showOnlyOnCloud: true,
@@ -29,6 +29,8 @@ import SecondaryNavItem from './SecondaryNavItem.vue';
import AccountContext from './AccountContext.vue';
import { mapGetters } from 'vuex';
import { FEATURE_FLAGS } from '../../../featureFlags';
import { hasPermissions } from '../../../helper/permissionsHelper';
import { routesWithPermissions } from '../../../routes';
export default {
components: {
@@ -60,9 +62,9 @@ export default {
type: Object,
default: () => {},
},
currentRole: {
type: String,
default: '',
currentUser: {
type: Object,
default: () => {},
},
isOnChatwootCloud: {
type: Boolean,
@@ -80,16 +82,16 @@ export default {
return this.customViews.filter(view => view.filter_type === 'contact');
},
accessibleMenuItems() {
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 { permissions: userPermissions = [] } = this.currentUser;
return hasPermissions(
routesWithPermissions[menuItem.toStateName],
userPermissions
);
}
);
return menuItemsFilteredByRole.filter(item => {
return menuItemsFilteredByPermissions.filter(item => {
if (item.showOnlyOnCloud) {
return this.isOnChatwootCloud;
}
@@ -65,27 +65,29 @@
:show-child-count="showChildCount(child.count)"
:child-item-count="child.count"
/>
<router-link
v-if="showItem(menuItem)"
v-slot="{ href, navigate }"
:to="menuItem.toState"
custom
>
<li class="pl-1">
<a :href="href">
<woot-button
size="tiny"
variant="clear"
color-scheme="secondary"
icon="add"
:data-testid="menuItem.dataTestid"
@click="e => newLinkClick(e, navigate)"
>
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
</woot-button>
</a>
</li>
</router-link>
<Policy :permissions="['administrator']">
<router-link
v-if="menuItem.newLink"
v-slot="{ href, navigate }"
:to="menuItem.toState"
custom
>
<li class="pl-1">
<a :href="href">
<woot-button
size="tiny"
variant="clear"
color-scheme="secondary"
icon="add"
:data-testid="menuItem.dataTestid"
@click="e => newLinkClick(e, navigate)"
>
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
</woot-button>
</a>
</li>
</router-link>
</Policy>
</ul>
</li>
</template>
@@ -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: {
@@ -0,0 +1,23 @@
<script setup>
import { useStoreGetters } from 'dashboard/composables/store';
import { computed } from 'vue';
import { hasPermissions } from '../helper/permissionsHelper';
const props = defineProps({
permissions: {
type: Array,
required: true,
},
});
const getters = useStoreGetters();
const user = getters.getCurrentUser.value;
const hasPermission = computed(() =>
hasPermissions(props.permissions, user.permissions)
);
</script>
<template>
<div v-if="hasPermission">
<slot />
</div>
</template>
@@ -0,0 +1,34 @@
export const hasPermissions = (
requiredPermissions = [],
availablePermissions = []
) => {
return requiredPermissions.some(permission =>
availablePermissions.includes(permission)
);
};
const isPermissionsPresentInRoute = route =>
route.meta && route.meta.permissions;
export const buildPermissionsFromRouter = (routes = []) =>
routes.reduce((acc, route) => {
if (route.name) {
if (!isPermissionsPresentInRoute(route)) {
// eslint-disable-next-line
console.error(route);
throw new Error(
"The route doesn't have the required permissions defined"
);
}
acc[route.name] = route.meta.permissions;
}
if (route.children) {
acc = {
...acc,
...buildPermissionsFromRouter(route.children),
};
}
return acc;
}, {});
@@ -1,19 +1,16 @@
import { hasPermissions } from './permissionsHelper';
// eslint-disable-next-line default-param-last
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 hasPermissions(routePermissions, userPermissions);
};
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
@@ -0,0 +1,84 @@
import {
buildPermissionsFromRouter,
hasPermissions,
} from '../permissionsHelper';
describe('hasPermissions', () => {
it('returns true if permission is present', () => {
expect(
hasPermissions(['contact_manage'], ['team_manage', 'contact_manage'])
).toBe(true);
});
it('returns true if permission is not present', () => {
expect(
hasPermissions(['contact_manage'], ['team_manage', 'user_manage'])
).toBe(false);
expect(hasPermissions()).toBe(false);
expect(hasPermissions([])).toBe(false);
});
});
describe('buildPermissionsFromRouter', () => {
it('returns a valid object when routes have permissions defined', () => {
expect(
buildPermissionsFromRouter([
{
path: 'agent',
name: 'agent_list',
meta: { permissions: ['agent_admin'] },
},
{
path: 'inbox',
children: [
{
path: '',
name: 'inbox_list',
meta: { permissions: ['inbox_admin'] },
},
],
},
{
path: 'conversations',
children: [
{
path: '',
children: [
{
path: 'attachments',
name: 'attachments_list',
meta: { permissions: ['conversation_admin'] },
},
],
},
],
},
])
).toEqual({
agent_list: ['agent_admin'],
inbox_list: ['inbox_admin'],
attachments_list: ['conversation_admin'],
});
});
it('throws an error if a named routed does not have permissions defined', () => {
expect(() => {
buildPermissionsFromRouter([
{
path: 'agent',
name: 'agent_list',
},
]);
}).toThrow("The route doesn't have the required permissions defined");
expect(() => {
buildPermissionsFromRouter([
{
path: 'agent',
name: 'agent_list',
meta: {},
},
]);
}).toThrow("The route doesn't have the required permissions defined");
});
});
@@ -1,7 +1,6 @@
import {
getConversationDashboardRoute,
getCurrentAccount,
getUserRole,
isAConversationRoute,
routeIsAccessibleFor,
validateLoggedInRoutes,
@@ -15,24 +14,11 @@ describe('#getCurrentAccount', () => {
});
});
describe('#getUserRole', () => {
it('should return the current role', () => {
expect(
getUserRole({ accounts: [{ id: 1, role: 'administrator' }] }, 1)
).toEqual('administrator');
expect(getUserRole({ accounts: [] }, 1)).toEqual(null);
});
});
describe('#routeIsAccessibleFor', () => {
it('should return the correct access', () => {
const roleWiseRoutes = { agent: ['conversations'], admin: ['billing'] };
expect(routeIsAccessibleFor('billing', 'agent', roleWiseRoutes)).toEqual(
false
);
expect(routeIsAccessibleFor('billing', 'admin', roleWiseRoutes)).toEqual(
true
);
let route = { meta: { permissions: ['administrator'] } };
expect(routeIsAccessibleFor(route, ['agent'])).toEqual(false);
expect(routeIsAccessibleFor(route, ['administrator'])).toEqual(true);
});
});
@@ -40,11 +26,7 @@ describe('#validateLoggedInRoutes', () => {
describe('when account access is missing', () => {
it('should return the login route', () => {
expect(
validateLoggedInRoutes(
{ params: { accountId: 1 } },
{ accounts: [] },
{}
)
validateLoggedInRoutes({ params: { accountId: 1 } }, { accounts: [] })
).toEqual(`app/login`);
});
});
@@ -53,9 +35,12 @@ describe('#validateLoggedInRoutes', () => {
it('return suspended route', () => {
expect(
validateLoggedInRoutes(
{ name: 'conversations', params: { accountId: 1 } },
{ accounts: [{ id: 1, role: 'agent', status: 'suspended' }] },
{ agent: ['conversations'] }
{
name: 'conversations',
params: { accountId: 1 },
meta: { permissions: ['agent'] },
},
{ accounts: [{ id: 1, role: 'agent', status: 'suspended' }] }
)
).toEqual(`accounts/1/suspended`);
});
@@ -65,9 +50,22 @@ describe('#validateLoggedInRoutes', () => {
it('returns null (no action required)', () => {
expect(
validateLoggedInRoutes(
{ name: 'conversations', params: { accountId: 1 } },
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] },
{ agent: ['conversations'] }
{
name: 'conversations',
params: { accountId: 1 },
meta: { permissions: ['agent'] },
},
{
permissions: ['agent'],
accounts: [
{
id: 1,
role: 'agent',
permissions: ['agent'],
status: 'active',
},
],
}
)
).toEqual(null);
});
@@ -76,9 +74,12 @@ describe('#validateLoggedInRoutes', () => {
it('returns dashboard url', () => {
expect(
validateLoggedInRoutes(
{ name: 'conversations', params: { accountId: 1 } },
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] },
{ admin: ['conversations'], agent: [] }
{
name: 'billing',
params: { accountId: 1 },
meta: { permissions: ['administrator'] },
},
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] }
)
).toEqual(`accounts/1/dashboard`);
});
@@ -88,8 +89,7 @@ describe('#validateLoggedInRoutes', () => {
expect(
validateLoggedInRoutes(
{ name: 'account_suspended', params: { accountId: 1 } },
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] },
{ agent: ['account_suspended'] }
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] }
)
).toEqual(`accounts/1/dashboard`);
});
@@ -7,7 +7,9 @@ export const routes = [
{
path: frontendURL('accounts/:accountId/search'),
name: 'search',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: SearchView,
},
];
@@ -7,13 +7,17 @@ export const routes = [
{
path: frontendURL('accounts/:accountId/contacts'),
name: 'contacts_dashboard',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ContactsView,
},
{
path: frontendURL('accounts/:accountId/contacts/custom_view/:id'),
name: 'contacts_segments_dashboard',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ContactsView,
props: route => {
return { segmentsId: route.params.id };
@@ -22,7 +26,9 @@ export const routes = [
{
path: frontendURL('accounts/:accountId/labels/:label/contacts'),
name: 'contacts_labels_dashboard',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ContactsView,
props: route => {
return { label: route.params.label };
@@ -31,7 +37,9 @@ export const routes = [
{
path: frontendURL('accounts/:accountId/contacts/:contactId'),
name: 'contact_profile_dashboard',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ContactManageView,
props: route => {
return { contactId: route.params.contactId };
@@ -22,25 +22,40 @@
:is-on-expanded-layout="isOnExpandedLayout"
@contact-panel-toggle="onToggleContactPanel"
/>
<woot-modal
:show.sync="showCustomSnoozeModal"
:on-close="hideCustomSnoozeModal"
>
<custom-snooze-modal
@close="hideCustomSnoozeModal"
@choose-time="chooseSnoozeTime"
/>
</woot-modal>
</section>
</template>
<script>
import { mapGetters } from 'vuex';
import { getUnixTime } from 'date-fns';
import ChatList from '../../../components/ChatList.vue';
import ConversationBox from '../../../components/widgets/conversation/ConversationBox.vue';
import PopOverSearch from './search/PopOverSearch.vue';
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import alertMixin from 'shared/mixins/alertMixin';
import wootConstants from 'dashboard/constants/globals';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { CMD_SNOOZE_CONVERSATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
export default {
components: {
ChatList,
ConversationBox,
PopOverSearch,
CustomSnoozeModal,
},
mixins: [uiSettingsMixin],
mixins: [uiSettingsMixin, alertMixin],
props: {
inboxId: {
type: [String, Number],
@@ -70,12 +85,14 @@ export default {
data() {
return {
showSearchModal: false,
showCustomSnoozeModal: false,
};
},
computed: {
...mapGetters({
chatList: 'getAllConversations',
currentChat: 'getSelectedChat',
contextMenuChatId: 'getContextMenuChatId',
}),
showConversationList() {
return this.isOnExpandedLayout ? !this.conversationId : true;
@@ -112,6 +129,10 @@ export default {
this.$watch('chatList.length', () => {
this.setActiveChat();
});
this.$emitter.on(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
},
beforeDestroy() {
this.$emitter.off(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
},
methods: {
@@ -186,6 +207,43 @@ export default {
closeSearch() {
this.showSearchModal = false;
},
onCmdSnoozeConversation(snoozeType) {
if (snoozeType === wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME) {
this.showCustomSnoozeModal = true;
} else {
this.toggleStatus(
wootConstants.STATUS_TYPE.SNOOZED,
findSnoozeTime(snoozeType) || null
);
}
},
chooseSnoozeTime(customSnoozeTime) {
this.showCustomSnoozeModal = false;
if (customSnoozeTime) {
this.toggleStatus(
wootConstants.STATUS_TYPE.SNOOZED,
getUnixTime(customSnoozeTime)
);
}
},
toggleStatus(status, snoozedUntil) {
this.$store
.dispatch('toggleStatus', {
conversationId: this.currentChat?.id || this.contextMenuChatId,
status,
snoozedUntil,
})
.then(() => {
this.$store.dispatch('setContextMenuChatId', null);
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
});
},
hideCustomSnoozeModal() {
// if we select custom snooze and the custom snooze modal is open
// Then if the custom snooze modal is closed then set the context menu chat id to null
this.$store.dispatch('setContextMenuChatId', null);
this.showCustomSnoozeModal = false;
},
},
};
</script>
@@ -7,7 +7,9 @@ export default {
{
path: frontendURL('accounts/:accountId/dashboard'),
name: 'home',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView,
props: () => {
return { inboxId: 0 };
@@ -16,7 +18,9 @@ export default {
{
path: frontendURL('accounts/:accountId/conversations/:conversation_id'),
name: 'inbox_conversation',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView,
props: route => {
return { inboxId: 0, conversationId: route.params.conversation_id };
@@ -25,7 +29,9 @@ export default {
{
path: frontendURL('accounts/:accountId/inbox/:inbox_id'),
name: 'inbox_dashboard',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView,
props: route => {
return { inboxId: route.params.inbox_id };
@@ -36,7 +42,9 @@ export default {
'accounts/:accountId/inbox/:inbox_id/conversations/:conversation_id'
),
name: 'conversation_through_inbox',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView,
props: route => {
return {
@@ -48,7 +56,9 @@ export default {
{
path: frontendURL('accounts/:accountId/label/:label'),
name: 'label_conversations',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView,
props: route => ({ label: route.params.label }),
},
@@ -57,7 +67,9 @@ export default {
'accounts/:accountId/label/:label/conversations/:conversation_id'
),
name: 'conversations_through_label',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView,
props: route => ({
conversationId: route.params.conversation_id,
@@ -67,7 +79,9 @@ export default {
{
path: frontendURL('accounts/:accountId/team/:teamId'),
name: 'team_conversations',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView,
props: route => ({ teamId: route.params.teamId }),
},
@@ -76,7 +90,9 @@ export default {
'accounts/:accountId/team/:teamId/conversations/:conversationId'
),
name: 'conversations_through_team',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView,
props: route => ({
conversationId: route.params.conversationId,
@@ -86,7 +102,9 @@ export default {
{
path: frontendURL('accounts/:accountId/custom_view/:id'),
name: 'folder_conversations',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView,
props: route => ({ foldersId: route.params.id }),
},
@@ -95,7 +113,9 @@ export default {
'accounts/:accountId/custom_view/:id/conversations/:conversation_id'
),
name: 'conversations_through_folders',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView,
props: route => ({
conversationId: route.params.conversation_id,
@@ -105,7 +125,9 @@ export default {
{
path: frontendURL('accounts/:accountId/mentions/conversations'),
name: 'conversation_mentions',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView,
props: () => ({ conversationType: 'mention' }),
},
@@ -114,7 +136,9 @@ export default {
'accounts/:accountId/mentions/conversations/:conversationId'
),
name: 'conversation_through_mentions',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView,
props: route => ({
conversationId: route.params.conversationId,
@@ -124,7 +148,9 @@ export default {
{
path: frontendURL('accounts/:accountId/unattended/conversations'),
name: 'conversation_unattended',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView,
props: () => ({ conversationType: 'unattended' }),
},
@@ -133,7 +159,9 @@ export default {
'accounts/:accountId/unattended/conversations/:conversationId'
),
name: 'conversation_through_unattended',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView,
props: route => ({
conversationId: route.params.conversationId,
@@ -143,7 +171,9 @@ export default {
{
path: frontendURL('accounts/:accountId/participating/conversations'),
name: 'conversation_participating',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView,
props: () => ({ conversationType: 'participating' }),
},
@@ -152,7 +182,9 @@ export default {
'accounts/:accountId/participating/conversations/:conversationId'
),
name: 'conversation_through_participating',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView,
props: route => ({
conversationId: route.params.conversationId,
@@ -28,7 +28,9 @@ export default {
{
path: frontendURL('accounts/:accountId/suspended'),
name: 'account_suspended',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: Suspended,
},
],
@@ -30,13 +30,17 @@ const portalRoutes = [
{
path: getPortalRoute(''),
name: 'default_portal_articles',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator'],
},
component: DefaultPortalArticles,
},
{
path: getPortalRoute('all'),
name: 'list_all_portals',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ListAllPortals,
},
{
@@ -47,55 +51,73 @@ const portalRoutes = [
path: '',
name: 'new_portal_information',
component: PortalDetails,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
{
path: ':portalSlug/customization',
name: 'portal_customization',
component: PortalCustomization,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
{
path: ':portalSlug/finish',
name: 'portal_finish',
component: PortalSettingsFinish,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
],
},
{
path: getPortalRoute(':portalSlug'),
name: 'portalSlug',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ShowPortal,
},
{
path: getPortalRoute(':portalSlug/edit'),
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: EditPortal,
children: [
{
path: '',
name: 'edit_portal_information',
component: EditPortalBasic,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
{
path: 'customizations',
name: 'edit_portal_customization',
component: EditPortalCustomization,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
{
path: 'locales',
name: 'edit_portal_locales',
component: EditPortalLocales,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
{
path: 'categories',
name: 'list_all_locale_categories',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ListAllCategories,
},
],
@@ -106,39 +128,51 @@ const articleRoutes = [
{
path: getPortalRoute(':portalSlug/:locale/articles'),
name: 'list_all_locale_articles',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ListAllArticles,
},
{
path: getPortalRoute(':portalSlug/:locale/articles/new'),
name: 'new_article',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: NewArticle,
},
{
path: getPortalRoute(':portalSlug/:locale/articles/mine'),
name: 'list_mine_articles',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ListAllArticles,
},
{
path: getPortalRoute(':portalSlug/:locale/articles/archived'),
name: 'list_archived_articles',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ListAllArticles,
},
{
path: getPortalRoute(':portalSlug/:locale/articles/draft'),
name: 'list_draft_articles',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ListAllArticles,
},
{
path: getPortalRoute(':portalSlug/:locale/articles/:articleSlug'),
name: 'edit_article',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: EditArticle,
},
];
@@ -147,19 +181,25 @@ const categoryRoutes = [
{
path: getPortalRoute(':portalSlug/:locale/categories'),
name: 'all_locale_categories',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ListAllCategories,
},
{
path: getPortalRoute(':portalSlug/:locale/categories/new'),
name: 'new_category_in_locale',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: NewCategory,
},
{
path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'),
name: 'show_category',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ListAllArticles,
},
{
@@ -167,13 +207,17 @@ const categoryRoutes = [
':portalSlug/:locale/categories/:categorySlug/articles'
),
name: 'show_category_articles',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: ListCategoryArticles,
},
{
path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'),
name: 'edit_category',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: EditCategory,
},
];
@@ -12,13 +12,17 @@ export const routes = [
path: '',
name: 'inbox_view',
component: InboxEmptyStateView,
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
},
{
path: ':notification_id',
name: 'inbox_view_conversation',
component: InboxDetailView,
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
},
],
},
@@ -18,7 +18,9 @@ export const routes = [
path: '',
name: 'notifications_index',
component: NotificationsView,
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
},
],
},
@@ -6,7 +6,9 @@ export default {
routes: [
{
path: frontendURL('accounts/:accountId/settings/general'),
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
component: SettingsContent,
props: {
headerTitle: 'GENERAL_SETTINGS.TITLE',
@@ -18,7 +20,9 @@ export default {
path: '',
name: 'general_settings_index',
component: Index,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
],
},
@@ -8,7 +8,9 @@ export default {
routes: [
{
path: frontendURL('accounts/:accountId/settings/agent-bots'),
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
component: SettingsContent,
props: {
headerTitle: 'AGENT_BOTS.HEADER',
@@ -20,19 +22,25 @@ export default {
path: '',
name: 'agent_bots',
component: Bot,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
{
path: 'csml/new',
name: 'agent_bots_csml_new',
component: CsmlNewBot,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
{
path: 'csml/:botId',
name: 'agent_bots_csml_edit',
component: CsmlEditBot,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
],
},
@@ -15,14 +15,15 @@ export default {
children: [
{
path: '',
name: 'agents_wrapper',
redirect: 'list',
},
{
path: 'list',
name: 'agent_list',
component: AgentHome,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
],
},
@@ -15,14 +15,15 @@ export default {
children: [
{
path: '',
name: 'attributes_wrapper',
redirect: 'list',
},
{
path: 'list',
name: 'attributes_list',
component: AttributesHome,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
],
},
@@ -16,13 +16,14 @@ export default {
children: [
{
path: '',
name: 'auditlogs_wrapper',
redirect: 'list',
},
{
path: 'list',
name: 'auditlogs_list',
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
component: AuditLogsHome,
},
],
@@ -15,14 +15,15 @@ export default {
children: [
{
path: '',
name: 'automation_wrapper',
redirect: 'list',
},
{
path: 'list',
name: 'automation_list',
component: Automation,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
],
},
@@ -6,7 +6,9 @@ export default {
routes: [
{
path: frontendURL('accounts/:accountId/settings/billing'),
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
component: SettingsContent,
props: {
headerTitle: 'BILLING_SETTINGS.TITLE',
@@ -18,7 +20,9 @@ export default {
path: '',
name: 'billing_settings_index',
component: Index,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
],
},
@@ -19,7 +19,9 @@ export default {
{
path: 'ongoing',
name: 'ongoing_campaigns',
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
component: Index,
},
],
@@ -35,7 +37,9 @@ export default {
{
path: 'one_off',
name: 'one_off',
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
component: Index,
},
],
@@ -16,13 +16,14 @@ export default {
children: [
{
path: '',
name: 'canned_wrapper',
redirect: 'list',
},
{
path: 'list',
name: 'canned_list',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: CannedHome,
},
],
@@ -28,14 +28,15 @@ export default {
children: [
{
path: '',
name: 'settings_inbox',
redirect: 'list',
},
{
path: 'list',
name: 'settings_inbox_list',
component: InboxHome,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
{
path: 'new',
@@ -45,19 +46,25 @@ export default {
path: '',
name: 'settings_inbox_new',
component: ChannelList,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
{
path: ':inbox_id/finish',
name: 'settings_inbox_finish',
component: FinishSetup,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
{
path: ':sub_page',
name: 'settings_inboxes_page_channel',
component: channelFactory.create(),
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
props: route => {
return { channel_name: route.params.sub_page };
},
@@ -65,7 +72,9 @@ export default {
{
path: ':inbox_id/agents',
name: 'settings_inboxes_add_agents',
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
component: AddAgents,
},
],
@@ -74,7 +83,9 @@ export default {
path: ':inboxId',
name: 'settings_inbox_show',
component: Settings,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
],
},
@@ -26,13 +26,17 @@ export default {
path: '',
name: 'settings_applications',
component: Index,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
{
path: ':integration_id',
name: 'settings_applications_integration',
component: IntegrationHooks,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
props: route => ({
integrationId: route.params.integration_id,
}),
@@ -30,32 +30,42 @@ export default {
path: '',
name: 'settings_integrations',
component: Index,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
{
path: 'webhook',
component: Webhook,
name: 'settings_integrations_webhook',
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
{
path: 'dashboard-apps',
component: DashboardApps,
name: 'settings_integrations_dashboard_apps',
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
{
path: 'slack',
name: 'settings_integrations_slack',
component: Slack,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
props: route => ({ code: route.query.code }),
},
{
path: ':integration_id',
name: 'settings_integrations_integration',
component: ShowIntegration,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
props: route => {
return {
integrationId: route.params.integration_id,
@@ -17,13 +17,17 @@ export default {
{
path: '',
name: 'labels_wrapper',
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
redirect: 'list',
},
{
path: 'list',
name: 'labels_list',
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
component: Index,
},
],
@@ -23,19 +23,25 @@ export default {
path: '',
name: 'macros_wrapper',
component: Macros,
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
},
{
path: 'new',
name: 'macros_new',
component: MacroEditor,
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
},
{
path: ':macroId/edit',
name: 'macros_edit',
component: MacroEditor,
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
},
],
},
@@ -8,14 +8,18 @@ export default {
{
path: frontendURL('accounts/:accountId/profile'),
name: 'profile_settings',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
component: SettingsContent,
children: [
{
path: 'settings',
name: 'profile_settings_index',
component: Index,
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
},
],
},
@@ -29,7 +29,9 @@ export default {
{
path: 'overview',
name: 'account_overview_reports',
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
component: LiveReports,
},
],
@@ -46,7 +48,9 @@ export default {
{
path: 'conversation',
name: 'conversation_reports',
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
component: Index,
},
],
@@ -63,7 +67,9 @@ export default {
{
path: 'csat',
name: 'csat_reports',
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
component: CsatResponses,
},
],
@@ -80,7 +86,9 @@ export default {
{
path: 'bot',
name: 'bot_reports',
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
component: BotReports,
},
],
@@ -97,7 +105,9 @@ export default {
{
path: 'agent',
name: 'agent_reports',
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
component: AgentReports,
},
],
@@ -114,7 +124,9 @@ export default {
{
path: 'label',
name: 'label_reports',
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
component: LabelReports,
},
],
@@ -131,7 +143,9 @@ export default {
{
path: 'inboxes',
name: 'inbox_reports',
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
component: InboxReports,
},
],
@@ -147,7 +161,9 @@ export default {
{
path: 'teams',
name: 'team_reports',
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
component: TeamReports,
},
],
@@ -164,7 +180,9 @@ export default {
{
path: 'sla',
name: 'sla_reports',
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
component: SLAReports,
},
],
@@ -24,7 +24,9 @@ export default {
{
path: frontendURL('accounts/:accountId/settings'),
name: 'settings_home',
roles: ['administrator', 'agent'],
meta: {
permissions: ['administrator', 'agent'],
},
redirect: () => {
if (store.getters.getCurrentRole === 'administrator') {
return frontendURL('accounts/:accountId/settings/general');
@@ -13,13 +13,17 @@ export default {
{
path: '',
name: 'sla_wrapper',
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
redirect: 'list',
},
{
path: 'list',
name: 'sla_list',
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
component: Index,
},
],
@@ -29,14 +29,15 @@ export default {
children: [
{
path: '',
name: 'settings_teams',
redirect: 'list',
},
{
path: 'list',
name: 'settings_teams_list',
component: TeamsHome,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
{
path: 'new',
@@ -46,18 +47,24 @@ export default {
path: '',
name: 'settings_teams_new',
component: CreateTeam,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
{
path: ':teamId/finish',
name: 'settings_teams_finish',
component: FinishSetup,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
{
path: ':teamId/agents',
name: 'settings_teams_add_agents',
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
component: AddAgents,
},
],
@@ -70,18 +77,24 @@ export default {
path: '',
name: 'settings_teams_edit',
component: EditTeam,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
{
path: 'agents',
name: 'settings_teams_edit_members',
component: EditAgents,
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
},
{
path: 'finish',
name: 'settings_teams_edit_finish',
roles: ['administrator'],
meta: {
permissions: ['administrator'],
},
component: FinishSetup,
},
],
+3 -28
View File
@@ -5,33 +5,12 @@ import dashboard from './dashboard/dashboard.routes';
import store from '../store';
import { validateLoggedInRoutes } from '../helper/routeHelpers';
import AnalyticsHelper from '../helper/AnalyticsHelper';
import { buildPermissionsFromRouter } from '../helper/permissionsHelper';
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 routesWithPermissions = buildPermissionsFromRouter(routes);
export const validateAuthenticateRoutePermission = (to, next, { getters }) => {
const { isLoggedIn, getCurrentUser: user } = getters;
@@ -45,11 +24,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();
};
+3 -3
View File
@@ -16,10 +16,10 @@ KillMode=mixed
StandardInput=null
SyslogIdentifier=%p
Environment="PATH=/home/chatwoot/.rvm/gems/ruby-3.2.2/bin:/home/chatwoot/.rvm/gems/ruby-3.2.2@global/bin:/home/chatwoot/.rvm/rubies/ruby-3.2.2/bin:/home/chatwoot/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/chatwoot/.rvm/bin:/home/chatwoot/.rvm/bin"
Environment="PATH=/home/chatwoot/.rvm/gems/ruby-3.3.3/bin:/home/chatwoot/.rvm/gems/ruby-3.3.3@global/bin:/home/chatwoot/.rvm/rubies/ruby-3.3.3/bin:/home/chatwoot/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/chatwoot/.rvm/bin:/home/chatwoot/.rvm/bin"
Environment="PORT=3000"
Environment="RAILS_ENV=production"
Environment="NODE_ENV=production"
Environment="RAILS_LOG_TO_STDOUT=true"
Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-3.2.2"
Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-3.2.2:/home/chatwoot/.rvm/gems/ruby-3.2.2@global"
Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-3.3.3"
Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-3.3.3:/home/chatwoot/.rvm/gems/ruby-3.3.3@global"
+3 -3
View File
@@ -16,10 +16,10 @@ KillMode=mixed
StandardInput=null
SyslogIdentifier=%p
Environment="PATH=/home/chatwoot/.rvm/gems/ruby-3.2.2/bin:/home/chatwoot/.rvm/gems/ruby-3.2.2@global/bin:/home/chatwoot/.rvm/rubies/ruby-3.2.2/bin:/home/chatwoot/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/chatwoot/.rvm/bin:/home/chatwoot/.rvm/bin"
Environment="PATH=/home/chatwoot/.rvm/gems/ruby-3.3.3/bin:/home/chatwoot/.rvm/gems/ruby-3.3.3@global/bin:/home/chatwoot/.rvm/rubies/ruby-3.3.3/bin:/home/chatwoot/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/chatwoot/.rvm/bin:/home/chatwoot/.rvm/bin"
Environment="PORT=3000"
Environment="RAILS_ENV=production"
Environment="NODE_ENV=production"
Environment="RAILS_LOG_TO_STDOUT=true"
Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-3.2.2"
Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-3.2.2:/home/chatwoot/.rvm/gems/ruby-3.2.2@global"
Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-3.3.3"
Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-3.3.3:/home/chatwoot/.rvm/gems/ruby-3.3.3@global"
+2 -2
View File
@@ -336,8 +336,8 @@ function setup_chatwoot() {
sudo -i -u chatwoot << EOF
rvm --version
rvm autolibs disable
rvm install "ruby-3.2.2"
rvm use 3.2.2 --default
rvm install "ruby-3.3.3"
rvm use 3.3.3 --default
git clone https://github.com/chatwoot/chatwoot.git
cd chatwoot
+5 -5
View File
@@ -1,5 +1,5 @@
# pre-build stage
FROM ruby:3.2.2-alpine3.18 AS pre-builder
FROM ruby:3.3.3-alpine3.19 AS pre-builder
# ARG default to production settings
# For development docker-compose file overrides ARGS
@@ -25,7 +25,7 @@ RUN apk update && apk add --no-cache \
tzdata \
postgresql-dev \
postgresql-client \
nodejs-current \
nodejs=20.12.1-r0 \
yarn \
git \
&& mkdir -p /var/app \
@@ -67,13 +67,13 @@ RUN if [ "$RAILS_ENV" = "production" ]; then \
RUN git rev-parse HEAD > /app/.git_sha
# Remove unnecessary files
RUN rm -rf /gems/ruby/3.2.0/cache/*.gem \
&& find /gems/ruby/3.2.0/gems/ \( -name "*.c" -o -name "*.o" \) -delete \
RUN rm -rf /gems/ruby/3.3.0/cache/*.gem \
&& find /gems/ruby/3.3.0/gems/ \( -name "*.c" -o -name "*.o" \) -delete \
&& rm -rf .git \
&& rm .gitignore
# final build stage
FROM ruby:3.2.2-alpine3.18
FROM ruby:3.3.3-alpine3.19
ARG BUNDLE_WITHOUT="development:test"