Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/components/ChallengesComponent/ChallengeList/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,9 @@ class ChallengeList extends Component {
const isMemberOfActiveProject = activeProject && activeProject.members && activeProject.members.some(member => member.userId === loginUserId)

if (warnMessage) {
return <Message warnMessage={warnMessage} />
const isLinkComponent = warnMessage.includes('{linkComponent}')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The use of warnMessage.includes('{linkComponent}') and warnMessage.replace('{linkComponent}', '') could lead to unexpected behavior if warnMessage contains multiple instances of {linkComponent}. Consider using a more robust method to ensure only the intended placeholder is replaced.

const message = isLinkComponent ? warnMessage.replace('{linkComponent}', '') : warnMessage
return <Message warnMessage={message} linkComponent={isLinkComponent ? <a href="mailto:support@topcoder.com">support@topcoder.com</a> : null} />
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 maintainability]
The inline creation of the <a> element for linkComponent could be extracted into a separate function or component to improve readability and maintainability.

}

const statusOptions = _.map(CHALLENGE_STATUS, item => ({
Expand Down
11 changes: 7 additions & 4 deletions src/components/ChallengesComponent/Message/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className={styles.messageContainer}>
<p>{warnMessage}</p>
{warnMessage}
{linkComponent}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
Rendering linkComponent directly without invoking it as a function may lead to unexpected behavior if linkComponent is not a valid React component or returns something other than a React element. Consider invoking it as a function: {linkComponent()}.

</div>
)
}

Message.defaultProps = {
warnMessage: ''
warnMessage: '',
linkComponent: () => null
}

Message.propTypes = {
warnMessage: PropTypes.string
warnMessage: PropTypes.string,
linkComponent: PropTypes.func
}

export default Message
2 changes: 2 additions & 0 deletions src/config/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 maintainability]
Consider using a template literal for PROJECT_ACCESS_DENIED_MESSAGE to improve readability and maintainability, especially if {linkComponent} is intended to be replaced dynamically.


/**
* Challenge cancel reasons
*/
Expand Down
70 changes: 61 additions & 9 deletions src/containers/Challenges/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,43 @@ import {
deleteChallenge,
loadChallengeTypes
} from '../../actions/challenges'
import { loadProject, loadProjects, updateProject } from '../../actions/projects'
import { loadProject, loadProjects, updateProject, clearProjectDetail } from '../../actions/projects'
import {
loadNextProjects,
setActiveProject,
resetSidebarActiveParams
} 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,
resetSidebarActiveParams,
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('', {})
Expand All @@ -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(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The error handling logic here assumes that the error object will always have a payload or response property with a status. Consider adding a default case or additional checks to handle unexpected error structures to prevent potential runtime errors.

error,
'payload.response.status',
_.get(error, 'response.status')
)

if (`${responseStatus}` === '403') {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 readability]
The comparison to check if responseStatus is '403' is done using string interpolation. Consider using a direct comparison with the number 403 for clarity and to avoid potential issues with type coercion.

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)) {
Expand All @@ -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
Expand Down Expand Up @@ -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 (
<Fragment>
{(dashboard || activeProjectId !== -1 || selfService) && (
{(dashboard || activeProjectId !== -1 || selfService || effectiveWarnMessage) && (
<ChallengesComponent
activeProject={{
...(isActiveProjectLoaded ? reduxProjectInfo : {})
}}
fetchNextProjects={fetchNextProjects}
warnMessage={warnMessage}
warnMessage={effectiveWarnMessage}
setActiveProject={setActiveProject}
dashboard={dashboard}
challenges={challenges}
Expand Down Expand Up @@ -279,7 +330,8 @@ const mapDispatchToProps = {
setActiveProject,
partiallyUpdateChallengeDetails,
deleteChallenge,
loadProjects
loadProjects,
clearProjectDetail
}

export default withRouter(
Expand Down
35 changes: 31 additions & 4 deletions src/containers/ProjectEntry/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import _ from 'lodash'
import Loader from '../../components/Loader'
import { loadOnlyProjectInfo } from '../../actions/projects'
import { checkIsUserInvitedToProject } from '../../util/tc'
import Challenges from '../Challenges'
import { clearProjectDetail } from '../../actions/projects'
import { PROJECT_ACCESS_DENIED_MESSAGE } from '../../config/constants'

/**
* Resolves the correct project landing route for `/projects/:projectId`.
Expand All @@ -18,12 +21,14 @@ const ProjectEntry = ({
history,
isProjectLoading,
loadOnlyProjectInfo,
clearProjectDetail,
match,
projectDetail,
token
}) => {
const projectId = _.get(match, 'params.projectId')
const [resolvedProjectId, setResolvedProjectId] = useState(null)
const [projectAccessDenied, setProjectAccessDenied] = useState(false)

useEffect(() => {
let isActive = true
Expand All @@ -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') {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Using string interpolation to compare responseStatus with '403' is unnecessary and could lead to subtle bugs if the type of responseStatus changes. Consider using a direct comparison: responseStatus === 403.

clearProjectDetail()
setProjectAccessDenied(true)
} else {
history.replace('/projects')
}
}
})

return () => {
isActive = false
}
}, [history, loadOnlyProjectInfo, projectId])
}, [history, loadOnlyProjectInfo, projectId, clearProjectDetail])

useEffect(() => {
if (
Expand All @@ -67,6 +83,15 @@ const ProjectEntry = ({
history.replace(destination)
}, [history, isProjectLoading, projectDetail, resolvedProjectId, token])

if (projectAccessDenied) {
return (
<Challenges
menu='NULL'
warnMessage={PROJECT_ACCESS_DENIED_MESSAGE}
/>
)
}

return <Loader />
}

Expand All @@ -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
Expand All @@ -92,7 +118,8 @@ const mapStateToProps = ({ auth, projects }) => ({
})

const mapDispatchToProps = {
loadOnlyProjectInfo
loadOnlyProjectInfo,
clearProjectDetail
}

export default withRouter(
Expand Down
Loading