diff --git a/doc/router-navigation-guard.md b/doc/router-navigation-guard.md new file mode 100644 index 00000000..8960a761 --- /dev/null +++ b/doc/router-navigation-guard.md @@ -0,0 +1,174 @@ +# Router Navigation Guard + +The access control logic for the routes is handled in the file `routes.js`. +Here the `beforeEach` navigation guard controls access to routes in the application based on authentication status, user role, user profile attributes, license availability and user capabilities. + + +## Authentication Check +First, the guard checks if the user has a valid JWT token using `hasJwt()` + +**Unauthenticated users**: +- Are redirected to `/login` if trying to access protected routes +- Can access public routes (`/login`, `/recoverpassword`, `/changepassword`) + + +## Route Redirects for Authenticated Users +- Users trying to access `/login` when already authenticated are redirected to the home page +- Users trying to access `/conference` are redirected to `/conference/room123` (default room) + + +## Route Authorization Checks +For all other routes, the guard implements a multi-layered authorization system: + + +### Route Authorization System +The implementation uses a sequential check system, evaluating each permission requirement in order: + +```js +default: { + // 1. Admin check + if (to.meta?.adminOnly && !store.getters['user/isAdmin']) { + return next('/') + } + + // 2. Profile attribute check + if (to.meta?.profileAttribute && + !store.getters['user/hasSubscriberProfileAttribute'](to.meta.profileAttribute)) { + return next('/') + } + + // 3. Profile attributes array check + if (to.meta?.profileAttributes && + !store.getters['user/hasSomeSubscriberProfileAttributes'](to.meta.profileAttributes)) { + return next('/') + } + + // 4. License check + if (to.meta?.license) { + const isSpCe = store.getters['user/isSpCe'] + + // CE-specific check + if (isSpCe && !to.meta.allowCE) { + return next('/') + } + + // License check for non-CE users + if (!isSpCe && !store.getters['user/hasLicenses']([to.meta.license])) { + return next('/') + } + } + // 5. Platform Feature check + if (to.meta?.platformFeature && + !store.getters['user/hasPlatformFeature'](to.meta.platformFeature)) { + return next('/') + } + + // 5. Capability check + if (to.meta?.capability && + !store.getters['user/hasCapability'](to.meta.capability)) { + return next('/') + } + + // All checks passed + next() +} +``` + +### 1. Admin-Only Check +- Verifies if the route requires admin access (`adminOnly: true`) +- Redirects to home page (/) if user is not an admin + +### 2. Single Profile Attribute Check +- Verifies the user has the specific profile attribute required by the route +- Redirects to home page (/) if the attribute is missing + +### 3. Multiple Profile Attributes Check +- Checks if the user has at least one of the required profile attributes in the array +- Redirects to home page (/) if no matching attributes are found + +### 4. License Check +- Two-part check based on user type: + - For Community Edition (CE) users: Only allows access if the route explicitly allows CE users (`allowCE: true`) + - For regular users: Verifies the required licenses are active +- Redirects to home page (/) if license requirements are not met + +### 5. Platform Feature Check +- Verifies if the feature required by the route is active platform-wide +- Redirects to home page (/) if the platform feature is missing + +### 6. Capability Check +- Verifies if the user has the specific capability required by the route +- Redirects to home page (/) if the capability is missing + +### Default Case (All Checks Pass) +- If all applicable checks pass, the user is allowed to access the route +- Routes without any restrictions are accessible to all authenticated users + + +## Route Meta Fields: + +- `profileAttribute`: Single profile attribute required (e.g., PROFILE_ATTRIBUTE_MAP.conversations) +- `profileAttributes`: Array of required group attributes (e.g., PROFILE_ATTRIBUTES_MAP.callSettings). +- `licenses`: Array of required license keys (e.g., LICENSES.fax). +- `platformFeature`: string of required ngcp feature +- `capability`: string of required user capability + +Note: Attributes are the response of the call `/api/subscriberprofiles/:profile_id`. `profile_id` is one of the properties returned by `/api/subscribers`. + + + +## Menu Visibility and Consistency with Route Authorization + +The main menu, implemented in `CscMainMenuTop.vue`, must maintain consistency with the route authorization checks to ensure a seamless user experience. Menu items are conditionally rendered based on the same criteria used for route access control. + + +### Menu Item Visibility Logic + +```js +{ + to: '/user/home', + icon: 'call', + label: this.callStateTitle, + sublabel: this.callStateSubtitle, + visible: this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.cscCalls) && + (this.isSpCe || this.hasLicenses([LICENSES.csc_calls])) +} +``` + +Each menu item includes a `visible` property that determines whether it should be displayed to the user. This visibility check typically includes: + +1. **Profile Attribute Checks** - Using the same methods as the route guard: + - `hasSubscriberProfileAttribute()` for single attribute requirements + - `hasSomeSubscriberProfileAttributes()` for multiple attribute requirements + +2. **License Validation** - For features requiring licenses: + - `hasLicenses()` checks if the required licenses are active + - Special handling for SpCe users with `isSpCe` flag + +3. **Platform and User Capability Checks** - Verifies that the ngcp platform has the necessary modules activated and that the module is enabled for the user. This check also incudes the license check for the feature: + - `this.isFaxFeatureEnabled()` checks if the fax feature is enabled in the platform, if it is enabled for the user and if the license fax is active. Note, this doesn't include the checks about fax server settings. + - `this.isPbxEnabled()` checks if the pbx feature is enabled in the platform, if it's enabled for the user and if the license pbx is active. + - `this.isSmsEnabled()` checks if the sms feature is enabled in the platform, if it's enabled for the user and if the license sms is active. + +**IMPORTANT** The Menu Item Visibility Logic needs to be aligned with with Route Guards + +### Menu Hierarchy and Nested Items + +Menu items with children (submenu items) follow additional rules: + +1. **Parent Visibility**: A parent menu item may be visible even when some children are not +2. **Child Visibility**: Each child item has its own visibility condition +3. **Dynamic Expansion**: Some menu sections are automatically expanded based on the current route: + ```js + opened: this.isPbxConfiguration + ``` + +### Preventing UI/Navigation Inconsistency + +This dual-layer approach ensures that: + +1. Users only see menu items for features they can access +2. If a menu item is mistakenly visible, the route guard still prevents unauthorized access +3. Direct URL navigation attempts are blocked for unauthorized routes, even if a user bypasses the UI + +By maintaining consistency between the menu visibility logic and the route authorization checks, the application provides a coherent user experience while maintaining strong access control. diff --git a/src/api/user.js b/src/api/user.js index e32484e5..fc763e9b 100644 --- a/src/api/user.js +++ b/src/api/user.js @@ -8,6 +8,7 @@ import { httpApi } from './common' import { getFaxServerSettings } from 'src/api/fax' +import { LICENSES } from 'src/constants' export function login (username, password) { return new Promise((resolve, reject) => { @@ -60,17 +61,21 @@ export async function getUserData (id) { ]) try { + let isFaxServerSettingsActive = false const [subscriber, capabilities, resellerBranding, platformInfo] = await allPromise - if (capabilities.faxserver && platformInfo.licenses.find((license) => license === 'fax')) { - const faxServerSettings = await getFaxServerSettings(id) - capabilities.faxactive = faxServerSettings.active + if (capabilities.faxserver && platformInfo.licenses.find((license) => license === LICENSES.fax)) { + // Note that isFaxServerSettingsActive determines if the menu has been enabled by admin + // or, in other words, if the relevant toggle is on/off. + const responseFaxServerSettings = await getFaxServerSettings(id) + isFaxServerSettingsActive = responseFaxServerSettings.active } return { subscriber, capabilities, resellerBranding: resellerBranding?.items[0] || null, - platformInfo + platformInfo, + isFaxServerSettingsActive } } catch (error) { throw new Error(error.response.data.message) @@ -89,6 +94,11 @@ export function getSubscriberById (id) { }) } +/** + * Determines if specific users should have access to features based on their roles and profiles. + * Retrieves a list of capabilities and their enabled status from the API. + * @returns {Promise} A promise that resolves to an object of capabilities with their enabled status + */ export function getCapabilities () { return new Promise((resolve, reject) => { getList({ diff --git a/src/boot/routes.js b/src/boot/routes.js index dcccf37d..a70fa656 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -32,25 +32,54 @@ export default ({ app, router, store }) => { path: '/conference/room123' }) break - default: - if (to.meta?.profileAttribute) { - const hasSubscriberProfileAttribute = store.getters['user/hasSubscriberProfileAttribute'](to.meta.profileAttribute) - if (to.meta.license && hasSubscriberProfileAttribute) { - // Guard to assure that: - // CE users have access to all available menus as they do not have licenses - if (store.getters['user/isSpCe']) { - next() - } - // users cannot click on menu if it is mistakenly visible when the license is inactive - store.getters['user/isLicenseActive'](to.meta.license) ? next() : next('/') + default: { + // 1. Admin check + if (to.meta?.adminOnly && !store.getters['user/isAdmin']) { + return next('/') + } + + // 2. Profile attribute check + if (to.meta?.profileAttribute && + !store.getters['user/hasSubscriberProfileAttribute'](to.meta.profileAttribute)) { + return next('/') + } + + // 3. Profile attributes array check + if (to.meta?.profileAttributes && + !store.getters['user/hasSomeSubscriberProfileAttributes'](to.meta.profileAttributes)) { + return next('/') + } + + // 4. License check + if (to.meta?.license) { + const isSpCe = store.getters['user/isSpCe'] + + // CE-specific check + if (isSpCe && !to.meta.allowCE) { + return next('/') } - hasSubscriberProfileAttribute ? next() : next('/') - } else if (to.meta?.profileAttributes) { - store.getters['user/hasSubscriberProfileAttributes'](to.meta.profileAttributes) ? next() : next('/') - } else { - next() + // License check for non-CE users + if (!isSpCe && !store.getters['user/hasLicenses']([to.meta.licenses])) { + return next('/') + } } + + // 5. Platform Feature check + if (to.meta?.platformFeature && + !store.getters['user/hasPlatformFeature'](to.meta.platformFeature)) { + return next('/') + } + + // 6. Capability check + if (to.meta?.capability && + !store.getters['user/hasCapability'](to.meta.capability)) { + return next('/') + } + + // All checks passed, route is accessible + next() + } } } }) diff --git a/src/components/CscMainMenuTop.vue b/src/components/CscMainMenuTop.vue index f1e50718..b1ce1ea6 100644 --- a/src/components/CscMainMenuTop.vue +++ b/src/components/CscMainMenuTop.vue @@ -45,16 +45,36 @@ export default { }, computed: { ...mapGetters('user', [ + 'isFaxFeatureEnabled', 'getCustomerId', - 'hasFaxCapability', 'hasSubscriberProfileAttribute', - 'hasSubscriberProfileAttributes', - 'isLicenseActive', - 'isOldCSCProxyingAllowed', + 'hasSomeSubscriberProfileAttributes', + 'hasLicenses', 'isPbxEnabled', 'isSpCe' ]), items () { + const hasCallSettingsSubmenus = this.hasSomeSubscriberProfileAttributes(PROFILE_ATTRIBUTES_MAP.callSettings) || + this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.voiceMail) || + this.hasSomeSubscriberProfileAttributes(PROFILE_ATTRIBUTES_MAP.callForwarding) || + this.hasSomeSubscriberProfileAttributes(PROFILE_ATTRIBUTES_MAP.callBlockingIncoming) || + this.hasSomeSubscriberProfileAttributes(PROFILE_ATTRIBUTES_MAP.callBlockingOutgoing) || + this.hasSomeSubscriberProfileAttributes(PROFILE_ATTRIBUTES_MAP.callBlockingPrivacy) || + this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.speedDial) || + ( + this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.recordings) && + (this.isSpCe || this.hasLicenses([LICENSES.call_recording])) + ) + + const hasCustomerPreferenceSubmenus = this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.blockInClir) || + this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.blockInList) || + this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.blockOutList) || + this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.blockInMode) || + this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.blockOutMode) || + this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.blockOutOverridePin) || + this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.huntGroups) || + this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.playAnnounceBeforeCallSetup) || + this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.playAnnounceToCallee) return [ { to: '/user/dashboard', @@ -67,7 +87,8 @@ export default { icon: 'call', label: this.callStateTitle, sublabel: this.callStateSubtitle, - visible: this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.cscCalls && (this.isSpCe || this.isLicenseActive(LICENSES.csc_calls))) + visible: this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.cscCalls) && + (this.isSpCe || this.hasLicenses([LICENSES.csc_calls])) }, { to: '/user/conversations', @@ -80,18 +101,18 @@ export default { to: '/user/subscriber-phonebook', icon: 'fas fa-user', label: this.$t('Subscriber Phonebook'), - visible: this.isLicenseActive(LICENSES.phonebook) + visible: this.hasLicenses([LICENSES.phonebook]) }, { icon: 'settings_phone', label: this.$t('Call Settings'), - visible: true, + visible: hasCallSettingsSubmenus, children: [ { to: '/user/call-settings', icon: 'settings', label: this.$t('General'), - visible: this.hasSubscriberProfileAttributes(PROFILE_ATTRIBUTES_MAP.callSettings) + visible: this.hasSomeSubscriberProfileAttributes(PROFILE_ATTRIBUTES_MAP.callSettings) }, { to: '/user/voicebox', @@ -103,25 +124,25 @@ export default { to: '/user/call-forwarding', icon: 'phone_forwarded', label: this.$t('Forwarding'), - visible: true + visible: this.hasSomeSubscriberProfileAttributes(PROFILE_ATTRIBUTES_MAP.callForwarding) }, { to: '/user/call-blocking/incoming', icon: 'call_received', label: this.$t('Block Incoming'), - visible: this.hasSubscriberProfileAttributes(PROFILE_ATTRIBUTES_MAP.callBlockingIncoming) + visible: this.hasSomeSubscriberProfileAttributes(PROFILE_ATTRIBUTES_MAP.callBlockingIncoming) }, { to: '/user/call-blocking/outgoing', icon: 'call_made', label: this.$t('Block Outgoing'), - visible: this.hasSubscriberProfileAttributes(PROFILE_ATTRIBUTES_MAP.callBlockingOutgoing) + visible: this.hasSomeSubscriberProfileAttributes(PROFILE_ATTRIBUTES_MAP.callBlockingOutgoing) }, { to: '/user/call-blocking/privacy', icon: 'fas fa-user-secret', label: this.$t('Privacy'), - visible: this.hasSubscriberProfileAttributes(PROFILE_ATTRIBUTES_MAP.callBlockingPrivacy) + visible: this.hasSomeSubscriberProfileAttributes(PROFILE_ATTRIBUTES_MAP.callBlockingPrivacy) }, { to: '/user/speeddial', @@ -139,7 +160,8 @@ export default { to: '/user/recordings', icon: 'play_circle', label: this.$t('Recordings'), - visible: this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.recordings) && (this.isSpCe || this.isLicenseActive(LICENSES.call_recording)) + visible: this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.recordings) && + (this.isSpCe || this.hasLicenses([LICENSES.call_recording])) } ] }, @@ -147,84 +169,84 @@ export default { to: '/user/fax-settings', icon: 'fas fa-fax', label: this.$t('Fax Settings'), - visible: this.hasFaxCapability && - this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.faxServer) && - this.isLicenseActive(LICENSES.fax) + visible: this.isFaxFeatureEnabled }, { icon: 'fas fa-chart-line', label: this.$t('PBX Statistics'), - visible: this.isPbxAdmin && this.isLicenseActive(LICENSES.pbx), + visible: this.isPbxAdmin, opened: this.isPbxConfiguration, children: [ { to: '/user/pbx-statistics/cdr', icon: 'fas fa-table', label: this.$t('Cdr'), - visible: true + visible: this.isPbxAdmin } ] }, { icon: 'miscellaneous_services', label: this.$t('PBX Configuration'), - visible: this.isPbxAdmin && this.isLicenseActive(LICENSES.pbx), + visible: this.isPbxAdmin, opened: this.isPbxConfiguration, children: [ { to: '/user/pbx-configuration/seats', icon: 'person', label: this.$t('Seats'), - visible: true + visible: this.isPbxAdmin }, { to: '/user/pbx-configuration/groups', icon: 'group', label: this.$t('Groups'), - visible: this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.huntGroups) + visible: this.isPbxAdmin && this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.huntGroups) }, { to: '/user/pbx-configuration/devices', icon: 'fas fa-fax', label: this.$t('Devices'), - visible: this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.deviceProvisioning) && this.isLicenseActive(LICENSES.device_provisioning) + visible: this.isPbxAdmin && + this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.deviceProvisioning) && + this.hasLicenses([LICENSES.device_provisioning]) }, { to: '/user/pbx-configuration/call-queues', icon: 'filter_none', label: this.$t('Call Queues'), - visible: this.hasSubscriberProfileAttributes(PROFILE_ATTRIBUTES_MAP.pbxSettingsCallQueue) + visible: this.isPbxAdmin && this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.cloudPbxCallQueue) }, { to: '/user/pbx-configuration/sound-sets', icon: 'queue_music', label: this.$t('Sound Sets'), - visible: this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.soundSet) + visible: this.isPbxAdmin && this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.soundSet) }, { to: '/user/pbx-configuration/ms-configs', icon: 'arrow_forward', label: this.$t('Manager Secretary'), - visible: this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.manager_secretary) + visible: this.isPbxAdmin && this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.managerSecretary) }, { to: '/user/pbx-configuration/auto-attendant', icon: 'dialpad', label: this.$t('Auto Attendant'), - visible: this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.auto_attendant) + visible: this.isPbxAdmin && this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.autoAttendant) }, { to: '/user/pbx-configuration/customer-phonebook', icon: 'person', label: this.$t('Customer Phonebook'), - visible: this.isLicenseActive(LICENSES.phonebook) + visible: this.isPbxAdmin && this.hasLicenses([LICENSES.phonebook]) }, { to: '/user/pbx-configuration/customer-preferences', icon: 'fas fa-user-cog', label: this.$t('Customer Preferences'), - visible: true + visible: this.isPbxAdmin && this.hasLicenses([LICENSES.phonebook]) && hasCustomerPreferenceSubmenus } ] }, @@ -232,26 +254,25 @@ export default { icon: 'settings', label: this.$t('Extension Settings'), visible: this.isPbxEnabled && - this.hasSubscriberProfileAttributes(PROFILE_ATTRIBUTES_MAP.pbxSettings) && - this.isLicenseActive(LICENSES.pbx), + this.hasSomeSubscriberProfileAttributes(PROFILE_ATTRIBUTES_MAP.pbxSettings), children: [ { to: '/user/extension-settings/call-queues', icon: 'filter_none', label: this.$t('Call Queues'), - visible: this.isPbxEnabled && this.hasSubscriberProfileAttributes(PROFILE_ATTRIBUTES_MAP.pbxSettingsCallQueue) + visible: this.isPbxEnabled && this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.cloudPbxCallQueue) }, { to: '/user/extension-settings/ms-configs', icon: 'arrow_forward', label: this.$t('Manager Secretary'), - visible: this.isPbxEnabled && this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.manager_secretary) + visible: this.isPbxEnabled && this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTES_MAP.manager_secretary) }, { to: '/user/extension-settings/auto-attendant', icon: 'dialpad', label: this.$t('Auto Attendant'), - visible: this.isPbxEnabled && this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.auto_attendant) + visible: this.isPbxEnabled && this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTES_MAP.autoAttendant) } ] }, diff --git a/src/components/CscVoiceboxLanguage.vue b/src/components/CscVoiceboxLanguage.vue index 41a3b907..3a64e925 100644 --- a/src/components/CscVoiceboxLanguage.vue +++ b/src/components/CscVoiceboxLanguage.vue @@ -5,8 +5,8 @@ :model-value="value" emit-value map-options - :disable="loading" - :readonly="loading" + :disable="disabled || loading" + :readonly="disabled || loading" :label="$t('Language for voicemail and app server')" data-cy="voicebox-change-language" :title="$t('Voice prompts language for voicemail, conference and application server')" @@ -54,6 +54,10 @@ export default { loading: { type: Boolean, default: false + }, + disabled: { + type: Boolean, + default: false } } } diff --git a/src/components/call-forwarding/CscCfGroupTitle.vue b/src/components/call-forwarding/CscCfGroupTitle.vue index ad8f874d..e082fb5f 100644 --- a/src/components/call-forwarding/CscCfGroupTitle.vue +++ b/src/components/call-forwarding/CscCfGroupTitle.vue @@ -229,7 +229,7 @@ })" />
import _ from 'lodash' +import { PROFILE_ATTRIBUTE_MAP } from 'src/constants' import CscSpinner from '../../CscSpinner' import { mapGetters @@ -184,7 +185,8 @@ export default { 'isAnonymousBlockRequesting' ]), ...mapGetters('user', [ - 'hasSubscriberProfileAttribute' + 'hasSubscriberProfileAttribute', + 'hasSomeSubscriberProfileAttributes' ]), toggleButtonLabel () { if (!this.enabled) { @@ -218,6 +220,9 @@ export default { classes.push('csc-toggle-disabled') } return classes + }, + showNcosMenus () { + return this.hasSomeSubscriberProfileAttributes([PROFILE_ATTRIBUTE_MAP.ncos, PROFILE_ATTRIBUTE_MAP.ncosSet]) } }, watch: { diff --git a/src/components/pages/CallForward/CscCallForwardDetails.vue b/src/components/pages/CallForward/CscCallForwardDetails.vue index 4dea49fb..8ffd2c5d 100644 --- a/src/components/pages/CallForward/CscCallForwardDetails.vue +++ b/src/components/pages/CallForward/CscCallForwardDetails.vue @@ -134,7 +134,7 @@ export default { ]), ...mapGetters('user', [ 'hasSubscriberProfileAttribute', - 'hasSubscriberProfileAttributes' + 'hasSomeSubscriberProfileAttributes' ]), ...mapGetters('callForwarding', [ 'groups', diff --git a/src/components/pages/FaxSettings/CscFaxToMailSettings.vue b/src/components/pages/FaxSettings/CscFaxToMailSettings.vue index 33c6c524..fd7a468b 100644 --- a/src/components/pages/FaxSettings/CscFaxToMailSettings.vue +++ b/src/components/pages/FaxSettings/CscFaxToMailSettings.vue @@ -163,10 +163,6 @@ export default { id: { type: String, default: '' - }, - isPbxConfigurationContext: { - type: Boolean, - default: false } }, data () { @@ -216,7 +212,7 @@ export default { }, async setChangedData (field, value) { try { - await this.faxServerSettingsUpdateAction({ field, value, id: this.id, fromPbxConfiguration: this.isPbxConfigurationContext }) + await this.faxServerSettingsUpdateAction({ field, value, id: this.id }) this.updateDataFromStore() } catch (err) { showGlobalError(err?.message) diff --git a/src/components/pages/PbxConfiguration/CscPbxCallQueue.vue b/src/components/pages/PbxConfiguration/CscPbxCallQueue.vue index 3c488080..ea7c1754 100644 --- a/src/components/pages/PbxConfiguration/CscPbxCallQueue.vue +++ b/src/components/pages/PbxConfiguration/CscPbxCallQueue.vue @@ -47,6 +47,7 @@ :label="$t('Maximum calls in queue')" :error="v$.changes.max_queue_length.$errors.length > 0" :error-message="queueMaxLengthErrorMessage" + :disable="disableMaxQueueLength" @update:model-value="v$.changes.max_queue_length.$touch()" @keyup.enter="save" > @@ -69,6 +70,7 @@ :label="$t('Wrap up time')" :error="v$.changes.queue_wrap_up_time.$errors.length > 0" :error-message="queueWrapUpTimeErrorMessage" + :disable="disableQueueWrapUpTime" @update:model-value="v$.changes.queue_wrap_up_time.$touch()" @keyup.enter="save" > @@ -102,6 +104,8 @@ import CscListMenuItem from '../../CscListMenuItem' import CscInputButtonSave from 'components/form/CscInputButtonSave' import CscInputButtonReset from 'components/form/CscInputButtonReset' import useValidate from '@vuelidate/core' +import { PROFILE_ATTRIBUTE_MAP } from 'src/constants' +import { mapGetters } from 'vuex' export default { name: 'CscPbxCallQueue', components: { @@ -164,6 +168,15 @@ export default { } }, computed: { + ...mapGetters('user', [ + 'hasSubscriberProfileAttribute' + ]), + disableMaxQueueLength () { + return !this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.maxQueueLength) + }, + disableQueueWrapUpTime () { + return !this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.queueWrapUpTime) + }, getTitleIcon () { let icon = 'person' if (this.subscriber.is_pbx_group) { diff --git a/src/components/pages/PbxConfiguration/CscPbxCallQueueAddForm.vue b/src/components/pages/PbxConfiguration/CscPbxCallQueueAddForm.vue index 311aa643..3bcc9924 100644 --- a/src/components/pages/PbxConfiguration/CscPbxCallQueueAddForm.vue +++ b/src/components/pages/PbxConfiguration/CscPbxCallQueueAddForm.vue @@ -13,17 +13,16 @@ v-model="data.max_queue_length" :error="v$.data.max_queue_length.$errors.length > 0" :error-message="maxQueueLengthErrorMessage" - :disable="loading" + :disable="disableMaxQueueLength || loading" :readonly="loading" :label="$t('Queue Length')" - default="3" @update:model-value="v$.data.max_queue_length.$touch()" /> 0 && errorsTab[0].$validator === 'numeric') { @@ -172,7 +182,7 @@ export default { return { subscriber_id: null, max_queue_length: this.defaultMaxQueueLength, - queue_wrap_up_time: this.defaultWrapUpTime + queue_wrap_up_time: this.defaultQueueWrapUpTime } }, cancel () { diff --git a/src/components/pages/PbxConfiguration/CscPbxDeviceConfigKeyForm.vue b/src/components/pages/PbxConfiguration/CscPbxDeviceConfigKeyForm.vue index 2938dc90..d21195a8 100644 --- a/src/components/pages/PbxConfiguration/CscPbxDeviceConfigKeyForm.vue +++ b/src/components/pages/PbxConfiguration/CscPbxDeviceConfigKeyForm.vue @@ -114,13 +114,10 @@ import _ from 'lodash' import CscPbxAutoAttendantSelection from './CscPbxAutoAttendantSelection' import CscInput from 'components/form/CscInput' import CscListSpinner from 'components/CscListSpinner' -import { mapState } from 'vuex' -import { - Platform -} from 'quasar' -import { - required -} from '@vuelidate/validators' +import { PROFILE_ATTRIBUTE_MAP } from 'src/constants' +import { mapGetters, mapState } from 'vuex' +import { Platform } from 'quasar' +import { required } from '@vuelidate/validators' import useValidate from '@vuelidate/core' export default { name: 'CscPbxDeviceConfigKeyForm', @@ -172,6 +169,9 @@ export default { ...mapState('pbx', [ 'subscriberList' ]), + ...mapGetters('user', [ + 'hasSubscriberProfileAttribute' + ]), hasSubscriberChanged () { return this.keyData.subscriber_id !== this.changes.subscriber_id }, @@ -261,7 +261,7 @@ export default { value: 'shared' }) } - if (this.selectedKey !== null && this.selectedKey.keySet.can_speeddial) { + if (this.selectedKey !== null && this.selectedKey.keySet.can_speeddial && this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.speedDial)) { options.push({ label: this.$t('Speed Dial'), value: 'speeddial' diff --git a/src/components/pages/PbxConfiguration/CscPbxSeat.vue b/src/components/pages/PbxConfiguration/CscPbxSeat.vue index d9a12b9a..c133e5a5 100644 --- a/src/components/pages/PbxConfiguration/CscPbxSeat.vue +++ b/src/components/pages/PbxConfiguration/CscPbxSeat.vue @@ -81,8 +81,9 @@ - + @@ -96,6 +97,7 @@ @@ -119,6 +121,8 @@ import CscMoreMenu from 'components/CscMoreMenu' import CscPopupMenuItemDelete from 'components/CscPopupMenuItemDelete' import CscPopupMenuItem from 'components/CscPopupMenuItem' import CscDialogChangePassword from 'components/CscDialogChangePassword' +import { PROFILE_ATTRIBUTES_MAP, PROFILE_ATTRIBUTE_MAP } from 'src/constants' +import { mapGetters } from 'vuex' export default { name: 'CscPbxSeat', components: { @@ -154,6 +158,18 @@ export default { changes: this.getSeatData() } }, + computed: { + ...mapGetters('user', [ + 'hasSubscriberProfileAttribute', + 'hasSomeSubscriberProfileAttributes' + ]), + showClirIntraPbx () { + return this.hasSubscriberProfileAttribute(PROFILE_ATTRIBUTE_MAP.clir_intrapbx) + }, + showMusicOnHold () { + return this.hasSomeSubscriberProfileAttributes(PROFILE_ATTRIBUTES_MAP.callSettings) + } + }, watch: { seat () { this.changes = this.getSeatData() diff --git a/src/components/pages/PbxStatistics/CscCdrFilters.vue b/src/components/pages/PbxStatistics/CscCdrFilters.vue index 54e65335..fb87d43b 100644 --- a/src/components/pages/PbxStatistics/CscCdrFilters.vue +++ b/src/components/pages/PbxStatistics/CscCdrFilters.vue @@ -150,7 +150,8 @@