diff --git a/src/apps/work/src/lib/utils/permissions.utils.spec.ts b/src/apps/work/src/lib/utils/permissions.utils.spec.ts index 18be5fbff..b6c41a8fc 100644 --- a/src/apps/work/src/lib/utils/permissions.utils.spec.ts +++ b/src/apps/work/src/lib/utils/permissions.utils.spec.ts @@ -5,6 +5,7 @@ import type { Project } from '../models' import { canCreateEngagement, canViewAllEngagements, + checkCanEditProjectDetails, checkCanManageProject, checkIsUserInvitedToProject, checkProjectAccess, @@ -41,6 +42,10 @@ describe('permissions.utils project management helpers', () => { role: 'customer', userId: 456, }, + { + role: 'copilot', + userId: 789, + }, ], name: 'Managed project', status: 'active', @@ -77,6 +82,20 @@ describe('permissions.utils project management helpers', () => { .toBe(false) }) + it('requires full access membership for project details edits', () => { + expect(checkCanEditProjectDetails(['Talent Manager'], '123', managedProject)) + .toBe(true) + expect(checkCanEditProjectDetails(['Talent Manager'], '789', managedProject)) + .toBe(false) + expect(checkCanEditProjectDetails(['Project Manager'], '456', managedProject)) + .toBe(false) + }) + + it('allows admins to edit project details without membership', () => { + expect(checkCanEditProjectDetails(['administrator'], '999', managedProject)) + .toBe(true) + }) + it('limits engagement creation to admins and talent managers', () => { expect(canCreateEngagement(['copilot'])) .toBe(false) diff --git a/src/apps/work/src/lib/utils/permissions.utils.ts b/src/apps/work/src/lib/utils/permissions.utils.ts index bc4776a0c..3b678598d 100644 --- a/src/apps/work/src/lib/utils/permissions.utils.ts +++ b/src/apps/work/src/lib/utils/permissions.utils.ts @@ -326,6 +326,36 @@ export function checkCanManageProject( || normalizedRole === PROJECT_ROLES.MANAGER } +/** + * Returns whether the caller can edit an existing project's core details. + * + * Admins always qualify. Non-admin callers must hold a manager-tier user role + * and the project's Full Access membership. Copilot membership is excluded so + * copilot users can keep other write access without changing project details. + * + * @param userRoles caller roles from the decoded auth token or app context. + * @param userId logged-in user identifier used for project membership checks. + * @param project project context for the edit check. + * @returns `true` when the caller can edit project details. + */ +export function checkCanEditProjectDetails( + userRoles: string[], + userId: number | string | undefined, + project: Project | undefined, +): boolean { + if (hasAdminRole(userRoles)) { + return true + } + + if (!project || !hasManagerRole(userRoles)) { + return false + } + + const normalizedRole = normalizeValue(getProjectMemberByUserId(project, userId)?.role) + + return normalizedRole === PROJECT_ROLES.MANAGER +} + export function checkAdminOrPmOrTaskManager( token: string, project?: Project, diff --git a/src/apps/work/src/pages/projects/ProjectEditorPage/ProjectEditorPage.tsx b/src/apps/work/src/pages/projects/ProjectEditorPage/ProjectEditorPage.tsx index 9c1623e06..7e399a1ef 100644 --- a/src/apps/work/src/pages/projects/ProjectEditorPage/ProjectEditorPage.tsx +++ b/src/apps/work/src/pages/projects/ProjectEditorPage/ProjectEditorPage.tsx @@ -26,7 +26,10 @@ import { ErrorMessage, LoadingSpinner, } from '../../../lib/components' -import { checkCanManageProject } from '../../../lib/utils' +import { + checkCanEditProjectDetails, + checkCanManageProject, +} from '../../../lib/utils' import { ProjectEditorForm, @@ -119,7 +122,7 @@ export const ProjectEditorPage: FC = () => { const canCreateProject = checkCanManageProject(userRoles, loginUserInfo?.userId) const canManageProject = !!projectResult.project - && checkCanManageProject(userRoles, loginUserInfo?.userId, projectResult.project) + && checkCanEditProjectDetails(userRoles, loginUserInfo?.userId, projectResult.project) const shouldRedirect = shouldRedirectToProjects( isEdit, canCreateProject, diff --git a/src/apps/work/src/pages/projects/ProjectsListPage/ProjectsListPage.spec.tsx b/src/apps/work/src/pages/projects/ProjectsListPage/ProjectsListPage.spec.tsx index 79f069146..47e18f6be 100644 --- a/src/apps/work/src/pages/projects/ProjectsListPage/ProjectsListPage.spec.tsx +++ b/src/apps/work/src/pages/projects/ProjectsListPage/ProjectsListPage.spec.tsx @@ -82,6 +82,8 @@ jest.mock('../../../lib/constants', () => ({ PROJECTS_PAGE_SIZE: 10, })) jest.mock('../../../lib/utils', () => ({ + checkCanEditProjectDetails: + jest.requireActual('../../../lib/utils/permissions.utils').checkCanEditProjectDetails, checkCanManageProject: jest.requireActual('../../../lib/utils/permissions.utils').checkCanManageProject, })) diff --git a/src/apps/work/src/pages/projects/ProjectsListPage/ProjectsListPage.tsx b/src/apps/work/src/pages/projects/ProjectsListPage/ProjectsListPage.tsx index ce59849a6..56af8d257 100644 --- a/src/apps/work/src/pages/projects/ProjectsListPage/ProjectsListPage.tsx +++ b/src/apps/work/src/pages/projects/ProjectsListPage/ProjectsListPage.tsx @@ -35,7 +35,10 @@ import { ProjectFilters, WorkAppContextModel, } from '../../../lib/models' -import { checkCanManageProject } from '../../../lib/utils' +import { + checkCanEditProjectDetails, + checkCanManageProject, +} from '../../../lib/utils' import styles from '../../../lib/components/ProjectsListPage/ProjectsListPage.module.scss' const DEFAULT_FILTERS: ProjectFilters = { @@ -127,7 +130,7 @@ export const ProjectsListPage: FC = () => { const canCreateProject = checkCanManageProject(userRoles, loginUserInfo?.userId) const canEditProject = useCallback( - (project: Project): boolean => checkCanManageProject(userRoles, loginUserInfo?.userId, project), + (project: Project): boolean => checkCanEditProjectDetails(userRoles, loginUserInfo?.userId, project), [loginUserInfo?.userId, userRoles], )