diff --git a/.env.example b/.env.example index 682bbaf6..d2e78b5e 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,7 @@ VUE_APP_I18N_FALLBACK_LOCALE=en VUE_APP_CACHE_MAX_AGE=3600 VUE_APP_VIEW_SIZE=10 VUE_APP_DATE_FORMAT=MM/dd/yyyy -VUE_APP_PERMISSION_ID= +VUE_APP_PERMISSION_ID="IMPORT_APP_VIEW" VUE_APP_ALIAS={} VUE_APP_MAPPING_TYPES={"PO": "PO_MAPPING_PREF","RSTINV": "INV_MAPPING_PREF"} VUE_APP_MAPPING_PO={"orderId": { "label": "Order ID", "required": true }, "productSku": { "label": "Shopify product SKU", "required": true },"orderDate": { "label": "Arrival date", "required": true }, "quantity": { "label": "Ordered quantity", "required": true }, "facility": { "label": "Facility ID", "required": true }} diff --git a/package-lock.json b/package-lock.json index caf88bd0..6443e1c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@capacitor/android": "^2.4.7", "@capacitor/core": "^2.4.7", + "@casl/ability": "^6.0.0", "@hotwax/app-version-info": "^1.0.0", "@hotwax/apps-theme": "^1.1.0", "@hotwax/dxp-components": "^1.12.1", @@ -19,6 +20,7 @@ "@ionic/vue-router": "6.7.5", "@types/file-saver": "^2.0.4", "@types/papaparse": "^5.3.1", + "boon-js": "^2.0.3", "core-js": "^3.6.5", "file-saver": "^2.0.5", "luxon": "^3.2.0", @@ -1926,6 +1928,17 @@ "tslib": "^1.9.0" } }, + "node_modules/@casl/ability": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.5.0.tgz", + "integrity": "sha512-3guc94ugr5ylZQIpJTLz0CDfwNi0mxKVECj1vJUPAvs+Lwunh/dcuUjwzc4MHM9D8JOYX0XUZMEPedpB3vIbOw==", + "dependencies": { + "@ucast/mongo2js": "^1.3.0" + }, + "funding": { + "url": "https://github.com/stalniy/casl/blob/master/BACKERS.md" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -4177,6 +4190,37 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ucast/core": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz", + "integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==" + }, + "node_modules/@ucast/js": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.3.tgz", + "integrity": "sha512-jBBqt57T5WagkAjqfCIIE5UYVdaXYgGkOFYv2+kjq2AVpZ2RIbwCo/TujJpDlwTVluUI+WpnRpoGU2tSGlEvFQ==", + "dependencies": { + "@ucast/core": "^1.0.0" + } + }, + "node_modules/@ucast/mongo": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz", + "integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==", + "dependencies": { + "@ucast/core": "^1.4.1" + } + }, + "node_modules/@ucast/mongo2js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.4.tgz", + "integrity": "sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==", + "dependencies": { + "@ucast/core": "^1.6.1", + "@ucast/js": "^3.0.0", + "@ucast/mongo": "^2.4.0" + } + }, "node_modules/@vue/babel-helper-vue-jsx-merge-props": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz", @@ -5975,6 +6019,11 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true }, + "node_modules/boon-js": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/boon-js/-/boon-js-2.0.5.tgz", + "integrity": "sha512-Ck9YXckVbbIjpZPxtKXJ5TcT+ptU4E9lJy2X6hBKsR2nZqg026eHf9aw3KGXoFbfO4coRLaW9ql0tWrBrqJt1A==" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", diff --git a/package.json b/package.json index 8d3c09db..013dd71e 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dependencies": { "@capacitor/android": "^2.4.7", "@capacitor/core": "^2.4.7", + "@casl/ability": "^6.0.0", "@hotwax/app-version-info": "^1.0.0", "@hotwax/apps-theme": "^1.1.0", "@hotwax/dxp-components": "^1.12.1", @@ -21,6 +22,7 @@ "@ionic/core": "6.7.5", "@ionic/vue": "6.7.5", "@ionic/vue-router": "6.7.5", + "boon-js": "^2.0.3", "@types/file-saver": "^2.0.4", "@types/papaparse": "^5.3.1", "core-js": "^3.6.5", diff --git a/src/adapter/index.ts b/src/adapter/index.ts index 696d783c..d1553573 100644 --- a/src/adapter/index.ts +++ b/src/adapter/index.ts @@ -2,6 +2,7 @@ import { api, client, getConfig, + hasError, fetchProducts, initialise, logout, @@ -16,6 +17,7 @@ export { api, client, getConfig, + hasError, fetchProducts, initialise, logout, diff --git a/src/authorization/Actions.ts b/src/authorization/Actions.ts new file mode 100644 index 00000000..29b1ecfc --- /dev/null +++ b/src/authorization/Actions.ts @@ -0,0 +1,2 @@ +export default { +} \ No newline at end of file diff --git a/src/authorization/Rules.ts b/src/authorization/Rules.ts new file mode 100644 index 00000000..39169013 --- /dev/null +++ b/src/authorization/Rules.ts @@ -0,0 +1,4 @@ +export default { + "APP_INVENTORY_VIEW": "MDM_IMP_INVENTORY_VIEW", + "IMPORT_APP_VIEW": "IMPORT_APP_VIEW" +} as any \ No newline at end of file diff --git a/src/authorization/index.ts b/src/authorization/index.ts new file mode 100644 index 00000000..42edc1b3 --- /dev/null +++ b/src/authorization/index.ts @@ -0,0 +1,124 @@ +import { AbilityBuilder, PureAbility } from '@casl/ability'; +import { getEvaluator, parse } from 'boon-js'; +import { Tokens } from 'boon-js/lib/types' + +// TODO Improve this +// We will move this code to an external plugin and use below Actions and Rules accordlingly +let Actions = {} as any; +let Rules = {} as any; + +// We are using CASL library to define permissions. +// Instead of using Action-Subject based authorisation we are going with Claim based Authorization. +// We would be defining the permissions for each action and case, map with server permissiosn based upon certain rules. +// https://casl.js.org/v5/en/cookbook/claim-authorization +// Following the comment of Sergii Stotskyi, author of CASL +// https://github.com/stalniy/casl/issues/525 +// We are defining a PureAbility and creating an instance with AbilityBuilder. +type ClaimBasedAbility = PureAbility; +const { build } = new AbilityBuilder(PureAbility); +const ability = build(); + +/** + * The method returns list of permissions required for the rules. We are having set of rules, + * through which app permissions are defined based upon the server permissions. + * When getting server permissions, as all the permissions are not be required. + * Specific permissions used defining the rules are extracted and sent to server. + * @returns permissions + */ +const getServerPermissionsFromRules = () => { + // Iterate for each rule + const permissions = Object.keys(Rules).reduce((permissions: any, rule: any) => { + const permissionRule = Rules[rule]; + // some rules may be empty, no permission is required from server + if (permissionRule) { + // Each rule may have multiple permissions along with operators + // Boon js parse rules into tokens, each token may be operator or server permission + // permissionId will have token name as identifier. + const permissionTokens = parse(permissionRule); + permissions = permissionTokens.reduce((permissions: any, permissionToken: any) => { + // Token object with name as identifier has permissionId + if (Tokens.IDENTIFIER === permissionToken.name) { + permissions.add(permissionToken.value); + } + return permissions; + }, permissions) + } + return permissions; + }, new Set()) + return [...permissions]; +} + +/** + * The method is used to prepare app permissions from the server permissions. + * Rules could be defined such that each app permission could be defined based upon certain one or more server permissions. + * @param serverPermissions + * @returns appPermissions + */ +const prepareAppPermissions = (serverPermissions: any) => { + const serverPermissionsInput = serverPermissions.reduce((serverPermissionsInput: any, permission: any) => { + serverPermissionsInput[permission] = true; + return serverPermissionsInput; + }, {}) + // Boonjs evaluator needs server permissions as object with permissionId and boolean value + // Each rule is passed to evaluator along with the server permissions + // if the server permissions and rule matches, app permission is added to list + const permissions = Object.keys(Rules).reduce((permissions: any, rule: any) => { + const permissionRule = Rules[rule]; + // If for any app permission, we have empty rule we user is assigned the permission + // If rule is not defined, the app permisions is still evaluated or provided to all the users. + if (!permissionRule || (permissionRule && getEvaluator(permissionRule)(serverPermissionsInput))) { + permissions.push(rule); + } + return permissions; + }, []) + const { can, rules } = new AbilityBuilder(PureAbility); + permissions.map((permission: any) => { + can(permission); + }) + return rules; +} + +/** + * + * Sets the current app permissions. This should be used after perparing the app permissions from the server permissions + * @param permissions + * @returns + */ +const setPermissions = (permissions: any) => { + // If the user has passed undefined or null, it should not break the code + if (!permissions) permissions = []; + ability.update(permissions) + return true; +}; + +/** + * Resets the permissions list. Used for cases like logout + */ +const resetPermissions = () => setPermissions([]); + +/** + * + * @param permission + * @returns + */ +const hasPermission = (permission: string) => ability.can(permission); + +export { Actions, getServerPermissionsFromRules, hasPermission, prepareAppPermissions, resetPermissions, setPermissions}; + +// TODO Move this code to an external plugin, to be used across the apps +export default { + install(app: any, options: any) { + + // Rules and Actions could be app and OMS package specific + Rules = options.rules; + Actions = options.actions; + + // TODO Check why global properties is not working and apply across. + app.config.globalProperties.$permission = this; + }, + getServerPermissionsFromRules, + hasPermission, + prepareAppPermissions, + resetPermissions, + setPermissions +} diff --git a/src/locales/en.json b/src/locales/en.json index 1375d68c..974bf387 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -173,5 +173,6 @@ "Version: ": "Version: {appVersion}", "View all date time formats supported by the HotWax Import app.": "View all date time formats supported by the HotWax Import app.", "View": "View", - "Would you like to update your time zone to . Your profile is currently set to . This setting can always be changed from the settings menu.": "Would you like to update your time zone to {localTimeZone}. Your profile is currently set to {profileTimeZone}. This setting can always be changed from the settings menu." + "Would you like to update your time zone to . Your profile is currently set to . This setting can always be changed from the settings menu.": "Would you like to update your time zone to {localTimeZone}. Your profile is currently set to {profileTimeZone}. This setting can always be changed from the settings menu.", + "You do not have permission to access this page": "You do not have permission to access this page" } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 99519658..2b40d8f4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -27,7 +27,9 @@ import '@hotwax/apps-theme'; import i18n from './i18n' import store from './store' import { DateTime } from 'luxon'; - +import permissionPlugin from '@/authorization'; +import permissionRules from '@/authorization/Rules'; +import permissionActions from '@/authorization/Actions'; import logger from './logger'; import { dxpComponents } from '@hotwax/dxp-components' import { login, logout, loader } from './user-utils'; @@ -44,6 +46,10 @@ const app = createApp(App) .use(router) .use(i18n) .use(store) + .use(permissionPlugin, { + rules: permissionRules, + actions: permissionActions + }) .use(dxpComponents, { defaultImgUrl: require("@/assets/images/defaultImage.png"), login, diff --git a/src/router/index.ts b/src/router/index.ts index 3bed6d96..ad4bc2ad 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -8,8 +8,17 @@ import SavedMappings from '@/views/SavedMappings.vue' import Settings from "@/views/Settings.vue" import store from '@/store' import MappingDetail from '@/views/MappingDetail.vue' -import { DxpLogin, useAuthStore } from '@hotwax/dxp-components'; +import { DxpLogin, translate, useAuthStore } from '@hotwax/dxp-components'; import { loader } from '@/user-utils'; +import { showToast } from '@/utils'; +import { hasPermission } from '@/authorization'; + +// Defining types for the meta values +declare module 'vue-router' { + interface RouteMeta { + permissionId?: string; + } +} const authGuard = async (to: any, from: any, next: any) => { const authStore = useAuthStore() @@ -52,7 +61,10 @@ const routes: Array = [ path: '/inventory', name: 'Inventory', component: Inventory, - beforeEnter: authGuard + beforeEnter: authGuard, + meta: { + permissionId: "APP_INVENTORY_VIEW" + } }, { path: '/inventory-review', @@ -91,4 +103,19 @@ const router = createRouter({ routes }) +router.beforeEach((to, from) => { + if (to.meta.permissionId && !hasPermission(to.meta.permissionId)) { + let redirectToPath = from.path; + // If the user has navigated from Login page or if it is page load, redirect user to settings page without showing any toast + if (redirectToPath == "/login" || redirectToPath == "/") redirectToPath = "/settings"; + else { + showToast(translate('You do not have permission to access this page')); + } + return { + path: redirectToPath, + } + } +}) + + export default router \ No newline at end of file diff --git a/src/services/UserService.ts b/src/services/UserService.ts index 97f2779b..5e740239 100644 --- a/src/services/UserService.ts +++ b/src/services/UserService.ts @@ -1,5 +1,6 @@ import { api, client } from '@/adapter' import store from '@/store'; +import { hasError } from '@/adapter'; const login = async (username: string, password: string): Promise => { return api({ @@ -12,17 +13,6 @@ const login = async (username: string, password: string): Promise => { }); } -const checkPermission = async (payload: any): Promise => { - let baseURL = store.getters['user/getInstanceUrl']; - baseURL = baseURL && baseURL.startsWith('http') ? (baseURL.includes('/api') ? baseURL : `${baseURL}/api/`) : `https://${baseURL}.hotwax.io/api/`; - return client({ - url: "checkPermission", - method: "post", - baseURL: baseURL, - ...payload - }); -} - const getProfile = async (): Promise => { return api({ url: "user-profile", @@ -76,6 +66,95 @@ const getFieldMappings = async (payload: any): Promise => { }); } +const getUserPermissions = async (payload: any, token: any): Promise => { + const baseURL = store.getters['user/getBaseUrl']; + let serverPermissions = [] as any; + + // If the server specific permission list doesn't exist, getting server permissions will be of no use + // It means there are no rules yet depending upon the server permissions. + if (payload.permissionIds && payload.permissionIds.length == 0) return serverPermissions; + // TODO pass specific permissionIds + let resp; + // TODO Make it configurable from the environment variables. + // Though this might not be an server specific configuration, + // we will be adding it to environment variable for easy configuration at app level + const viewSize = 200; + + try { + const params = { + "viewIndex": 0, + viewSize, + permissionIds: payload.permissionIds + } + resp = await client({ + url: "getPermissions", + method: "post", + baseURL, + data: params, + headers: { + Authorization: 'Bearer ' + token, + 'Content-Type': 'application/json' + } + }) + if (resp.status === 200 && resp.data.docs?.length && !hasError(resp)) { + serverPermissions = resp.data.docs.map((permission: any) => permission.permissionId); + const total = resp.data.count; + const remainingPermissions = total - serverPermissions.length; + if (remainingPermissions > 0) { + // We need to get all the remaining permissions + const apiCallsNeeded = Math.floor(remainingPermissions / viewSize) + (remainingPermissions % viewSize != 0 ? 1 : 0); + const responses = await Promise.all([...Array(apiCallsNeeded).keys()].map(async (index: any) => { + const response = await client({ + url: "getPermissions", + method: "post", + baseURL, + data: { + "viewIndex": index + 1, + viewSize, + permissionIds: payload.permissionIds + }, + headers: { + Authorization: 'Bearer ' + token, + 'Content-Type': 'application/json' + } + }) + if (!hasError(response)) { + return Promise.resolve(response); + } else { + return Promise.reject(response); + } + })) + const permissionResponses = { + success: [], + failed: [] + } + responses.reduce((permissionResponses: any, permissionResponse: any) => { + if (permissionResponse.status !== 200 || hasError(permissionResponse) || !permissionResponse.data?.docs) { + permissionResponses.failed.push(permissionResponse); + } else { + permissionResponses.success.push(permissionResponse); + } + return permissionResponses; + }, permissionResponses) + + serverPermissions = permissionResponses.success.reduce((serverPermissions: any, response: any) => { + serverPermissions.push(...response.data.docs.map((permission: any) => permission.permissionId)); + return serverPermissions; + }, serverPermissions) + + // If partial permissions are received and we still allow user to login, some of the functionality might not work related to the permissions missed. + // Show toast to user intimiting about the failure + // Allow user to login + // TODO Implement Retry or improve experience with show in progress icon and allowing login only if all the data related to user profile is fetched. + if (permissionResponses.failed.length > 0) Promise.reject("Something went wrong while getting complete user permissions."); + } + } + return serverPermissions; + } catch (error: any) { + return Promise.reject(error); + } +} + export const UserService = { createFieldMapping, deleteFieldMapping, @@ -83,7 +162,7 @@ export const UserService = { getAvailableTimeZones, getFieldMappings, getProfile, + getUserPermissions, setUserTimeZone, - checkPermission, updateFieldMapping } \ No newline at end of file diff --git a/src/store/index.ts b/src/store/index.ts index 0f60a0a0..b05f7e7d 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -9,6 +9,7 @@ import productModule from "./modules/product"; import orderModule from "./modules/order"; import stockModule from "./modules/stock"; import utilModule from "./modules/util" +import { setPermissions } from "@/authorization" // TODO check how to register it from the components only @@ -41,6 +42,8 @@ const store = createStore({ }, }) +setPermissions(store.getters['user/getUserPermissions']); + export default store export function useStore(): typeof store { return useVuexStore() diff --git a/src/store/modules/user/UserState.ts b/src/store/modules/user/UserState.ts index 41166a5c..275b1568 100644 --- a/src/store/modules/user/UserState.ts +++ b/src/store/modules/user/UserState.ts @@ -12,4 +12,5 @@ export default interface UserState { name: string; value: object; }; + permissions: any; } \ No newline at end of file diff --git a/src/store/modules/user/actions.ts b/src/store/modules/user/actions.ts index 494f7716..375bb0a7 100644 --- a/src/store/modules/user/actions.ts +++ b/src/store/modules/user/actions.ts @@ -9,6 +9,12 @@ import { logout, updateInstanceUrl, updateToken, resetConfig } from '@/adapter' import logger from "@/logger"; import { useAuthStore } from '@hotwax/dxp-components'; import emitter from '@/event-bus' +import { + getServerPermissionsFromRules, + prepareAppPermissions, + resetPermissions, + setPermissions +} from '@/authorization' const actions: ActionTree = { @@ -19,37 +25,44 @@ const actions: ActionTree = { try { const { token, oms } = payload; dispatch("setUserInstanceUrl", oms); - - if (token) { + + if(token) { const permissionId = process.env.VUE_APP_PERMISSION_ID; + + // Prepare permissions list + const serverPermissionsFromRules = getServerPermissionsFromRules(); + if (permissionId) serverPermissionsFromRules.push(permissionId); + + const serverPermissions = await UserService.getUserPermissions({ + permissionIds: [...new Set(serverPermissionsFromRules)] + }, token); + + const appPermissions = prepareAppPermissions(serverPermissions); + + // Checking if the user has permission to access the app + // If there is no configuration, the permission check is not enabled if (permissionId) { - const checkPermissionResponse = await UserService.checkPermission({ - data: { - permissionId - }, - headers: { - Authorization: 'Bearer ' + token, - 'Content-Type': 'application/json' - } - }); - - if (checkPermissionResponse.status === 200 && !hasError(checkPermissionResponse) && checkPermissionResponse.data && checkPermissionResponse.data.hasPermission) { - commit(types.USER_TOKEN_CHANGED, { newToken: token }) - updateToken(token) - await dispatch('getProfile') - dispatch('setPreferredDateTimeFormat', process.env.VUE_APP_DATE_FORMAT ? process.env.VUE_APP_DATE_FORMAT : 'MM/dd/yyyy'); - } else { + // As the token is not yet set in the state passing token headers explicitly + // TODO Abstract this out, how token is handled should be part of the method not the callee + const hasPermission = appPermissions.some((appPermission: any) => appPermission.action === permissionId); + // If there are any errors or permission check fails do not allow user to login + if (!hasPermission) { const permissionError = 'You do not have permission to access the app.'; showToast(translate(permissionError)); logger.error("error", permissionError); return Promise.reject(new Error(permissionError)); } - } else { - commit(types.USER_TOKEN_CHANGED, { newToken: token }) - updateToken(token) - await dispatch('getProfile') - dispatch('setPreferredDateTimeFormat', process.env.VUE_APP_DATE_FORMAT ? process.env.VUE_APP_DATE_FORMAT : 'MM/dd/yyyy'); } + + updateToken(token) + setPermissions(appPermissions); + + // TODO user single mutation + commit(types.USER_PERMISSIONS_UPDATED, appPermissions); + commit(types.USER_TOKEN_CHANGED, { newToken: token }) + + await dispatch('getProfile') + dispatch('setPreferredDateTimeFormat', process.env.VUE_APP_DATE_FORMAT ? process.env.VUE_APP_DATE_FORMAT : 'MM/dd/yyyy'); } } catch (err: any) { showToast(translate('Something went wrong')); @@ -92,6 +105,7 @@ const actions: ActionTree = { // TODO add any other tasks if need commit(types.USER_END_SESSION) + resetPermissions(); resetConfig(); this.dispatch('order/updatePurchaseOrders', {parsed: {}, original: {}, unidentifiedItems: []}); this.dispatch('util/clearFacilities'); diff --git a/src/store/modules/user/getters.ts b/src/store/modules/user/getters.ts index 25059c47..88b13241 100644 --- a/src/store/modules/user/getters.ts +++ b/src/store/modules/user/getters.ts @@ -9,6 +9,11 @@ const getters: GetterTree = { isUserAuthenticated(state) { return state.token && state.current }, + getBaseUrl(state) { + let baseURL = process.env.VUE_APP_BASE_URL; + if (!baseURL) baseURL = state.instanceUrl; + return baseURL.startsWith('http') ? baseURL.includes('/api') ? baseURL : `${baseURL}/api/` : `https://${baseURL}.hotwax.io/api/`; + }, getUserToken (state) { return state.token }, @@ -37,6 +42,9 @@ const getters: GetterTree = { }, getCurrentMapping(state) { return JSON.parse(JSON.stringify(state.currentMapping)) - } + }, + getUserPermissions(state) { + return state.permissions; + }, } export default getters; \ No newline at end of file diff --git a/src/store/modules/user/index.ts b/src/store/modules/user/index.ts index c60b216f..8cd2d3e3 100644 --- a/src/store/modules/user/index.ts +++ b/src/store/modules/user/index.ts @@ -23,7 +23,8 @@ const userModule: Module = { mappingType: '', name: '', value: {} - } + }, + permissions: [], }, getters, actions, diff --git a/src/store/modules/user/mutation-types.ts b/src/store/modules/user/mutation-types.ts index 40af55b0..817fa913 100644 --- a/src/store/modules/user/mutation-types.ts +++ b/src/store/modules/user/mutation-types.ts @@ -9,3 +9,4 @@ export const USER_DATETIME_FORMAT_UPDATED = SN_USER + '/DATETIME_FORMAT_UPDATED' export const USER_CURRENT_FIELD_MAPPING_UPDATED = SN_USER + '/_CURRENT_FIELD_MAPPING_UPDATED' export const USER_FIELD_MAPPINGS_UPDATED = SN_USER + '/FIELD_MAPPINGS_UPDATED' export const USER_FIELD_MAPPING_CREATED = SN_USER + '/FIELD_MAPPING_CREATED' +export const USER_PERMISSIONS_UPDATED = SN_USER + '/PERMISSIONS_UPDATED' diff --git a/src/store/modules/user/mutations.ts b/src/store/modules/user/mutations.ts index f5839e29..a6ba5803 100644 --- a/src/store/modules/user/mutations.ts +++ b/src/store/modules/user/mutations.ts @@ -38,6 +38,9 @@ const mutations: MutationTree = { name: payload.name, value: payload.value }; - } + }, + [types.USER_PERMISSIONS_UPDATED](state, payload) { + state.permissions = payload + }, } export default mutations; \ No newline at end of file