Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented: support for permissions in the app and added suppot to make api calls on redirection oms(dxp-323) #43

Merged
merged 2 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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="COMPANY_APP_VIEW"
VUE_APP_DEFAULT_LOG_LEVEL="error"
VUE_APP_COMPANY_PARTY_ID="COMPANY"
VUE_APP_LOGIN_URL="https://launchpad.hotwax.io/login"
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.15.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",
"http-status-codes": "^2.1.4",
"luxon": "^2.3.0",
Expand Down
22 changes: 13 additions & 9 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import OfflineHelper from "@/offline-helper"
import emitter from "@/event-bus"
import store from "@/store";
import { StatusCodes } from "http-status-codes";


axios.interceptors.request.use((config: any) => {
// TODO: pass csrf token
Expand Down Expand Up @@ -42,13 +42,13 @@
setTimeout(() => emitter.emit("dismissLoader"), 100);
if (error.response) {
// TODO Handle case for failed queue request
const { status } = error.response;

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

View workflow job for this annotation

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

'status' is assigned a value but never used

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

View workflow job for this annotation

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

'status' is assigned a value but never used
if (status === StatusCodes.UNAUTHORIZED) {
store.dispatch("user/logout");
const redirectUrl = window.location.origin + '/login';
// Explicitly passing isLoggedOut as in case of maarg apps we need to call the logout api in launchpad
window.location.href = `${process.env.VUE_APP_LOGIN_URL}?redirectUrl=${redirectUrl}&isLoggedOut=true`;
}
// if (status === StatusCodes.UNAUTHORIZED) {
// store.dispatch("user/logout");
// const redirectUrl = window.location.origin + '/login';
// // Explicitly passing isLoggedOut as in case of maarg apps we need to call the logout api in launchpad
// window.location.href = `${process.env.VUE_APP_LOGIN_URL}?redirectUrl=${redirectUrl}&isLoggedOut=true`;
// }
}
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
Expand Down Expand Up @@ -87,7 +87,11 @@
}

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

if (baseURL) {
config.baseURL = baseURL.startsWith('http') ? baseURL.includes('/rest/s1/admin') ? baseURL : `${baseURL}/rest/s1/admin/` : `https://${baseURL}.hotwax.io/rest/s1/admin/`;
}

if(customConfig.cache) config.adapter = axiosCache.adapter;
const networkStatus = await OfflineHelper.getNetworkStatus();
if (customConfig.queue && !networkStatus.connected) {
Expand All @@ -108,7 +112,7 @@
* @return {Promise} Response from API as returned by Axios
*/
const client = (config: any) => {
return axios.request(config);
return axios.create().request(config);
}

export { api as default, client, axios };
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 {
"COMPANY_APP_VIEW": "COMPANY_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 {
"COMPANY_APP_VIEW": "COMPANY_APP_VIEW"
} as any
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 @@ -29,6 +29,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';
import { dxpComponents } from "@hotwax/dxp-components"
import { login, logout, loader } from "@/user-utils";
import { askQuery, getConfig, getGitBookPage, initialise, searchQuery } from '@/adapter';
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
91 changes: 91 additions & 0 deletions src/services/UserService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,100 @@ const setUserTimeZone = async (payload: any): Promise <any> => {
});
}

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

export const UserService = {
getAvailableTimeZones,
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 productStoreModule from "./modules/productStore";
import utilModule from "./modules/util"
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
3 changes: 2 additions & 1 deletion src/store/modules/user/UserState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export default interface UserState {
omsRedirectionInfo: {
url: string;
token: string;
}
},
permissions: any;
}
Loading
Loading