From 54b793d532d28fa565d322b99e3ecfb26bd5b022 Mon Sep 17 00:00:00 2001 From: amansinghbais Date: Thu, 18 Jan 2024 10:07:19 +0530 Subject: [PATCH 1/5] Implemented: authorization for permission support in app (#244) --- package-lock.json | 49 +++++++++ package.json | 2 + src/adapter/index.ts | 2 + src/authorization/Actions.ts | 2 + src/authorization/Rules.ts | 4 + src/authorization/index.ts | 124 +++++++++++++++++++++++ src/main.ts | 8 +- src/router/index.ts | 34 ++++++- src/services/UserService.ts | 91 +++++++++++++++++ src/store/index.ts | 3 + src/store/modules/user/UserState.ts | 1 + src/store/modules/user/actions.ts | 101 ++++++++++++------ src/store/modules/user/getters.ts | 10 +- src/store/modules/user/index.ts | 3 +- src/store/modules/user/mutation-types.ts | 1 + src/store/modules/user/mutations.ts | 5 +- 16 files changed, 405 insertions(+), 35 deletions(-) create mode 100644 src/authorization/Actions.ts create mode 100644 src/authorization/Rules.ts create mode 100644 src/authorization/index.ts diff --git a/package-lock.json b/package-lock.json index 73c18fca..f4644fa6 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.11.0", @@ -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", @@ -4162,6 +4175,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", @@ -5980,6 +6024,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 dfdac66b..291b27f1 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.11.0", @@ -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/main.ts b/src/main.ts index 3a0eae26..066060cc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -28,7 +28,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..47b122f4 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -8,8 +8,18 @@ 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 +62,10 @@ const routes: Array = [ path: '/inventory', name: 'Inventory', component: Inventory, - beforeEnter: authGuard + beforeEnter: authGuard, + meta: { + permissionId: "APP_INVENTORY_VIEW" + } }, { path: '/inventory-review', @@ -91,4 +104,21 @@ const router = createRouter({ routes }) +router.beforeEach((to, from) => { + + if (to.meta.permissionId && !hasPermission(to.meta.permissionId)) { + console.log('h') + 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 b5388834..809ccd1a 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({ @@ -76,6 +77,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,6 +173,7 @@ export const UserService = { getAvailableTimeZones, getFieldMappings, getProfile, + getUserPermissions, setUserTimeZone, checkPermission, updateFieldMapping 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 4a2cc6b6..697a78df 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,38 +25,75 @@ const actions: ActionTree = { try { const { token, oms } = payload; dispatch("setUserInstanceUrl", oms); + + 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: serverPermissionsFromRules + }, token); - if (token) { - const permissionId = process.env.VUE_APP_PERMISSION_ID; - 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 { - 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'); + 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) { + // 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)); } } + + updateToken(token) + + // 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'); + + // if (token) { + // const permissionId = process.env.VUE_APP_PERMISSION_ID; + // 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 { + // 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'); + // } + // } } catch (err: any) { showToast(translate('Something went wrong')); logger.error("error", err); diff --git a/src/store/modules/user/getters.ts b/src/store/modules/user/getters.ts index 25059c47..565ed33c 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 : `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 From 250b44c2e228624caab5fb736869a26a627407e9 Mon Sep 17 00:00:00 2001 From: amansinghbais Date: Thu, 18 Jan 2024 11:37:27 +0530 Subject: [PATCH 2/5] Fixed: permission set and reset inconsistency on login and logout (#244) --- src/App.vue | 3 ++- src/router/index.ts | 3 --- src/store/modules/user/actions.ts | 3 ++- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/App.vue b/src/App.vue index 8c0da02f..485ec68a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -132,7 +132,8 @@ export default defineComponent({ computed: { ...mapGetters({ userToken: 'user/getUserToken', - instanceUrl: 'user/getInstanceUrl' + instanceUrl: 'user/getInstanceUrl', + permissions: 'user/getUserPermissions' }) }, setup(){ diff --git a/src/router/index.ts b/src/router/index.ts index 47b122f4..ad4bc2ad 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -20,7 +20,6 @@ declare module 'vue-router' { } } - const authGuard = async (to: any, from: any, next: any) => { const authStore = useAuthStore() if (!authStore.isAuthenticated || !store.getters['user/isAuthenticated']) { @@ -105,9 +104,7 @@ const router = createRouter({ }) router.beforeEach((to, from) => { - if (to.meta.permissionId && !hasPermission(to.meta.permissionId)) { - console.log('h') 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"; diff --git a/src/store/modules/user/actions.ts b/src/store/modules/user/actions.ts index 697a78df..5cea9c11 100644 --- a/src/store/modules/user/actions.ts +++ b/src/store/modules/user/actions.ts @@ -54,12 +54,12 @@ const actions: ActionTree = { } 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'); @@ -135,6 +135,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'); From 44588ed5e868890536e7d08d89a656caef832505 Mon Sep 17 00:00:00 2001 From: amansinghbais Date: Thu, 18 Jan 2024 12:02:21 +0530 Subject: [PATCH 3/5] Improved: syntax, removed unused code, and fixed permissionId duplication in api payload (#244) --- src/App.vue | 3 +- src/locales/en.json | 3 +- src/services/UserService.ts | 12 ---- src/store/modules/user/actions.ts | 102 +++++++++++------------------- 4 files changed, 39 insertions(+), 81 deletions(-) diff --git a/src/App.vue b/src/App.vue index 485ec68a..8c0da02f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -132,8 +132,7 @@ export default defineComponent({ computed: { ...mapGetters({ userToken: 'user/getUserToken', - instanceUrl: 'user/getInstanceUrl', - permissions: 'user/getUserPermissions' + instanceUrl: 'user/getInstanceUrl' }) }, setup(){ diff --git a/src/locales/en.json b/src/locales/en.json index b7222b36..2ab03196 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -170,5 +170,6 @@ "Username": "Username", "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/services/UserService.ts b/src/services/UserService.ts index 809ccd1a..5e740239 100644 --- a/src/services/UserService.ts +++ b/src/services/UserService.ts @@ -13,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 : `https://${baseURL}.hotwax.io/api/`; - return client({ - url: "checkPermission", - method: "post", - baseURL: baseURL, - ...payload - }); -} - const getProfile = async (): Promise => { return api({ url: "user-profile", @@ -175,6 +164,5 @@ export const UserService = { getProfile, getUserPermissions, setUserTimeZone, - checkPermission, updateFieldMapping } \ No newline at end of file diff --git a/src/store/modules/user/actions.ts b/src/store/modules/user/actions.ts index 5cea9c11..4074efda 100644 --- a/src/store/modules/user/actions.ts +++ b/src/store/modules/user/actions.ts @@ -26,74 +26,44 @@ const actions: ActionTree = { const { token, oms } = payload; dispatch("setUserInstanceUrl", oms); - 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: 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) { - // 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)); + 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) { + // 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)); + } } + + 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'); } - - 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'); - - // if (token) { - // const permissionId = process.env.VUE_APP_PERMISSION_ID; - // 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 { - // 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'); - // } - // } } catch (err: any) { showToast(translate('Something went wrong')); logger.error("error", err); From 9a553d478c08316a2d88cd18b5dc5e285802702d Mon Sep 17 00:00:00 2001 From: amansinghbais Date: Thu, 18 Jan 2024 12:46:14 +0530 Subject: [PATCH 4/5] Improved: added base permission in env file (#244) --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 }} From 5c261b2c8e4e765c716fc51f05251dc646dc7dc0 Mon Sep 17 00:00:00 2001 From: amansinghbais Date: Mon, 18 Mar 2024 12:27:04 +0530 Subject: [PATCH 5/5] Improved: getter for baseUrl to handle case with api with /api and removed unused service (#244) --- src/services/UserService.ts | 11 ----------- src/store/modules/user/getters.ts | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/services/UserService.ts b/src/services/UserService.ts index 2e4df071..5e740239 100644 --- a/src/services/UserService.ts +++ b/src/services/UserService.ts @@ -13,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", diff --git a/src/store/modules/user/getters.ts b/src/store/modules/user/getters.ts index 565ed33c..88b13241 100644 --- a/src/store/modules/user/getters.ts +++ b/src/store/modules/user/getters.ts @@ -12,7 +12,7 @@ const getters: GetterTree = { getBaseUrl(state) { let baseURL = process.env.VUE_APP_BASE_URL; if (!baseURL) baseURL = state.instanceUrl; - return baseURL.startsWith('http') ? baseURL : `https://${baseURL}.hotwax.io/api/`; + return baseURL.startsWith('http') ? baseURL.includes('/api') ? baseURL : `${baseURL}/api/` : `https://${baseURL}.hotwax.io/api/`; }, getUserToken (state) { return state.token