Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
caf0a6a
fix: prevent download for submitters if challenge is configured that way
hentrymartin Dec 15, 2025
ebce38b
Merge branch 'dev' into pm-2662_4
hentrymartin Dec 18, 2025
caeaa4f
fix: review comments
hentrymartin Dec 18, 2025
9f4747a
Merge branch 'dev' into pm-2662_4
hentrymartin Jan 9, 2026
6b554a4
PM-3717 - map challenge role to clear labels
vas3a Feb 10, 2026
7e2e4cf
lint
vas3a Feb 10, 2026
0177659
Merge pull request #1467 from topcoder-platform/PM-3717_verified-skil…
vas3a Feb 10, 2026
30b31f9
Merge pull request #1385 from topcoder-platform/pm-2662_4
hentrymartin Feb 11, 2026
5bbdaa5
Hide emails except if the user is viewing their own profile (PS-526)
jmgasper Feb 11, 2026
b9c899a
Merge branch 'dev' of github.com:topcoder-platform/platform-ui into dev
jmgasper Feb 11, 2026
affce2a
Allow talent manager, product manager, and admins ability to see emai…
jmgasper Feb 11, 2026
95e3658
PM-3825 #time 1h - make sure we properly restrict visibility over use…
vas3a Feb 12, 2026
1b468fa
Merge pull request #1468 from topcoder-platform/PM-3825_ensure-profil…
vas3a Feb 12, 2026
eeccab3
PM-3825 #time 30min profile privac
vas3a Feb 13, 2026
549ccd4
Merge pull request #1472 from topcoder-platform/PM-3825_ensure-profil…
vas3a Feb 13, 2026
ef88f60
PM-3825 #15m compare lowercase
vas3a Feb 13, 2026
e20e595
hide contact section for copilot & pm
vas3a Feb 13, 2026
df4060e
hide phone & email for copilot & PM
vas3a Feb 13, 2026
7743fcf
improve readability
vas3a Feb 13, 2026
aa72957
lint
vas3a Feb 13, 2026
967457d
Merge pull request #1473 from topcoder-platform/PM-3825_ensure-profil…
vas3a Feb 13, 2026
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
7 changes: 5 additions & 2 deletions src/apps/profiles/src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export const ADMIN_ROLES = [UserRole.administrator]
export const PHONE_NUMBER_ROLES = [
UserRole.administrator,
UserRole.talentManager,
UserRole.projectManager,
UserRole.copilot,
]

export const EMAIL_VIEW_ROLES = [
UserRole.administrator,
UserRole.talentManager,
]
46 changes: 34 additions & 12 deletions src/apps/profiles/src/lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { UserProfile, UserRole, UserTrait } from '~/libs/core'
import { availabilityOptions, preferredRoleOptions } from '~/libs/shared/lib/components/modify-open-to-work-modal'

import { ADMIN_ROLES, PHONE_NUMBER_ROLES } from '../config'
import { ADMIN_ROLES, EMAIL_VIEW_ROLES, PHONE_NUMBER_ROLES } from '../config'

