From 3953e784e3d67e0dccf44ecc4aa04053c516085a Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Thu, 19 Mar 2026 10:38:23 +0200 Subject: [PATCH] Fix 4393 --- src/containers/Challenges/index.js | 21 ++++++++++++++++-- src/containers/ProjectEntry/index.js | 33 +++++++++++++++++++++------- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/containers/Challenges/index.js b/src/containers/Challenges/index.js index 871f078b..6ba9c1c9 100644 --- a/src/containers/Challenges/index.js +++ b/src/containers/Challenges/index.js @@ -20,7 +20,7 @@ import { setActiveProject, resetSidebarActiveParams } from '../../actions/sidebar' -import { checkAdmin, checkIsUserInvitedToProject } from '../../util/tc' +import { checkAdmin, checkCopilot, checkIsProjectMember, checkIsUserInvitedToProject } from '../../util/tc' import { withRouter } from 'react-router-dom' class Challenges extends Component { @@ -125,8 +125,10 @@ class Challenges extends Component { filterProjectOption, projects, activeProjectId, + projectId, status, projectDetail: reduxProjectInfo, + hasProjectAccess, loadChallengesByPage, page, perPage, @@ -153,6 +155,19 @@ class Challenges extends Component { const isActiveProjectLoaded = reduxProjectInfo && `${reduxProjectInfo.id}` === `${activeProjectId}` + // Check if user has access to this specific project + const isProjectRoute = !!projectId && !dashboard && !selfService + const isProjectDetailLoaded = reduxProjectInfo && `${reduxProjectInfo.id}` === `${projectId}` + const isAdmin = checkAdmin(auth.token) + const isCopilot = checkCopilot(auth.token) + const isProjectMember = isProjectDetailLoaded && checkIsProjectMember(auth.token, reduxProjectInfo) + const canAccessProject = isAdmin || isCopilot || isProjectMember || hasProjectAccess + + // Show access denied message if user cannot access this project + const accessDeniedMessage = isProjectRoute && isProjectDetailLoaded && !canAccessProject + ? "You don't have access to this project. Please contact support@topcoder.com." + : warnMessage + return ( {(dashboard || activeProjectId !== -1 || selfService) && ( @@ -161,7 +176,7 @@ class Challenges extends Component { ...(isActiveProjectLoaded ? reduxProjectInfo : {}) }} fetchNextProjects={fetchNextProjects} - warnMessage={warnMessage} + warnMessage={accessDeniedMessage} setActiveProject={setActiveProject} dashboard={dashboard} challenges={challenges} @@ -210,6 +225,7 @@ Challenges.propTypes = { menu: PropTypes.string, challenges: PropTypes.arrayOf(PropTypes.object), projectDetail: PropTypes.object, + hasProjectAccess: PropTypes.bool, isLoading: PropTypes.bool, loadChallengesByPage: PropTypes.func, loadProject: PropTypes.func.isRequired, @@ -256,6 +272,7 @@ const mapStateToProps = ({ challenges, sidebar, projects, auth }) => ({ activeProjectId: sidebar.activeProjectId, projects: sidebar.projects, projectDetail: projects.projectDetail, + hasProjectAccess: projects.hasProjectAccess, isBillingAccountExpired: projects.isBillingAccountExpired, billingStartDate: projects.billingStartDate, billingEndDate: projects.billingEndDate, diff --git a/src/containers/ProjectEntry/index.js b/src/containers/ProjectEntry/index.js index 1215b497..24d1ac3b 100644 --- a/src/containers/ProjectEntry/index.js +++ b/src/containers/ProjectEntry/index.js @@ -15,6 +15,7 @@ import { checkIsUserInvitedToProject } from '../../util/tc' * the invitation modal before challenge-specific requests run. */ const ProjectEntry = ({ + hasProjectAccess, history, isProjectLoading, loadOnlyProjectInfo, @@ -24,6 +25,7 @@ const ProjectEntry = ({ }) => { const projectId = _.get(match, 'params.projectId') const [resolvedProjectId, setResolvedProjectId] = useState(null) + const [accessDenied, setAccessDenied] = useState(false) useEffect(() => { let isActive = true @@ -34,15 +36,22 @@ const ProjectEntry = ({ } setResolvedProjectId(null) + setAccessDenied(false) loadOnlyProjectInfo(projectId) .then(() => { if (isActive) { setResolvedProjectId(projectId) } }) - .catch(() => { + .catch((error) => { if (isActive) { - history.replace('/projects') + const status = _.get(error, 'payload.response.status', _.get(error, 'response.status')) + if (status === 403) { + setAccessDenied(true) + setResolvedProjectId(projectId) + } else { + history.replace('/projects') + } } }) @@ -52,11 +61,17 @@ const ProjectEntry = ({ }, [history, loadOnlyProjectInfo, projectId]) useEffect(() => { - if ( - !resolvedProjectId || - isProjectLoading || - `${_.get(projectDetail, 'id', '')}` !== `${resolvedProjectId}` - ) { + if (!resolvedProjectId || isProjectLoading) { + return + } + + // Handle 403 access denied - redirect to challenges page which will show the error + if (accessDenied || !hasProjectAccess) { + history.replace(`/projects/${resolvedProjectId}/challenges`) + return + } + + if (`${_.get(projectDetail, 'id', '')}` !== `${resolvedProjectId}`) { return } @@ -65,12 +80,13 @@ const ProjectEntry = ({ : `/projects/${resolvedProjectId}/challenges` history.replace(destination) - }, [history, isProjectLoading, projectDetail, resolvedProjectId, token]) + }, [accessDenied, hasProjectAccess, history, isProjectLoading, projectDetail, resolvedProjectId, token]) return } ProjectEntry.propTypes = { + hasProjectAccess: PropTypes.bool, history: PropTypes.shape({ replace: PropTypes.func.isRequired }).isRequired, @@ -86,6 +102,7 @@ ProjectEntry.propTypes = { } const mapStateToProps = ({ auth, projects }) => ({ + hasProjectAccess: projects.hasProjectAccess, isProjectLoading: projects.isLoading, projectDetail: projects.projectDetail, token: auth.token