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 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
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
36 changes: 24 additions & 12 deletions src/constants/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*
* Examples of CORRECT permission naming and meaning:
* - `VIEW_PROJECT`
* - `EDIT_MILESTONE`
* - `UPDATE_MILESTONE`
* - `DELETE_WORK`
*
* Examples of INCORRECT permissions naming and meaning:
Expand Down Expand Up @@ -78,31 +78,43 @@ export const PROJECT_ROLE = {

export const PERMISSIONS = {
/**
* Resource Booking
* Job
*/
EDIT_RESOURCE_BOOKING: {
UPDATE_JOB_STATUS: {
meta: {
group: "Resource Booking",
title: "Edit Resource Booking",
group: "Job",
title: 'Edit Job "status"',
},
topcoderRoles: [TOPCODER_ROLE.BOOKING_MANAGER, TOPCODER_ROLE.ADMINISTRATOR],
},

ACCESS_RESOURCE_BOOKING_MEMBER_RATE: {
/**
* Job Candidate
*/
UPDATE_JOB_CANDIDATE: {
meta: {
group: "Resource Booking",
title: "Access Member Rate (view and edit)",
group: "Job Candidate",
title: 'Update Job Candidate',
},
projectRoles: true,
topcoderRoles: [TOPCODER_ROLE.BOOKING_MANAGER, TOPCODER_ROLE.ADMINISTRATOR],
},

/**
* Job
* Resource Booking
*/
EDIT_JOB_STATUS: {
UPDATE_RESOURCE_BOOKING: {
meta: {
group: "Job",
title: 'Edit Job "status"',
group: "Resource Booking",
title: "Edit Resource Booking",
},
topcoderRoles: [TOPCODER_ROLE.BOOKING_MANAGER, TOPCODER_ROLE.ADMINISTRATOR],
},

ACCESS_RESOURCE_BOOKING_MEMBER_RATE: {
meta: {
group: "Resource Booking",
title: "Access Member Rate (view and edit)",
},
topcoderRoles: [TOPCODER_ROLE.BOOKING_MANAGER, TOPCODER_ROLE.ADMINISTRATOR],
},
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,
});
84 changes: 65 additions & 19 deletions src/hoc/withAuthentication/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,95 @@
* 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, teamMembersLoaded, teamMembersLoadingError } = 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 (
<>
{/* Show loading indicator until we know if user is logged-in or no.
Also, show loading indicator if we need to know team members but haven't loaded them yet.
In we got error during this process, show error */}
{isLoggedIn === null && <LoadingIndicator error={authError} />}
{isLoggedIn === null || (params.teamId && !teamMembersLoaded) && <LoadingIndicator error={authError || teamMembersLoadingError} />}

{/* Show component only if user is logged-in */}
{isLoggedIn === true ? <Component {...props} /> : null}
{/* Show component only if user is logged-in and if we don't need team members or we already loaded them */}
{isLoggedIn === true && (!params.teamId || teamMembersLoaded) ? <Component {...props} /> : null}
</>
);
};
Expand Down
82 changes: 75 additions & 7 deletions src/hoc/withAuthentication/reducers/index.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,100 @@
/**
* 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,
teamMembersLoaded: false,
};

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,
teamMembersLoaded: false,
};

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,
teamMembersLoaded: true,
};
}

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,
teamMembersLoaded: false,
};
}

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/JobForm/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export const getEditJobConfig = (skillOptions, onSubmit) => {
validationMessage: "Please, select Status",
name: "status",
selectOptions: STATUS_OPTIONS,
disabled: !hasPermission(PERMISSIONS.EDIT_JOB_STATUS),
disabled: !hasPermission(PERMISSIONS.UPDATE_JOB_STATUS),
},
],
onSubmit: onSubmit,
Expand Down
4 changes: 2 additions & 2 deletions src/routes/MyTeamsDetails/components/TeamMembers/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,11 @@ const TeamMembers = ({ team }) => {
`/taas/myteams/${team.id}/rb/${member.id}/edit`
);
},
hidden: !hasPermission(PERMISSIONS.EDIT_RESOURCE_BOOKING),
hidden: !hasPermission(PERMISSIONS.UPDATE_RESOURCE_BOOKING),
},
{
separator: true,
hidden: !hasPermission(PERMISSIONS.EDIT_RESOURCE_BOOKING),
hidden: !hasPermission(PERMISSIONS.UPDATE_RESOURCE_BOOKING),
},
{
label: "Report an Issue",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import Pagination from "components/Pagination";
import IconResume from "../../../../assets/images/icon-resume.svg";
import { toastr } from "react-redux-toastr";
import { getJobById } from "services/jobs";
import { PERMISSIONS } from "constants/permissions";
import { hasPermission } from "utils/permissions";

/**
* Generates a function to sort candidates
Expand Down Expand Up @@ -192,7 +194,7 @@ const PositionCandidates = ({ position, candidateStatus, updateCandidate }) => {
)}
</div>
<div styleName="table-cell cell-action">
{candidateStatus === CANDIDATE_STATUS.OPEN && (
{candidateStatus === CANDIDATE_STATUS.OPEN && hasPermission(PERMISSIONS.UPDATE_JOB_CANDIDATE) && (
<>
Interested in this candidate?
<div styleName="actions">
Expand Down
2 changes: 1 addition & 1 deletion src/routes/ResourceBookingDetails/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const ResourceBookingDetails = ({ teamId, resourceBookingId }) => {
<div styleName="content-wrapper">
<ResourceSummary member={member} />
<ResourceDetails resource={resource} jobTitle={jobTitle} />
{hasPermission(PERMISSIONS.EDIT_RESOURCE_BOOKING) && (
{hasPermission(PERMISSIONS.UPDATE_RESOURCE_BOOKING) && (
<div styleName="actions">
<Button
size="medium"
Expand Down
Loading