diff --git a/src/constants/index.js b/src/constants/index.js index 24530d12..e10a51f6 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -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 @@ -204,7 +210,7 @@ export const ACTION_TYPE = { }; /** - * All fonr field types + * All form field types */ export const FORM_FIELD_TYPE = { TEXT: "text", diff --git a/src/constants/permissions.js b/src/constants/permissions.js index 299dd8ec..c57a6d82 100644 --- a/src/constants/permissions.js +++ b/src/constants/permissions.js @@ -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: @@ -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], }, diff --git a/src/hoc/withAuthentication/actions/index.js b/src/hoc/withAuthentication/actions/index.js index 5e89291f..904e2c25 100644 --- a/src/hoc/withAuthentication/actions/index.js +++ b/src/hoc/withAuthentication/actions/index.js @@ -2,6 +2,7 @@ * Auth User actions */ import { ACTION_TYPE } from "constants"; +import { getTeamMembers } from "services/teams"; /** * Action to set auth user data @@ -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, }); diff --git a/src/hoc/withAuthentication/index.js b/src/hoc/withAuthentication/index.js index a767fec7..a8de5ea3 100644 --- a/src/hoc/withAuthentication/index.js +++ b/src/hoc/withAuthentication/index.js @@ -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 && } + {isLoggedIn === null || (params.teamId && !teamMembersLoaded) && } - {/* Show component only if user is logged-in */} - {isLoggedIn === true ? : 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) ? : null} ); }; diff --git a/src/hoc/withAuthentication/reducers/index.js b/src/hoc/withAuthentication/reducers/index.js index 8db1e38e..89302a18 100644 --- a/src/hoc/withAuthentication/reducers/index.js +++ b/src/hoc/withAuthentication/reducers/index.js @@ -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; } diff --git a/src/routes/JobForm/utils.js b/src/routes/JobForm/utils.js index 556565c1..a5923620 100644 --- a/src/routes/JobForm/utils.js +++ b/src/routes/JobForm/utils.js @@ -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, diff --git a/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx b/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx index 71da63c2..d4d041ae 100644 --- a/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx +++ b/src/routes/MyTeamsDetails/components/TeamMembers/index.jsx @@ -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", diff --git a/src/routes/PositionDetails/components/PositionCandidates/index.jsx b/src/routes/PositionDetails/components/PositionCandidates/index.jsx index 44fc99e6..821cce64 100644 --- a/src/routes/PositionDetails/components/PositionCandidates/index.jsx +++ b/src/routes/PositionDetails/components/PositionCandidates/index.jsx @@ -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 @@ -192,7 +194,7 @@ const PositionCandidates = ({ position, candidateStatus, updateCandidate }) => { )}
- {candidateStatus === CANDIDATE_STATUS.OPEN && ( + {candidateStatus === CANDIDATE_STATUS.OPEN && hasPermission(PERMISSIONS.UPDATE_JOB_CANDIDATE) && ( <> Interested in this candidate?
diff --git a/src/routes/ResourceBookingDetails/index.jsx b/src/routes/ResourceBookingDetails/index.jsx index 3dfd30f3..084d07b0 100644 --- a/src/routes/ResourceBookingDetails/index.jsx +++ b/src/routes/ResourceBookingDetails/index.jsx @@ -63,7 +63,7 @@ const ResourceBookingDetails = ({ teamId, resourceBookingId }) => {
- {hasPermission(PERMISSIONS.EDIT_RESOURCE_BOOKING) && ( + {hasPermission(PERMISSIONS.UPDATE_RESOURCE_BOOKING) && (