diff --git a/src/components/ChallengesComponent/ChallengeList/index.js b/src/components/ChallengesComponent/ChallengeList/index.js index 8de51f21..7e23b282 100644 --- a/src/components/ChallengesComponent/ChallengeList/index.js +++ b/src/components/ChallengesComponent/ChallengeList/index.js @@ -411,7 +411,9 @@ class ChallengeList extends Component { const isMemberOfActiveProject = activeProject && activeProject.members && activeProject.members.some(member => member.userId === loginUserId) if (warnMessage) { - return + const isLinkComponent = warnMessage.includes('{linkComponent}') + const message = isLinkComponent ? warnMessage.replace('{linkComponent}', '') : warnMessage + return support@topcoder.com : null} /> } const statusOptions = _.map(CHALLENGE_STATUS, item => ({ diff --git a/src/components/ChallengesComponent/Message/index.js b/src/components/ChallengesComponent/Message/index.js index fcb0e731..addc9f5d 100644 --- a/src/components/ChallengesComponent/Message/index.js +++ b/src/components/ChallengesComponent/Message/index.js @@ -5,20 +5,23 @@ import React from 'react' import PropTypes from 'prop-types' import styles from './Message.module.scss' -const Message = ({ warnMessage }) => { +const Message = ({ warnMessage, linkComponent }) => { return (
-

{warnMessage}

+ {warnMessage} + {linkComponent}
) } Message.defaultProps = { - warnMessage: '' + warnMessage: '', + linkComponent: () => null } Message.propTypes = { - warnMessage: PropTypes.string + warnMessage: PropTypes.string, + linkComponent: PropTypes.func } export default Message diff --git a/src/config/constants.js b/src/config/constants.js index 209e5d6b..f9302d39 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -488,6 +488,8 @@ export const MESSAGE = { MARK_COMPLETE: 'This will close the task and generate a payment for the assignee and copilot.' } +export const PROJECT_ACCESS_DENIED_MESSAGE = 'You don’t have access to this project. Please contact {linkComponent}' + /** * Challenge cancel reasons */ diff --git a/src/containers/Challenges/index.js b/src/containers/Challenges/index.js index 871f078b..590bb5b9 100644 --- a/src/containers/Challenges/index.js +++ b/src/containers/Challenges/index.js @@ -14,7 +14,7 @@ import { deleteChallenge, loadChallengeTypes } from '../../actions/challenges' -import { loadProject, loadProjects, updateProject } from '../../actions/projects' +import { loadProject, loadProjects, updateProject, clearProjectDetail } from '../../actions/projects' import { loadNextProjects, setActiveProject, @@ -22,16 +22,18 @@ import { } from '../../actions/sidebar' import { checkAdmin, checkIsUserInvitedToProject } from '../../util/tc' import { withRouter } from 'react-router-dom' +import { PROJECT_ACCESS_DENIED_MESSAGE } from '../../config/constants' class Challenges extends Component { constructor (props) { super(props) this.state = { - onlyMyProjects: true + onlyMyProjects: true, + projectAccessDenied: false } } - componentDidMount () { + async componentDidMount () { const { dashboard, activeProjectId, @@ -39,8 +41,16 @@ class Challenges extends Component { menu, projectId, selfService, - loadChallengeTypes + loadChallengeTypes, + warnMessage } = this.props + + // If we were rendered specifically to display a validation/warning message, + // do not load any project/challenge data. + if (warnMessage) { + return + } + loadChallengeTypes() if (dashboard) { this.props.loadProjects('', {}) @@ -51,13 +61,46 @@ class Challenges extends Component { } else if (projectId || selfService) { if (projectId && projectId !== -1) { window.localStorage.setItem('projectLoading', 'true') - this.props.loadProject(projectId) + + // For direct `/projects/:projectId/*` navigation, block unauthorized users. + if (menu !== 'NULL') { + try { + await this.props.loadProject(projectId) + } catch (error) { + const responseStatus = _.get( + error, + 'payload.response.status', + _.get(error, 'response.status') + ) + + if (`${responseStatus}` === '403') { + this.setState({ projectAccessDenied: true }) + this.props.clearProjectDetail() + window.localStorage.removeItem('projectLoading') + return + } + } + } else { + this.props.loadProject(projectId) + } + } + + // Only load challenge listing after successful project resolution. + if (!this.state.projectAccessDenied) { + this.reloadChallenges(this.props, true) } - this.reloadChallenges(this.props, true) } } componentDidUpdate () { + if (this.state.projectAccessDenied) { + return + } + + if (this.props.warnMessage) { + return + } + const { auth } = this.props if (checkIsUserInvitedToProject(auth.token, this.props.projectDetail)) { @@ -66,6 +109,9 @@ class Challenges extends Component { } componentWillReceiveProps (nextProps) { + if (this.state.projectAccessDenied) { + return + } if ( (nextProps.dashboard && this.props.dashboard !== nextProps.dashboard) || this.props.activeProjectId !== nextProps.activeProjectId @@ -149,19 +195,24 @@ class Challenges extends Component { metadata, fetchNextProjects } = this.props + + const { projectAccessDenied } = this.state + const effectiveWarnMessage = projectAccessDenied + ? PROJECT_ACCESS_DENIED_MESSAGE + : warnMessage const { challengeTypes = [] } = metadata const isActiveProjectLoaded = reduxProjectInfo && `${reduxProjectInfo.id}` === `${activeProjectId}` return ( - {(dashboard || activeProjectId !== -1 || selfService) && ( + {(dashboard || activeProjectId !== -1 || selfService || effectiveWarnMessage) && ( { const projectId = _.get(match, 'params.projectId') const [resolvedProjectId, setResolvedProjectId] = useState(null) + const [projectAccessDenied, setProjectAccessDenied] = useState(false) useEffect(() => { let isActive = true @@ -34,22 +39,33 @@ const ProjectEntry = ({ } setResolvedProjectId(null) + setProjectAccessDenied(false) loadOnlyProjectInfo(projectId) .then(() => { if (isActive) { setResolvedProjectId(projectId) } }) - .catch(() => { + .catch((error) => { if (isActive) { - history.replace('/projects') + const responseStatus = _.get( + error, + 'payload.response.status', + _.get(error, 'response.status') + ) + if (`${responseStatus}` === '403') { + clearProjectDetail() + setProjectAccessDenied(true) + } else { + history.replace('/projects') + } } }) return () => { isActive = false } - }, [history, loadOnlyProjectInfo, projectId]) + }, [history, loadOnlyProjectInfo, projectId, clearProjectDetail]) useEffect(() => { if ( @@ -67,6 +83,15 @@ const ProjectEntry = ({ history.replace(destination) }, [history, isProjectLoading, projectDetail, resolvedProjectId, token]) + if (projectAccessDenied) { + return ( + + ) + } + return } @@ -76,6 +101,7 @@ ProjectEntry.propTypes = { }).isRequired, isProjectLoading: PropTypes.bool, loadOnlyProjectInfo: PropTypes.func.isRequired, + clearProjectDetail: PropTypes.func.isRequired, match: PropTypes.shape({ params: PropTypes.shape({ projectId: PropTypes.string @@ -92,7 +118,8 @@ const mapStateToProps = ({ auth, projects }) => ({ }) const mapDispatchToProps = { - loadOnlyProjectInfo + loadOnlyProjectInfo, + clearProjectDetail } export default withRouter(