Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Permissions part 2 - permission based on Project Roles (members) #110

Merged
merged 2 commits into from
Feb 23, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
feat: loading team members for permissinos
ref issue #83
  • Loading branch information
maxceem committed Feb 23, 2021
commit 074e0fb113a04208f2c34d3b00ea0b43a37f9620
8 changes: 7 additions & 1 deletion src/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ export const ACTION_TYPE = {
*/
AUTH_USER_SUCCESS: "AUTH_USER_SUCCESS",
AUTH_USER_ERROR: "AUTH_USER_ERROR",
// load team members for authentication/permission purposes
AUTH_LOAD_TEAM_MEMBERS: "AUTH_LOAD_TEAM_MEMBERS",
AUTH_LOAD_TEAM_MEMBERS_PENDING: "AUTH_LOAD_TEAM_MEMBERS_PENDING",
AUTH_LOAD_TEAM_MEMBERS_SUCCESS: "AUTH_LOAD_TEAM_MEMBERS_SUCCESS",
AUTH_LOAD_TEAM_MEMBERS_ERROR: "AUTH_LOAD_TEAM_MEMBERS_ERROR",
AUTH_CLEAR_TEAM_MEMBERS: "AUTH_CLEAR_TEAM_MEMBERS",

/*
Report Popup
Expand Down Expand Up @@ -204,7 +210,7 @@ export const ACTION_TYPE = {
};

/**
* All fonr field types
* All form field types
*/
export const FORM_FIELD_TYPE = {
TEXT: "text",
Expand Down
28 changes: 24 additions & 4 deletions src/hoc/withAuthentication/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Auth User actions
*/
import { ACTION_TYPE } from "constants";
import { getTeamMembers } from "services/teams";

/**
* Action to set auth user data
Expand All @@ -22,9 +23,28 @@ export const authUserError = (error) => ({
});

/**
* Action to load project/team members
* Loads team members for authentication/permission purposes
*
* @param {string|number} teamId
*
* @returns {Promise} loaded members or error
*/
export const loadTeamMembers = (error) => ({
type: ACTION_TYPE.AUTH_USER_ERROR,
payload: error,
export const authLoadTeamMembers = (teamId) => ({
type: ACTION_TYPE.AUTH_LOAD_TEAM_MEMBERS,
payload: async () => {
const res = await getTeamMembers(teamId);
return res.data;
},
meta: {
teamId,
},
});

/**
* Clear team members for authentication/permission purposes
*
* We need this if we are going to some route which doesn't have `teamId`
*/
export const authClearTeamMembers = () => ({
type: ACTION_TYPE.AUTH_CLEAR_TEAM_MEMBERS,
});
77 changes: 61 additions & 16 deletions src/hoc/withAuthentication/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,85 @@
* Authentication
*
* wrap component for authentication
*
* - checks if user is logged-in, and if not, then redirects to the login page
*
* Also, this component load important data for `hasPermission` method:
* - decodes user token and set in Redux Store `authUser.userId, handle, roles`
* - we need to know user `roles` to check if user user has Topcoder Roles
* - load team (project) members if current route has `:teamId` param
* - we need to know members of the team to check user users Project Roles
*/
import React, { useState, useEffect } from "react";
import React, { useEffect } from "react";
import _ from "lodash";
import { getAuthUserTokens, login } from "@topcoder/micro-frontends-navbar-app";
import LoadingIndicator from "../../components/LoadingIndicator";
import { authUserSuccess, authUserError } from "./actions";
import {
authUserSuccess,
authUserError,
authLoadTeamMembers,
authClearTeamMembers,
} from "./actions";
import { decodeToken } from "tc-auth-lib";
import { useDispatch, useSelector } from "react-redux";
import { useParams } from "@reach/router";

export default function withAuthentication(Component) {
const AuthenticatedComponent = (props) => {
const dispatch = useDispatch();
const { isLoggedIn, authError } = useSelector((state) => state.authUser);
const { isLoggedIn, authError, teamId } = useSelector(
(state) => state.authUser
);
const params = useParams();

/*
Check if user is logged-in or redirect ot the login page
*/
useEffect(() => {
// prevent page redirecting to login page when unmount
let isUnmount = false;
getAuthUserTokens()
.then(({ tokenV3 }) => {
if (!!tokenV3) {
const tokenData = decodeToken(tokenV3);
dispatch(
authUserSuccess(_.pick(tokenData, ["userId", "handle", "roles"]))
);
} else if (!isUnmount) {
login();
}
})
.catch((error) => dispatch(authUserError(error)));

if (!isLoggedIn) {
getAuthUserTokens()
.then(({ tokenV3 }) => {
if (!!tokenV3) {
const tokenData = decodeToken(tokenV3);
dispatch(
authUserSuccess(
_.pick(tokenData, ["userId", "handle", "roles"])
)
);
} else if (!isUnmount) {
login();
}
})
.catch((error) => dispatch(authUserError(error)));
}

return () => {
isUnmount = true;
};
}, [dispatch]);
}, [dispatch, isLoggedIn]);

/*
Load team (project) members if current URL has `:teamId` param
*/
useEffect(() => {
// if we haven't loaded team members yet, or we if we've moved to a page for another team
// we have to load team members which we would use for checking permissions
if (
isLoggedIn &&
params.teamId &&
(!teamId || params.teamId !== teamId)
) {
dispatch(authLoadTeamMembers(params.teamId));

// if we are going to some page without `teamId` then we have to clear team members
// if we had some
} else if (teamId && !params.teamId) {
dispatch(authClearTeamMembers());
}
}, [params.teamId, teamId, dispatch, isLoggedIn]);

return (
<>
Expand Down
78 changes: 71 additions & 7 deletions src/hoc/withAuthentication/reducers/index.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,96 @@
/**
* Reducer for `authUser`
*/

import _ from "lodash";
import { ACTION_TYPE } from "constants";

const initialState = {
isLoggedIn: null,
userId: null,
handle: null,
isLoggedIn: undefined,
userId: undefined,
handle: undefined,
roles: [],
authError: null,
authError: undefined,
// for permissions check purpose we need to know team `members'
teamId: undefined,
teamMembers: undefined,
teamMembersLoading: undefined,
teamMembersLoadingError: undefined,
};

const authInitialState = _.pick(initialState, [
"isLoggedIn",
"userId",
"handle",
"roles",
"authError",
]);

const teamMembersInitialState = _.pick(initialState, [
"teamId",
"teamMembers",
"teamMembersLoading",
"teamMembersLoadingError",
]);

const reducer = (state = initialState, action) => {
switch (action.type) {
case ACTION_TYPE.AUTH_USER_SUCCESS:
return {
...initialState,
...state,
...authInitialState,
...action.payload,
isLoggedIn: true,
};

case ACTION_TYPE.AUTH_USER_ERROR:
return {
...initialState,
...state,
...authInitialState,
authError: action.payload,
};

case ACTION_TYPE.AUTH_LOAD_TEAM_MEMBERS_PENDING:
return {
...state,
teamId: action.meta.teamId,
teamMembers: initialState.teamMembersLoadingError,
teamMembersLoading: true,
teamMembersLoadingError: initialState.teamMembersLoadingError,
};

case ACTION_TYPE.AUTH_LOAD_TEAM_MEMBERS_SUCCESS: {
// only set loaded team members if we haven't changed the team yet
if (state.teamId === action.meta.teamId) {
return {
...state,
teamMembersLoading: false,
teamMembers: action.payload,
};
}

return state;
}

case ACTION_TYPE.AUTH_LOAD_TEAM_MEMBERS_ERROR: {
// only set error for loading team members if we haven't changed the team yet
if (state.teamId === action.meta.teamId) {
return {
...state,
teamMembersLoading: false,
teamMembersLoadingError: action.payload,
};
}

return state;
}

case ACTION_TYPE.AUTH_CLEAR_TEAM_MEMBERS: {
return {
...state,
...teamMembersInitialState,
};
}

default:
return state;
}
Expand Down
2 changes: 1 addition & 1 deletion src/routes/TeamAccess/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
getMemberSuggestions,
postMembers,
} from "services/teams";
import { ACTION_TYPE } from "constants"
import { ACTION_TYPE } from "constants";

/**
* Loads team members
Expand Down
16 changes: 14 additions & 2 deletions src/utils/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,20 @@ import store from "../store";
*/
export const hasPermission = (permission, entities = {}) => {
const user = entities.user || _.get(store.getState(), "authUser", {});
// TODO: at the moment there is no place in Redux Store where we store project, so we have to always pass it manually
const project = entities.project || null;
let project = entities.project;

// if project was not provided directly, then try to build it
// based on the team members which might be loaded to the Redux Store
// into `authUser.teamMembers` (this only happens for pages which have URL param `:teamId`)
if (!project) {
const teamMembers = _.get(store.getState(), "authUser.teamMembers");

if (teamMembers) {
project = {
members: teamMembers,
};
}
}

const allowRule = permission.allowRule ? permission.allowRule : permission;
const denyRule = permission.denyRule ? permission.denyRule : null;
Expand Down