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

Commit 72cfe63

Browse files
authored
Merge pull request #108 from topcoder-platform/feature/permissions
Permissions part 1 - permission based on Topcoder Roles
2 parents 2aca828 + da3f5c4 commit 72cfe63

File tree

16 files changed

+467
-29
lines changed

16 files changed

+467
-29
lines changed

src/components/ActionsMenu/index.jsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* Shows dropdown menu with actions.
55
*/
66
import React, { useState, useCallback } from "react";
7+
import _ from "lodash";
78
import PT from "prop-types";
89
import "./styles.module.scss";
910
import OutsideClickHandler from "react-outside-click-handler";
@@ -89,7 +90,7 @@ const ActionsMenu = ({ options = [] }) => {
8990
{...attributes.popper}
9091
>
9192
<div styleName="list">
92-
{options.map((option, index) => {
93+
{_.reject(options, "hidden").map((option, index) => {
9394
if (option.separator) {
9495
return <div key={index} styleName="separator" />;
9596
} else {
@@ -124,6 +125,7 @@ ActionsMenu.propTypes = {
124125
label: PT.string,
125126
action: PT.func,
126127
separator: PT.bool,
128+
hidden: PT.bool,
127129
})
128130
),
129131
};

src/components/FormField/index.jsx

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ const FormField = ({ field }) => {
8787
onChange={input.onChange}
8888
onBlur={input.onBlur}
8989
onFocus={input.onFocus}
90+
disabled={field.disabled}
9091
/>
9192
)}
9293
{(field.isRequired || field.customValidator) &&

src/components/ReactSelect/index.jsx

+3
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const ReactSelect = (props) => {
8484
onInputChange={props.onInputChange}
8585
noOptionsMessage={() => props.noOptionsText}
8686
createOptionPosition="first"
87+
isDisabled={props.disabled}
8788
/>
8889
) : (
8990
<Select
@@ -98,6 +99,7 @@ const ReactSelect = (props) => {
9899
placeholder={props.placeholder}
99100
onInputChange={props.onInputChange}
100101
noOptionsMessage={() => props.noOptionsText}
102+
isDisabled={props.disabled}
101103
/>
102104
)}
103105
</div>
@@ -121,6 +123,7 @@ ReactSelect.propTypes = {
121123
),
122124
isCreatable: PT.bool,
123125
noOptionsText: PT.string,
126+
disabled: PT.bool,
124127
};
125128

126129
export default ReactSelect;

src/components/TCForm/index.jsx

+16-8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* Shows form, field and actions.
55
*/
66
import React, { useState, useEffect } from "react";
7+
import _ from "lodash";
78
import PT from "prop-types";
89
import { FORM_ROW_TYPE, FORM_FIELD_TYPE } from "../../constants";
910
import { Form } from "react-final-form";
@@ -56,17 +57,24 @@ const TCForm = ({
5657
<>
5758
{row.type === FORM_ROW_TYPE.GROUP && (
5859
<div styleName="field-group">
59-
{row.fields.map((field) => (
60-
<div styleName="field-group-field" key={field.name}>
61-
<FormField field={fields[field]} />
62-
</div>
63-
))}
60+
{row.fields.map((field) =>
61+
!fields[field].hidden ? (
62+
<div
63+
styleName="field-group-field"
64+
key={field.name}
65+
>
66+
<FormField field={fields[field]} />
67+
</div>
68+
) : null
69+
)}
6470
</div>
6571
)}
6672
{row.type === FORM_ROW_TYPE.SINGLE &&
67-
row.fields.map((field) => {
68-
return <FormField field={fields[field]} />;
69-
})}
73+
row.fields.map((field) =>
74+
!fields[field].hidden ? (
75+
<FormField field={fields[field]} />
76+
) : null
77+
)}
7078
</>
7179
);
7280
})}

src/constants/permissions.js

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* User permission policies.
3+
* Can be used with `hasPermission` method.
4+
*
5+
* PERMISSION GUIDELINES
6+
*
7+
* All the permission name and meaning should define **WHAT** can be done having such permission
8+
* but not **WHO** can do it.
9+
*
10+
* Examples of CORRECT permission naming and meaning:
11+
* - `VIEW_PROJECT`
12+
* - `EDIT_MILESTONE`
13+
* - `DELETE_WORK`
14+
*
15+
* Examples of INCORRECT permissions naming and meaning:
16+
* - `COPILOT_AND_MANAGER`
17+
* - `PROJECT_MEMBERS`
18+
* - `ADMINS`
19+
*
20+
* The same time **internally only** in this file, constants like `COPILOT_AND_ABOVE`,
21+
* `PROJECT_MEMBERS`, `ADMINS` could be used to define permissions.
22+
*
23+
* NAMING GUIDELINES
24+
*
25+
* There are unified prefixes to indicate what kind of permissions.
26+
* If no prefix is suitable, please, feel free to use a new prefix.
27+
*
28+
* CREATE_ - create somethings
29+
* READ_ - read something
30+
* UPDATE_ - update something
31+
* DELETE_ - delete something
32+
*
33+
* MANAGE_ - means combination of 3 operations CREATE/UPDATE/DELETE.
34+
* usually should be used, when READ operation is allowed to everyone
35+
* while 3 manage operations require additional permissions
36+
* ACCESS_ - means combination of all 4 operations READ/CREATE/UPDATE/DELETE.
37+
* usually should be used, when by default users cannot even READ something
38+
* and if someone can READ, then also can do other kind of operations.
39+
*
40+
* ANTI-PERMISSIONS
41+
*
42+
* If it's technically impossible to create permission rules for some situation in "allowed" manner,
43+
* in such case we can create permission rules, which would disallow somethings.
44+
* - Create such rules ONLY IF CREATING ALLOW RULE IS IMPOSSIBLE.
45+
* - Add a comment to such rules explaining why allow-rule cannot be created.
46+
*/
47+
/* eslint-disable no-unused-vars */
48+
import _ from "lodash";
49+
50+
/**
51+
* Topcoder User roles
52+
*
53+
* Here we list only the part of the roles which might be used in the TaaS App
54+
*/
55+
export const TOPCODER_ROLE = {
56+
BOOKING_MANAGER: "bookingmanager",
57+
CONNECT_MANAGER: "Connect Manager",
58+
ADMINISTRATOR: "administrator",
59+
TOPCODER_USER: "Topcoder User",
60+
};
61+
62+
/**
63+
* Project Member roles
64+
*
65+
* NOTE: Team in the TaaS app is same as Project in Connect App or Projects Serivce API
66+
*
67+
* Here we list only the part of the project member roles which are used in TaaS App
68+
*/
69+
export const PROJECT_ROLE = {
70+
CUSTOMER: "customer",
71+
};
72+
73+
/*
74+
* The next sets of roles are exported for outside usage with `hasPermission` method.
75+
*/
76+
77+
export const PERMISSIONS = {
78+
EDIT_RESOURCE_BOOKING: {
79+
meta: {
80+
group: "Resource Booking",
81+
title: "Edit Resource Booking",
82+
},
83+
topcoderRoles: [TOPCODER_ROLE.BOOKING_MANAGER, TOPCODER_ROLE.ADMINISTRATOR],
84+
},
85+
86+
ACCESS_RESOURCE_BOOKING_MEMBER_RATE: {
87+
meta: {
88+
group: "Resource Booking",
89+
title: "Access Member Rate (view and edit)",
90+
},
91+
topcoderRoles: [TOPCODER_ROLE.BOOKING_MANAGER, TOPCODER_ROLE.ADMINISTRATOR],
92+
},
93+
94+
EDIT_JOB_STATUS: {
95+
meta: {
96+
group: "Job",
97+
title: 'Edit Job "status"',
98+
},
99+
topcoderRoles: [TOPCODER_ROLE.BOOKING_MANAGER, TOPCODER_ROLE.ADMINISTRATOR],
100+
},
101+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Auth User actions
3+
*/
4+
5+
export const ACTION_TYPE = {
6+
AUTH_USER_SUCCESS: "AUTH_USER_SUCCESS",
7+
AUTH_USER_ERROR: "AUTH_USER_ERROR",
8+
};
9+
10+
/**
11+
* Action to set auth user data
12+
*
13+
* @param {object} tokenData user data from token
14+
*/
15+
export const authUserSuccess = (tokenData) => ({
16+
type: ACTION_TYPE.AUTH_USER_SUCCESS,
17+
payload: tokenData,
18+
});
19+
20+
/**
21+
* Action to set auth user error
22+
*/
23+
export const authUserError = (error) => ({
24+
type: ACTION_TYPE.AUTH_USER_ERROR,
25+
payload: error,
26+
});

src/hoc/withAuthentication.js renamed to src/hoc/withAuthentication/index.js

+14-6
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,38 @@
44
* wrap component for authentication
55
*/
66
import React, { useState, useEffect } from "react";
7+
import _ from "lodash";
78
import { getAuthUserTokens, login } from "@topcoder/micro-frontends-navbar-app";
8-
import LoadingIndicator from "../components/LoadingIndicator";
9+
import LoadingIndicator from "../../components/LoadingIndicator";
10+
import { authUserSuccess, authUserError } from "./actions";
11+
import { decodeToken } from "utils/helpers";
12+
import { useDispatch, useSelector } from "react-redux";
913

1014
export default function withAuthentication(Component) {
1115
const AuthenticatedComponent = (props) => {
12-
let [isLoggedIn, setIsLoggedIn] = useState(null);
13-
let [authError, setAuthError] = useState(false);
16+
const dispatch = useDispatch();
17+
const { isLoggedIn, authError } = useSelector((state) => state.authUser);
1418

1519
useEffect(() => {
1620
// prevent page redirecting to login page when unmount
1721
let isUnmount = false;
1822
getAuthUserTokens()
1923
.then(({ tokenV3 }) => {
2024
if (!!tokenV3) {
21-
setIsLoggedIn(!!tokenV3);
25+
const tokenData = decodeToken(tokenV3);
26+
dispatch(
27+
authUserSuccess(_.pick(tokenData, ["userId", "handle", "roles"]))
28+
);
2229
} else if (!isUnmount) {
2330
login();
2431
}
2532
})
26-
.catch(setAuthError);
33+
.catch((error) => dispatch(authUserError(error)));
34+
2735
return () => {
2836
isUnmount = true;
2937
};
30-
}, []);
38+
}, [dispatch]);
3139

3240
return (
3341
<>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Reducer for `authUser`
3+
*/
4+
5+
import { ACTION_TYPE } from "../actions";
6+
7+
const initialState = {
8+
isLoggedIn: null,
9+
userId: null,
10+
handle: null,
11+
roles: [],
12+
authError: null,
13+
};
14+
15+
const reducer = (state = initialState, action) => {
16+
switch (action.type) {
17+
case ACTION_TYPE.AUTH_USER_SUCCESS:
18+
return {
19+
...initialState,
20+
...action.payload,
21+
isLoggedIn: true,
22+
};
23+
24+
case ACTION_TYPE.AUTH_USER_ERROR:
25+
return {
26+
...initialState,
27+
authError: action.payload,
28+
};
29+
30+
default:
31+
return state;
32+
}
33+
};
34+
35+
export default reducer;

src/reducers/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import { reducer as toastrReducer } from "react-redux-toastr";
66
import positionDetailsReducer from "../routes/PositionDetails/reducers";
77
import teamMembersReducer from "../routes/TeamAccess/reducers";
88
import reportPopupReducer from "../components/ReportPopup/reducers";
9+
import authUserReducer from "../hoc/withAuthentication/reducers";
910

1011
const rootReducer = combineReducers({
1112
toastr: toastrReducer,
1213
positionDetails: positionDetailsReducer,
1314
teamMembers: teamMembersReducer,
1415
reportPopup: reportPopupReducer,
16+
authUser: authUserReducer,
1517
});
1618

1719
export default rootReducer;

src/routes/JobForm/utils.js

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/**
22
* JobForm utilities
33
*/
4+
import { PERMISSIONS } from "constants/permissions";
5+
import { hasPermission } from "utils/permissions";
46
import {
57
RATE_TYPE_OPTIONS,
68
STATUS_OPTIONS,
@@ -101,6 +103,7 @@ export const getEditJobConfig = (skillOptions, onSubmit) => {
101103
validationMessage: "Please, select Status",
102104
name: "status",
103105
selectOptions: STATUS_OPTIONS,
106+
disabled: !hasPermission(PERMISSIONS.EDIT_JOB_STATUS),
104107
},
105108
],
106109
onSubmit: onSubmit,

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

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
import Input from "components/Input";
2525
import { skillShape } from "components/SkillsList";
2626
import { useReportPopup } from "components/ReportPopup/hooks/useReportPopup";
27+
import { hasPermission } from "utils/permissions";
28+
import { PERMISSIONS } from "constants/permissions";
2729

2830
const TeamMembers = ({ team }) => {
2931
const { resources, jobs } = team;
@@ -156,9 +158,11 @@ const TeamMembers = ({ team }) => {
156158
`/taas/myteams/${team.id}/rb/${member.id}/edit`
157159
);
158160
},
161+
hidden: !hasPermission(PERMISSIONS.EDIT_RESOURCE_BOOKING),
159162
},
160163
{
161164
separator: true,
165+
hidden: !hasPermission(PERMISSIONS.EDIT_RESOURCE_BOOKING),
162166
},
163167
{
164168
label: "Report an Issue",

src/routes/ResourceBookingDetails/ResourceDetails/index.jsx

+9-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import IconMoney from "../../../assets/images/icon-money.svg";
1111
import IconComputer from "../../../assets/images/icon-computer.svg";
1212
import IconBag from "../../../assets/images/icon-bag.svg";
1313
import "./styles.module.scss";
14+
import { PERMISSIONS } from "constants/permissions";
15+
import { hasPermission } from "utils/permissions";
1416

1517
const ResourceDetails = ({ resource, jobTitle }) => {
1618
return (
@@ -55,11 +57,13 @@ const ResourceDetails = ({ resource, jobTitle }) => {
5557
/>
5658
</div>
5759
<div styleName="table-cell">
58-
<DataItem
59-
icon={<IconMoney />}
60-
title="Member Rate"
61-
children={formatMoney(resource.memberRate)}
62-
/>
60+
{hasPermission(PERMISSIONS.ACCESS_RESOURCE_BOOKING_MEMBER_RATE) && (
61+
<DataItem
62+
icon={<IconMoney />}
63+
title="Member Rate"
64+
children={formatMoney(resource.memberRate)}
65+
/>
66+
)}
6367
</div>
6468
</div>
6569
</div>

0 commit comments

Comments
 (0)