Skip to content

Commit

Permalink
Implemented: support for permissions in the app and added suppot to m…
Browse files Browse the repository at this point in the history
…ake api calls on redirection oms(#323)
  • Loading branch information
R-Sourabh committed Oct 9, 2024
1 parent cb24562 commit 810b490
Show file tree
Hide file tree
Showing 15 changed files with 298 additions and 11 deletions.
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ VUE_APP_I18N_LOCALE=en
VUE_APP_I18N_FALLBACK_LOCALE=en
VUE_APP_CACHE_MAX_AGE=3600
VUE_APP_VIEW_SIZE=10
VUE_APP_PERMISSION_ID=
VUE_APP_PERMISSION_ID="ORDER_ROUTING_APP_VIEW"
VUE_APP_DEFAULT_LOG_LEVEL="error"
VUE_APP_RULE_ENUMS={"QUEUE":{"id":"OIP_QUEUE","code":"facilityId"},"SHIPPING_METHOD":{"id":"OIP_SHIP_METH_TYPE","code":"shipmentMethodTypeId"},"PRIORITY":{"id":"OIP_PRIORITY","code":"priority"},"PROMISE_DATE":{"id":"OIP_PROMISE_DATE","code":"promiseDaysCutoff"},"SALES_CHANNEL":{"id":"OIP_SALES_CHANNEL","code":"salesChannelEnumId"},"ORIGIN_FACILITY_GROUP":{"id":"OIP_ORIGIN_FAC_GRP","code":"originFacilityGroupId"},"SHIP_BY":{"id":"OSP_SHIP_BY","code":"shipBeforeDate"},"SHIP_AFTER":{"id":"OSP_SHIP_AFTER","code":"shipAfterDate"},"ORDER_DATE":{"id":"OSP_ORDER_DATE","code":"orderDate"},"SHIPPING_METHOD_SORT":{"id":"OSP_SHIP_METH","code":"deliveryDays"},"SORT_PRIORITY":{"id":"OSP_PRIORITY","code":"priority"}}
VUE_APP_RULE_FILTER_ENUMS={"FACILITY_GROUP":{"id":"IIP_FACILITY_GROUP","code":"facilityGroupId"},"PROXIMITY":{"id":"IIP_PROXIMITY","code":"distance"},"BRK_SAFETY_STOCK":{"id":"IIP_BRK_SFTY_STOCK","code":"brokeringSafetyStock"},"MEASUREMENT_SYSTEM":{"id":"IIP_MSMNT_SYSTEM","code":"measurementSystem"}, "SPLIT_ITEM_GROUP":{"id":"IIP_SPLIT_ITEM_GROUP","code":"splitOrderItemGroup"}, "FACILITY_ORDER_LIMIT": {"id":"IFP_IGNORE_ORD_FAC_LIMIT", "code":"ignoreFacilityOrderLimit"}}
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.2.6",
"@hotwax/dxp-components": "^1.14.0",
Expand All @@ -23,6 +24,7 @@
"@ionic/vue-router": "^7.6.0",
"axios": "^0.21.1",
"axios-cache-adapter": "^2.7.3",
"boon-js": "^2.0.3",
"core-js": "^3.6.5",
"cron-parser": "^4.9.0",
"cronstrue": "^2.50.0",
Expand Down
19 changes: 17 additions & 2 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@ import store from "@/store";
import { StatusCodes } from "http-status-codes";
import router from "@/router"

Check warning on line 7 in src/api/index.ts

View workflow job for this annotation

GitHub Actions / call-workflow-in-another-repo / reusable_workflow_job (18.x)

'router' is defined but never used

Check warning on line 7 in src/api/index.ts

View workflow job for this annotation

GitHub Actions / call-workflow-in-another-repo / reusable_workflow_job (20.x)

'router' is defined but never used

let apiConfig = {} as any

axios.interceptors.request.use((config: any) => {
// TODO: pass csrf token
const token = store.getters["user/getUserToken"];
if (token) {
if (token && !apiConfig.useOmsRedirection) {
config.headers["api_key"] = token;
config.headers["Content-Type"] = "application/json";
}
const omsRedirectionInfo = store.getters["user/getOmsRedirectionInfo"]
if (apiConfig.useOmsRedirection && omsRedirectionInfo.token) {
config.headers["Authorization"] = `Bearer ${omsRedirectionInfo.token}`;
config.headers["Content-Type"] = "application/json";
}

return config;
});
Expand Down Expand Up @@ -78,6 +85,7 @@ const axiosCache = setupCache({
* @return {Promise} Response from API as returned by Axios
*/
const api = async (customConfig: any) => {
apiConfig = customConfig
// Prepare configuration
const config: any = {
url: customConfig.url,
Expand All @@ -88,7 +96,14 @@ const api = async (customConfig: any) => {
}

const baseURL = store.getters["user/getInstanceUrl"];
if (baseURL) config.baseURL = baseURL.startsWith('http') ? baseURL.includes('/rest/s1/order-routing') ? baseURL : `${baseURL}/rest/s1/order-routing/` : `https://${baseURL}.hotwax.io/rest/s1/order-routing/`;
const omsRedirectionInfo = store.getters["user/getOmsRedirectionInfo"]

if(customConfig.useOmsRedirection) {
config.baseURL = omsRedirectionInfo.url.startsWith('http') ? omsRedirectionInfo.url.includes('/api') ? omsRedirectionInfo.url : `${omsRedirectionInfo.url}/api/` : `https://${omsRedirectionInfo.url}.hotwax.io/api/`;
} else if (baseURL) {
config.baseURL = baseURL.startsWith('http') ? baseURL.includes('/rest/s1/order-routing') ? baseURL : `${baseURL}/rest/s1/order-routing/` : `https://${baseURL}.hotwax.io/rest/s1/order-routing/`;
}

if(customConfig.cache) config.adapter = axiosCache.adapter;
const networkStatus = await OfflineHelper.getNetworkStatus();
if (customConfig.queue && !networkStatus.connected) {
Expand Down
3 changes: 3 additions & 0 deletions src/authorization/Actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
"ORDER_ROUTING_APP_VIEW": "ORDER_ROUTING_APP_VIEW"
}
3 changes: 3 additions & 0 deletions src/authorization/Rules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
"ORDER_ROUTING_APP_VIEW": "ORDER_ROUTING_APP_VIEW"
}
124 changes: 124 additions & 0 deletions src/authorization/index.ts
Original file line number Diff line number Diff line change
@@ -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<string>;
const { build } = new AbilityBuilder<ClaimBasedAbility>(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<ClaimBasedAbility>(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
}
7 changes: 7 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ import i18n from "./i18n"
import store from "./store"
import { DateTime } from "luxon";
import logger from './logger';
import permissionPlugin from '@/authorization';
import permissionRules from '@/authorization/Rules';
import permissionActions from '@/authorization/Actions';

const app = createApp(App)
.use(IonicVue, {
Expand All @@ -44,6 +47,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,
Expand Down
92 changes: 92 additions & 0 deletions src/services/UserService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,97 @@ const login = async (token: string): Promise <any> => {
return Promise.resolve(api_key)
}

const getUserPermissions = async (payload: any, url: string, token: any): Promise<any> => {
// Currently, making this request in ofbiz
const omsRedirectionInfo = store.getters["user/getOmsRedirectionInfo"]
const baseURL = omsRedirectionInfo.url.startsWith('http') ? omsRedirectionInfo.url.includes('/api') ? omsRedirectionInfo.url : `${omsRedirectionInfo.url}/api/` : `https://${omsRedirectionInfo.url}.hotwax.io/api/`;
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);
}
}

const getUserProfile = async (token: any): Promise<any> => {
const url = store.getters["user/getBaseUrl"]
const baseURL = url.startsWith('http') ? url.includes('/rest/s1/order-routing') ? url : `${url}/rest/s1/order-routing/` : `https://${url}.hotwax.io/rest/s1/order-routing/`;
Expand Down Expand Up @@ -106,6 +197,7 @@ export const UserService = {
getAvailableTimeZones,
getEComStores,
getUserProfile,
getUserPermissions,
login,
setUserTimeZone,
}
3 changes: 3 additions & 0 deletions src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import createPersistedState from "vuex-persistedstate";
import userModule from "./modules/user";
import utilModule from "./modules/util"
import orderRoutingModule from "./modules/orderRouting"
import { setPermissions } from "@/authorization"

// TODO check how to register it from the components only
// Handle same module registering multiple time on page refresh
Expand All @@ -33,6 +34,8 @@ const store = createStore<RootState>({
},
})

setPermissions(store.getters['user/getUserPermissions']);

export default store
export function useStore(): typeof store {
return useVuexStore()
Expand Down
1 change: 1 addition & 0 deletions src/store/modules/user/UserState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export default interface UserState {
url: string;
token: string;
}
permissions: any;
}
Loading

0 comments on commit 810b490

Please sign in to comment.