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

Commit 382e9ba

Browse files
authored
Merge pull request #110 from topcoder-platform/feature/permissions-part-2
Permissions part 2 - permission based on Project Roles (members)
2 parents cf60658 + f18605e commit 382e9ba

File tree

12 files changed

+219
-53
lines changed

12 files changed

+219
-53
lines changed

src/constants/index.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,12 @@ export const ACTION_TYPE = {
169169
*/
170170
AUTH_USER_SUCCESS: "AUTH_USER_SUCCESS",
171171
AUTH_USER_ERROR: "AUTH_USER_ERROR",
172+
// load team members for authentication/permission purposes
173+
AUTH_LOAD_TEAM_MEMBERS: "AUTH_LOAD_TEAM_MEMBERS",
174+
AUTH_LOAD_TEAM_MEMBERS_PENDING: "AUTH_LOAD_TEAM_MEMBERS_PENDING",
175+
AUTH_LOAD_TEAM_MEMBERS_SUCCESS: "AUTH_LOAD_TEAM_MEMBERS_SUCCESS",
176+
AUTH_LOAD_TEAM_MEMBERS_ERROR: "AUTH_LOAD_TEAM_MEMBERS_ERROR",
177+
AUTH_CLEAR_TEAM_MEMBERS: "AUTH_CLEAR_TEAM_MEMBERS",
172178

173179
/*
174180
Report Popup
@@ -204,7 +210,7 @@ export const ACTION_TYPE = {
204210
};
205211

206212
/**
207-
* All fonr field types
213+
* All form field types
208214
*/
209215
export const FORM_FIELD_TYPE = {
210216
TEXT: "text",

src/constants/permissions.js

+24-12
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
*
1010
* Examples of CORRECT permission naming and meaning:
1111
* - `VIEW_PROJECT`
12-
* - `EDIT_MILESTONE`
12+
* - `UPDATE_MILESTONE`
1313
* - `DELETE_WORK`
1414
*
1515
* Examples of INCORRECT permissions naming and meaning:
@@ -78,31 +78,43 @@ export const PROJECT_ROLE = {
7878

7979
export const PERMISSIONS = {
8080
/**
81-
* Resource Booking
81+
* Job
8282
*/
83-
EDIT_RESOURCE_BOOKING: {
83+
UPDATE_JOB_STATUS: {
8484
meta: {
85-
group: "Resource Booking",
86-
title: "Edit Resource Booking",
85+
group: "Job",
86+
title: 'Edit Job "status"',
8787
},
8888
topcoderRoles: [TOPCODER_ROLE.BOOKING_MANAGER, TOPCODER_ROLE.ADMINISTRATOR],
8989
},
9090

91-
ACCESS_RESOURCE_BOOKING_MEMBER_RATE: {
91+
/**
92+
* Job Candidate
93+
*/
94+
UPDATE_JOB_CANDIDATE: {
9295
meta: {
93-
group: "Resource Booking",
94-
title: "Access Member Rate (view and edit)",
96+
group: "Job Candidate",
97+
title: 'Update Job Candidate',
9598
},
99+
projectRoles: true,
96100
topcoderRoles: [TOPCODER_ROLE.BOOKING_MANAGER, TOPCODER_ROLE.ADMINISTRATOR],
97101
},
98102

99103
/**
100-
* Job
104+
* Resource Booking
101105
*/
102-
EDIT_JOB_STATUS: {
106+
UPDATE_RESOURCE_BOOKING: {
103107
meta: {
104-
group: "Job",
105-
title: 'Edit Job "status"',
108+
group: "Resource Booking",
109+
title: "Edit Resource Booking",
110+
},
111+
topcoderRoles: [TOPCODER_ROLE.BOOKING_MANAGER, TOPCODER_ROLE.ADMINISTRATOR],
112+
},
113+
114+
ACCESS_RESOURCE_BOOKING_MEMBER_RATE: {
115+
meta: {
116+
group: "Resource Booking",
117+
title: "Access Member Rate (view and edit)",
106118
},
107119
topcoderRoles: [TOPCODER_ROLE.BOOKING_MANAGER, TOPCODER_ROLE.ADMINISTRATOR],
108120
},

src/hoc/withAuthentication/actions/index.js

+24-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Auth User actions
33
*/
44
import { ACTION_TYPE } from "constants";
5+
import { getTeamMembers } from "services/teams";
56

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

2425
/**
25-
* Action to load project/team members
26+
* Loads team members for authentication/permission purposes
27+
*
28+
* @param {string|number} teamId
29+
*
30+
* @returns {Promise} loaded members or error
2631
*/
27-
export const loadTeamMembers = (error) => ({
28-
type: ACTION_TYPE.AUTH_USER_ERROR,
29-
payload: error,
32+
export const authLoadTeamMembers = (teamId) => ({
33+
type: ACTION_TYPE.AUTH_LOAD_TEAM_MEMBERS,
34+
payload: async () => {
35+
const res = await getTeamMembers(teamId);
36+
return res.data;
37+
},
38+
meta: {
39+
teamId,
40+
},
41+
});
42+
43+
/**
44+
* Clear team members for authentication/permission purposes
45+
*
46+
* We need this if we are going to some route which doesn't have `teamId`
47+
*/
48+
export const authClearTeamMembers = () => ({
49+
type: ACTION_TYPE.AUTH_CLEAR_TEAM_MEMBERS,
3050
});

src/hoc/withAuthentication/index.js

+65-19
Original file line numberDiff line numberDiff line change
@@ -2,49 +2,95 @@
22
* Authentication
33
*
44
* wrap component for authentication
5+
*
6+
* - checks if user is logged-in, and if not, then redirects to the login page
7+
*
8+
* Also, this component load important data for `hasPermission` method:
9+
* - decodes user token and set in Redux Store `authUser.userId, handle, roles`
10+
* - we need to know user `roles` to check if user user has Topcoder Roles
11+
* - load team (project) members if current route has `:teamId` param
12+
* - we need to know members of the team to check user users Project Roles
513
*/
6-
import React, { useState, useEffect } from "react";
14+
import React, { useEffect } from "react";
715
import _ from "lodash";
816
import { getAuthUserTokens, login } from "@topcoder/micro-frontends-navbar-app";
917
import LoadingIndicator from "../../components/LoadingIndicator";
10-
import { authUserSuccess, authUserError } from "./actions";
18+
import {
19+
authUserSuccess,
20+
authUserError,
21+
authLoadTeamMembers,
22+
authClearTeamMembers,
23+
} from "./actions";
1124
import { decodeToken } from "tc-auth-lib";
1225
import { useDispatch, useSelector } from "react-redux";
26+
import { useParams } from "@reach/router";
1327

1428
export default function withAuthentication(Component) {
1529
const AuthenticatedComponent = (props) => {
1630
const dispatch = useDispatch();
17-
const { isLoggedIn, authError } = useSelector((state) => state.authUser);
31+
const { isLoggedIn, authError, teamId, teamMembersLoaded, teamMembersLoadingError } = useSelector(
32+
(state) => state.authUser
33+
);
34+
const params = useParams();
1835

36+
/*
37+
Check if user is logged-in or redirect ot the login page
38+
*/
1939
useEffect(() => {
2040
// prevent page redirecting to login page when unmount
2141
let isUnmount = false;
22-
getAuthUserTokens()
23-
.then(({ tokenV3 }) => {
24-
if (!!tokenV3) {
25-
const tokenData = decodeToken(tokenV3);
26-
dispatch(
27-
authUserSuccess(_.pick(tokenData, ["userId", "handle", "roles"]))
28-
);
29-
} else if (!isUnmount) {
30-
login();
31-
}
32-
})
33-
.catch((error) => dispatch(authUserError(error)));
42+
43+
if (!isLoggedIn) {
44+
getAuthUserTokens()
45+
.then(({ tokenV3 }) => {
46+
if (!!tokenV3) {
47+
const tokenData = decodeToken(tokenV3);
48+
dispatch(
49+
authUserSuccess(
50+
_.pick(tokenData, ["userId", "handle", "roles"])
51+
)
52+
);
53+
} else if (!isUnmount) {
54+
login();
55+
}
56+
})
57+
.catch((error) => dispatch(authUserError(error)));
58+
}
3459

3560
return () => {
3661
isUnmount = true;
3762
};
38-
}, [dispatch]);
63+
}, [dispatch, isLoggedIn]);
64+
65+
/*
66+
Load team (project) members if current URL has `:teamId` param
67+
*/
68+
useEffect(() => {
69+
// if we haven't loaded team members yet, or we if we've moved to a page for another team
70+
// we have to load team members which we would use for checking permissions
71+
if (
72+
isLoggedIn &&
73+
params.teamId &&
74+
(!teamId || params.teamId !== teamId)
75+
) {
76+
dispatch(authLoadTeamMembers(params.teamId));
77+
78+
// if we are going to some page without `teamId` then we have to clear team members
79+
// if we had some
80+
} else if (teamId && !params.teamId) {
81+
dispatch(authClearTeamMembers());
82+
}
83+
}, [params.teamId, teamId, dispatch, isLoggedIn]);
3984

4085
return (
4186
<>
4287
{/* Show loading indicator until we know if user is logged-in or no.
88+
Also, show loading indicator if we need to know team members but haven't loaded them yet.
4389
In we got error during this process, show error */}
44-
{isLoggedIn === null && <LoadingIndicator error={authError} />}
90+
{isLoggedIn === null || (params.teamId && !teamMembersLoaded) && <LoadingIndicator error={authError || teamMembersLoadingError} />}
4591

46-
{/* Show component only if user is logged-in */}
47-
{isLoggedIn === true ? <Component {...props} /> : null}
92+
{/* Show component only if user is logged-in and if we don't need team members or we already loaded them */}
93+
{isLoggedIn === true && (!params.teamId || teamMembersLoaded) ? <Component {...props} /> : null}
4894
</>
4995
);
5096
};

src/hoc/withAuthentication/reducers/index.js

+75-7
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,100 @@
11
/**
22
* Reducer for `authUser`
33
*/
4-
4+
import _ from "lodash";
55
import { ACTION_TYPE } from "constants";
66

77
const initialState = {
8-
isLoggedIn: null,
9-
userId: null,
10-
handle: null,
8+
isLoggedIn: undefined,
9+
userId: undefined,
10+
handle: undefined,
1111
roles: [],
12-
authError: null,
12+
authError: undefined,
13+
// for permissions check purpose we need to know team `members'
14+
teamId: undefined,
15+
teamMembers: undefined,
16+
teamMembersLoading: undefined,
17+
teamMembersLoadingError: undefined,
18+
teamMembersLoaded: false,
1319
};
1420

21+
const authInitialState = _.pick(initialState, [
22+
"isLoggedIn",
23+
"userId",
24+
"handle",
25+
"roles",
26+
"authError",
27+
]);
28+
29+
const teamMembersInitialState = _.pick(initialState, [
30+
"teamId",
31+
"teamMembers",
32+
"teamMembersLoading",
33+
"teamMembersLoadingError",
34+
]);
35+
1536
const reducer = (state = initialState, action) => {
1637
switch (action.type) {
1738
case ACTION_TYPE.AUTH_USER_SUCCESS:
1839
return {
19-
...initialState,
40+
...state,
41+
...authInitialState,
2042
...action.payload,
2143
isLoggedIn: true,
2244
};
2345

2446
case ACTION_TYPE.AUTH_USER_ERROR:
2547
return {
26-
...initialState,
48+
...state,
49+
...authInitialState,
2750
authError: action.payload,
2851
};
2952

53+
case ACTION_TYPE.AUTH_LOAD_TEAM_MEMBERS_PENDING:
54+
return {
55+
...state,
56+
teamId: action.meta.teamId,
57+
teamMembers: initialState.teamMembersLoadingError,
58+
teamMembersLoading: true,
59+
teamMembersLoadingError: initialState.teamMembersLoadingError,
60+
teamMembersLoaded: false,
61+
};
62+
63+
case ACTION_TYPE.AUTH_LOAD_TEAM_MEMBERS_SUCCESS: {
64+
// only set loaded team members if we haven't changed the team yet
65+
if (state.teamId === action.meta.teamId) {
66+
return {
67+
...state,
68+
teamMembersLoading: false,
69+
teamMembers: action.payload,
70+
teamMembersLoaded: true,
71+
};
72+
}
73+
74+
return state;
75+
}
76+
77+
case ACTION_TYPE.AUTH_LOAD_TEAM_MEMBERS_ERROR: {
78+
// only set error for loading team members if we haven't changed the team yet
79+
if (state.teamId === action.meta.teamId) {
80+
return {
81+
...state,
82+
teamMembersLoading: false,
83+
teamMembersLoadingError: action.payload,
84+
teamMembersLoaded: false,
85+
};
86+
}
87+
88+
return state;
89+
}
90+
91+
case ACTION_TYPE.AUTH_CLEAR_TEAM_MEMBERS: {
92+
return {
93+
...state,
94+
...teamMembersInitialState,
95+
};
96+
}
97+
3098
default:
3199
return state;
32100
}

src/routes/JobForm/utils.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export const getEditJobConfig = (skillOptions, onSubmit) => {
103103
validationMessage: "Please, select Status",
104104
name: "status",
105105
selectOptions: STATUS_OPTIONS,
106-
disabled: !hasPermission(PERMISSIONS.EDIT_JOB_STATUS),
106+
disabled: !hasPermission(PERMISSIONS.UPDATE_JOB_STATUS),
107107
},
108108
],
109109
onSubmit: onSubmit,

src/routes/MyTeamsDetails/components/TeamMembers/index.jsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,11 @@ const TeamMembers = ({ team }) => {
158158
`/taas/myteams/${team.id}/rb/${member.id}/edit`
159159
);
160160
},
161-
hidden: !hasPermission(PERMISSIONS.EDIT_RESOURCE_BOOKING),
161+
hidden: !hasPermission(PERMISSIONS.UPDATE_RESOURCE_BOOKING),
162162
},
163163
{
164164
separator: true,
165-
hidden: !hasPermission(PERMISSIONS.EDIT_RESOURCE_BOOKING),
165+
hidden: !hasPermission(PERMISSIONS.UPDATE_RESOURCE_BOOKING),
166166
},
167167
{
168168
label: "Report an Issue",

src/routes/PositionDetails/components/PositionCandidates/index.jsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import Pagination from "components/Pagination";
2323
import IconResume from "../../../../assets/images/icon-resume.svg";
2424
import { toastr } from "react-redux-toastr";
2525
import { getJobById } from "services/jobs";
26+
import { PERMISSIONS } from "constants/permissions";
27+
import { hasPermission } from "utils/permissions";
2628

2729
/**
2830
* Generates a function to sort candidates
@@ -192,7 +194,7 @@ const PositionCandidates = ({ position, candidateStatus, updateCandidate }) => {
192194
)}
193195
</div>
194196
<div styleName="table-cell cell-action">
195-
{candidateStatus === CANDIDATE_STATUS.OPEN && (
197+
{candidateStatus === CANDIDATE_STATUS.OPEN && hasPermission(PERMISSIONS.UPDATE_JOB_CANDIDATE) && (
196198
<>
197199
Interested in this candidate?
198200
<div styleName="actions">

src/routes/ResourceBookingDetails/index.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ const ResourceBookingDetails = ({ teamId, resourceBookingId }) => {
6363
<div styleName="content-wrapper">
6464
<ResourceSummary member={member} />
6565
<ResourceDetails resource={resource} jobTitle={jobTitle} />
66-
{hasPermission(PERMISSIONS.EDIT_RESOURCE_BOOKING) && (
66+
{hasPermission(PERMISSIONS.UPDATE_RESOURCE_BOOKING) && (
6767
<div styleName="actions">
6868
<Button
6969
size="medium"

0 commit comments

Comments
 (0)