declare global {
interface Window { tcUniNav: any }
Expand Down Expand Up @@ -145,17 +145,13 @@ export function canDownloadProfile(authProfile: UserProfile | undefined, profile
return true
}

// Check if user has admin roles
if (authProfile.roles?.some(role => ADMIN_ROLES.includes(role.toLowerCase() as UserRole))) {
return true
}

// Check if user has PM or Talent Manager roles
const allowedRoles = ['Project Manager', 'Talent Manager']
if (authProfile
.roles?.some(
role => allowedRoles.some(allowed => role.toLowerCase() === allowed.toLowerCase()),
)
// Check if user has admin roles or talent manager
if (
authProfile.roles?.some(role => [

Choose a reason for hiding this comment

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

[⚠️ correctness]
The use of role.toLowerCase() assumes that all roles are case-insensitive. If UserRole.talentManager or any role in ADMIN_ROLES is case-sensitive, this could lead to incorrect behavior. Consider ensuring that all roles are consistently cased or explicitly handle case sensitivity.

UserRole.talentManager,
...ADMIN_ROLES,
].map(r => r.toLowerCase())

Choose a reason for hiding this comment

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

[⚠️ performance]
The use of map followed by includes could be optimized by using some with a case-insensitive comparison directly. This avoids creating an intermediate array and improves performance slightly.

.includes(role.toLowerCase() as UserRole))
) {
return true
}
Expand Down Expand Up @@ -193,6 +189,32 @@ export function canSeePhones(authProfile: UserProfile | undefined, profile: User
return false
}

/**
* Check if the user can see email address
* @param authProfile - The authenticated user profile
* @param profile - The profile to check if the user can see email
* @returns {boolean} - Whether the user can see email
*/
export function canSeeEmail(authProfile: UserProfile | undefined, profile: UserProfile): boolean {
if (!authProfile) {
return false
}

if (authProfile.handle === profile.handle) {
return true
}

if (authProfile
.roles?.some(
role => EMAIL_VIEW_ROLES.some(allowed => role.toLowerCase() === allowed.toLowerCase()),

Choose a reason for hiding this comment

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

[💡 performance]
Consider using includes instead of some for checking role membership, as it can improve readability and performance slightly when checking for a single role match.

)
) {
return true
}

return false
}

export function getAvailabilityLabel(value?: string): string | undefined {
return availabilityOptions.find(o => o.value === value)?.label
}
Expand Down
11 changes: 9 additions & 2 deletions src/apps/profiles/src/member-profile/phones/Phones.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { CopyButton } from '~/apps/admin/src/lib/components/CopyButton'
import { IconOutline, IconSolid, Tooltip } from '~/libs/ui'

import { AddButton } from '../../components'
import { canSeePhones } from '../../lib/helpers'
import { canSeeEmail, canSeePhones } from '../../lib/helpers'

import { ModifyPhonesModal } from './ModifyPhonesModal'
import { PhoneCard } from './PhoneCard'
Expand All @@ -21,6 +21,7 @@ interface PhonesProps {
const Phones: FC<PhonesProps> = (props: PhonesProps) => {
const canEdit: boolean = props.authProfile?.handle === props.profile.handle
const canSeePhonesValue: boolean = canSeePhones(props.authProfile, props.profile)
const canSeeEmailValue: boolean = canSeeEmail(props.authProfile, props.profile)

Choose a reason for hiding this comment

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

[❗❗ security]
The canSeeEmail function is newly introduced here. Ensure that this function is properly tested to verify that it correctly determines email visibility based on the provided profiles. This is important to prevent unauthorized access to sensitive information.


const [isEditMode, setIsEditMode]: [boolean, Dispatch<SetStateAction<boolean>>]
= useState<boolean>(false)
Expand Down Expand Up @@ -50,6 +51,12 @@ const Phones: FC<PhonesProps> = (props: PhonesProps) => {
}, 1000)
}

// Don't render anything if user cannot edit AND cannot see any contact info
const hasContactInfo = props.profile?.email || phones.length > 0
if (!canEdit && (!canSeeEmailValue || !hasContactInfo)) {

Choose a reason for hiding this comment

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

[❗❗ correctness]
The condition !canSeeEmailValue || !hasContactInfo might not work as intended if the user can see the email but there is no contact info. Consider changing the condition to !canSeeEmailValue && !hasContactInfo to ensure that the component is only hidden when both conditions are false.

return <></>
}

return (
<div className={styles.container}>
<div className={styles.titleWrap}>
Expand All @@ -61,7 +68,7 @@ const Phones: FC<PhonesProps> = (props: PhonesProps) => {
</Tooltip>
)}
</p>
{props.profile?.email && (
{canSeeEmailValue && props.profile?.email && (
<div className={styles.email}>
<div className={styles.emailIcon}>
<IconSolid.MailIcon width={20} height={20} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ const ProfileHeader: FC<ProfileHeaderProps> = (props: ProfileHeaderProps) => {
= !canEdit
&& (
roles.includes(UserRole.administrator)
|| roles.includes(UserRole.projectManager)
|| roles.includes(UserRole.talentManager)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import type { SubmissionHistoryPartition } from '../../utils'
import { TABLE_DATE_FORMAT } from '../../../config/index.config'
import { CollapsibleAiReviewsRow } from '../CollapsibleAiReviewsRow'
import { useRolePermissions, UseRolePermissionsResult } from '../../hooks'
import { SUBMISSION_DOWNLOAD_RESTRICTION_MESSAGE } from '../../constants'

import styles from './TabContentSubmissions.module.scss'

Expand Down Expand Up @@ -240,24 +241,13 @@ export const TabContentSubmissions: FC<Props> = props => {

const filteredSubmissions = useMemo<BackendSubmission[]>(
() => {

const filterFunc = (submissions: BackendSubmission[]): BackendSubmission[] => submissions
.filter(submission => {
if (!canViewSubmissions) {
return String(submission.memberId) === String(loginUserInfo?.userId)
}

return true
})
const filteredByUserId = filterFunc(latestBackendSubmissions)
const filteredByUserIdSubmissions = filterFunc(props.submissions)
if (restrictToLatest && hasLatestFlag) {
return latestBackendSubmissions.length
? filteredByUserId
: filteredByUserIdSubmissions
? latestBackendSubmissions
: props.submissions
}

return filteredByUserIdSubmissions
return props.submissions
},
[
latestBackendSubmissions,
Expand Down Expand Up @@ -288,13 +278,21 @@ export const TabContentSubmissions: FC<Props> = props => {
? undefined
: submission.virusScan
const failedScan = normalizedVirusScan === false
const isRestricted = isRestrictedBase || failedScan
const tooltipMessage = failedScan
const cannotDownloadSubmission = (
!canViewSubmissions && String(submission.memberId) === String(loginUserInfo?.userId)
)
const isRestricted = isRestrictedBase || failedScan || !cannotDownloadSubmission

Choose a reason for hiding this comment

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

[❗❗ correctness]
The logic for isRestricted seems incorrect. The condition !cannotDownloadSubmission should be cannotDownloadSubmission to ensure that the restriction is applied when the user cannot download the submission.

let tooltipMessage = failedScan
? VIRUS_SCAN_FAILED_MESSAGE
: (
getRestrictionMessageForMember(submission.memberId)
?? restrictionMessage
)

if (!cannotDownloadSubmission) {
tooltipMessage = SUBMISSION_DOWNLOAD_RESTRICTION_MESSAGE
}

const isButtonDisabled = Boolean(
props.isDownloading[submission.id]
|| isRestricted,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { useWindowSize, WindowSize } from '~/libs/shared'
import { Table, TableColumn } from '~/libs/ui'

import { WITHOUT_APPEAL } from '../../../config/index.config'
import { ChallengeDetailContext } from '../../contexts'
import { ChallengeDetailContext, ReviewAppContext } from '../../contexts'
import { useSubmissionDownloadAccess } from '../../hooks/useSubmissionDownloadAccess'
import type { UseSubmissionDownloadAccessResult } from '../../hooks/useSubmissionDownloadAccess'
import { useRolePermissions } from '../../hooks/useRolePermissions'
Expand All @@ -22,6 +22,7 @@ import {
ChallengeDetailContextModel,
ChallengeInfo,
MappingReviewAppeal,
ReviewAppContextModel,
SubmissionInfo,
} from '../../models'
import { TableWrapper } from '../TableWrapper'
Expand Down Expand Up @@ -70,7 +71,7 @@ export const TableAppeals: FC<TableAppealsProps> = (props: TableAppealsProps) =>
reviewers,
}: ChallengeDetailContextModel = useContext(ChallengeDetailContext)
const { width: screenWidth }: WindowSize = useWindowSize()

const { loginUserInfo }: ReviewAppContextModel = useContext(ReviewAppContext)
const downloadAccess: UseSubmissionDownloadAccessResult = useSubmissionDownloadAccess()
const {
getRestrictionMessageForMember,
Expand Down Expand Up @@ -219,13 +220,45 @@ export const TableAppeals: FC<TableAppealsProps> = (props: TableAppealsProps) =>
[aggregatedResults],
)

const { canViewAllSubmissions }: UseRolePermissionsResult = useRolePermissions()

const isCompletedDesignChallenge = useMemo(() => {

Choose a reason for hiding this comment

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

[💡 readability]
The isCompletedDesignChallenge function could be simplified by directly returning the boolean expression without the if statement. This would improve readability.

if (!challengeInfo) return false
const type = challengeInfo.track.name ? String(challengeInfo.track.name)
.toLowerCase() : ''
const status = challengeInfo.status ? String(challengeInfo.status)
.toLowerCase() : ''
return type === 'design' && (
status === 'completed'
)
}, [challengeInfo])

const isSubmissionsViewable = useMemo(() => {

Choose a reason for hiding this comment

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

[⚠️ correctness]
The isSubmissionsViewable function uses some to check metadata, which is efficient. However, ensure that metadata is always an array to avoid potential runtime errors.

if (!challengeInfo?.metadata?.length) return false
return challengeInfo.metadata.some(m => m.name === 'submissionsViewable' && String(m.value)
.toLowerCase() === 'true')
}, [challengeInfo])

const canViewSubmissions = useMemo(() => {
if (isCompletedDesignChallenge) {
return canViewAllSubmissions || isSubmissionsViewable
}

return true
}, [isCompletedDesignChallenge, isSubmissionsViewable, canViewAllSubmissions])

const isSubmissionNotViewable = (submission: SubmissionRow): boolean => (

Choose a reason for hiding this comment

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

[⚠️ correctness]
The isSubmissionNotViewable function compares memberId and userId as strings. Ensure these values are always strings to prevent unexpected behavior.

!canViewSubmissions && String(submission.memberId) !== String(loginUserInfo?.userId)
)

const downloadButtonConfig = useMemo<DownloadButtonConfig>(
() => ({
downloadSubmission,
getRestrictionMessageForMember,
isDownloading,
isSubmissionDownloadRestricted,
isSubmissionDownloadRestrictedForMember,
isSubmissionNotViewable,
ownedMemberIds,
restrictionMessage,
shouldRestrictSubmitterToOwnSubmission: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ import {
import { ChallengeDetailContext, ReviewAppContext } from '../../contexts'
import { updateReview } from '../../services'
import { ConfirmModal } from '../ConfirmModal'
import { useSubmissionDownloadAccess } from '../../hooks'
import { useRolePermissions, UseRolePermissionsResult, useSubmissionDownloadAccess } from '../../hooks'
import type { UseSubmissionDownloadAccessResult } from '../../hooks/useSubmissionDownloadAccess'
import { CollapsibleAiReviewsRow } from '../CollapsibleAiReviewsRow'
import { SUBMISSION_DOWNLOAD_RESTRICTION_MESSAGE } from '../../constants'

import styles from './TableCheckpointSubmissions.module.scss'

Expand Down Expand Up @@ -65,6 +66,7 @@ export const TableCheckpointSubmissions: FC<Props> = (props: Props) => {
const datas: Screening[] | undefined = props.datas
const downloadSubmission = props.downloadSubmission
const isDownloading = props.isDownloading
const { canViewAllSubmissions }: UseRolePermissionsResult = useRolePermissions()

const {
challengeInfo,
Expand Down Expand Up @@ -115,6 +117,31 @@ export const TableCheckpointSubmissions: FC<Props> = (props: Props) => {
getRestrictionMessageForMember,
}: UseSubmissionDownloadAccessResult = useSubmissionDownloadAccess()

const isCompletedDesignChallenge = useMemo(() => {

Choose a reason for hiding this comment

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

[⚠️ performance]
The use of useMemo here is appropriate for optimizing performance by memoizing the result of the computation. However, ensure that challengeInfo is a stable reference or consider using a deep comparison to avoid unnecessary recalculations.

if (!challengeInfo) return false
const type = challengeInfo.track.name ? String(challengeInfo.track.name)
.toLowerCase() : ''
const status = challengeInfo.status ? String(challengeInfo.status)
.toLowerCase() : ''
return type === 'design' && (
status === 'completed'
)
}, [challengeInfo])

const isSubmissionsViewable = useMemo(() => {

Choose a reason for hiding this comment

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

[⚠️ performance]
The useMemo hook is used to memoize the result of checking if submissions are viewable. Ensure that challengeInfo.metadata is a stable reference or consider using a deep comparison to avoid unnecessary recalculations.

if (!challengeInfo?.metadata?.length) return false
return challengeInfo.metadata.some(m => m.name === 'submissionsViewable' && String(m.value)
.toLowerCase() === 'true')
}, [challengeInfo])

const canViewSubmissions = useMemo(() => {

Choose a reason for hiding this comment

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

[⚠️ performance]
The useMemo hook is used to determine if submissions can be viewed. Ensure that all dependencies (isCompletedDesignChallenge, isSubmissionsViewable, canViewAllSubmissions) are stable references or consider using a deep comparison to avoid unnecessary recalculations.

if (isCompletedDesignChallenge) {
return canViewAllSubmissions || isSubmissionsViewable
}

return true
}, [isCompletedDesignChallenge, isSubmissionsViewable, canViewAllSubmissions])

const openReopenDialog = useCallback(
(entry: Screening, isOwnReview: boolean): void => {
if (!entry.reviewId) {
Expand Down Expand Up @@ -211,10 +238,18 @@ export const TableCheckpointSubmissions: FC<Props> = (props: Props) => {
? undefined
: data.virusScan
const failedScan = normalizedVirusScan === false
const isRestrictedForRow = isRestrictedBase || failedScan
const tooltipMessage = failedScan
const isDownloadDisabled = (
!canViewSubmissions && String(data.memberId) !== String(loginUserInfo?.userId)

Choose a reason for hiding this comment

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

[💡 readability]
The condition String(data.memberId) !== String(loginUserInfo?.userId) could be simplified by ensuring data.memberId and loginUserInfo?.userId are of the same type before comparison, which would improve readability and potentially avoid unnecessary type conversion.

)
const isRestrictedForRow = isRestrictedBase || failedScan || isDownloadDisabled
let tooltipMessage = failedScan
? 'Submission failed virus scan'
: (getRestrictionMessageForMember(data.memberId) ?? restrictionMessage)

if (isDownloadDisabled) {
tooltipMessage = SUBMISSION_DOWNLOAD_RESTRICTION_MESSAGE
}

const isButtonDisabled = Boolean(
isDownloading[data.submissionId]
|| isRestrictedForRow,
Expand Down
Loading
Loading