diff --git a/plane-src/apps/api/plane/app/permissions/project.py b/plane-src/apps/api/plane/app/permissions/project.py index 49a8cb2..86ba26b 100644 --- a/plane-src/apps/api/plane/app/permissions/project.py +++ b/plane-src/apps/api/plane/app/permissions/project.py @@ -34,7 +34,7 @@ class ProjectBasePermission(BasePermission): return WorkspaceMember.objects.filter( workspace__slug=view.workspace_slug, member=request.user, - role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value], + role=ROLE.ADMIN.value, is_active=True, ).exists() @@ -78,7 +78,7 @@ class ProjectMemberPermission(BasePermission): return WorkspaceMember.objects.filter( workspace__slug=view.workspace_slug, member=request.user, - role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value], + role=ROLE.ADMIN.value, is_active=True, ).exists() diff --git a/plane-src/apps/api/plane/app/views/project/base.py b/plane-src/apps/api/plane/app/views/project/base.py index 0a7378c..bec168c 100644 --- a/plane-src/apps/api/plane/app/views/project/base.py +++ b/plane-src/apps/api/plane/app/views/project/base.py @@ -25,6 +25,10 @@ from plane.app.serializers import ( from plane.app.views.base import BaseAPIView, BaseViewSet from plane.bgtasks.recent_visited_task import recent_visited_task from plane.bgtasks.webhook_task import model_activity, webhook_activity +from plane.authentication.nodedc_project_memberships import ( + ensure_project_admin_membership, + ensure_workspace_admin_project_memberships, +) from plane.db.models import ( UserFavorite, DeployBoard, @@ -49,6 +53,20 @@ class ProjectViewSet(BaseViewSet): webhook_event = "project" use_read_replica = True + def ensure_workspace_admin_project_access(self, request, slug): + workspace_member = ( + WorkspaceMember.objects.filter( + member=request.user, + workspace__slug=slug, + is_active=True, + role=ROLE.ADMIN.value, + ) + .select_related("workspace") + .first() + ) + if workspace_member is not None: + ensure_workspace_admin_project_memberships(workspace_member.workspace) + def get_queryset(self): sort_order = ProjectUserProperty.objects.filter( user=self.request.user, @@ -99,6 +117,7 @@ class ProjectViewSet(BaseViewSet): @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") def list_detail(self, request, slug): + self.ensure_workspace_admin_project_access(request, slug) fields = [field for field in request.GET.get("fields", "").split(",") if field] projects = self.get_queryset().order_by("sort_order", "name") if WorkspaceMember.objects.filter( @@ -119,11 +138,8 @@ class ProjectViewSet(BaseViewSet): role=ROLE.MEMBER.value, ).exists(): projects = projects.filter( - Q( - project_projectmember__member=self.request.user, - project_projectmember__is_active=True, - ) - | Q(network=2) + project_projectmember__member=self.request.user, + project_projectmember__is_active=True, ) if request.GET.get("per_page", False) and request.GET.get("cursor", False): @@ -139,6 +155,7 @@ class ProjectViewSet(BaseViewSet): @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") def list(self, request, slug): + self.ensure_workspace_admin_project_access(request, slug) sort_order = ProjectUserProperty.objects.filter( user=self.request.user, project_id=OuterRef("pk"), @@ -209,11 +226,8 @@ class ProjectViewSet(BaseViewSet): role=ROLE.MEMBER.value, ).exists(): projects = projects.filter( - Q( - project_projectmember__member=self.request.user, - project_projectmember__is_active=True, - ) - | Q(network=2) + project_projectmember__member=self.request.user, + project_projectmember__is_active=True, ) return Response(projects, status=status.HTTP_200_OK) @@ -227,7 +241,14 @@ class ProjectViewSet(BaseViewSet): member_ids = [str(project_member.member_id) for project_member in project.members_list] if str(request.user.id) not in member_ids: - if project.network == ProjectNetwork.SECRET.value: + if WorkspaceMember.objects.filter( + member=request.user, + workspace__slug=slug, + is_active=True, + role=ROLE.ADMIN.value, + ).exists(): + ensure_project_admin_membership(project, request.user) + elif project.network == ProjectNetwork.SECRET.value: return Response( {"error": "You do not have permission"}, status=status.HTTP_403_FORBIDDEN, @@ -249,7 +270,7 @@ class ProjectViewSet(BaseViewSet): serializer = ProjectListSerializer(project) return Response(serializer.data, status=status.HTTP_200_OK) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") + @allow_permission([ROLE.ADMIN], level="WORKSPACE") def create(self, request, slug): workspace = Workspace.objects.get(slug=slug) @@ -290,6 +311,7 @@ class ProjectViewSet(BaseViewSet): ) project = self.get_queryset().filter(pk=serializer.data["id"]).first() + ensure_workspace_admin_project_memberships(workspace, project=project) # Create the model activity model_activity.delay( diff --git a/plane-src/apps/api/plane/app/views/project/member.py b/plane-src/apps/api/plane/app/views/project/member.py index d3a0ec4..57fb3c2 100644 --- a/plane-src/apps/api/plane/app/views/project/member.py +++ b/plane-src/apps/api/plane/app/views/project/member.py @@ -11,6 +11,10 @@ from django.db.models import Min from .base import BaseViewSet, BaseAPIView from plane.app.realtime.issue_events import publish_assignee_cleanup_issue_events_on_commit from plane.app.realtime.nodedc_events import publish_nodedc_workspace_event_on_commit +from plane.authentication.nodedc_project_memberships import ( + ensure_project_admin_membership, + ensure_workspace_admin_project_memberships, +) from plane.authentication.nodedc_workspace_policy import ( is_nodedc_launcher_managed_workspace, nodedc_launcher_managed_workspace_response, @@ -431,12 +435,32 @@ class ProjectMemberViewSet(BaseViewSet): class ProjectMemberUserEndpoint(BaseAPIView): def get(self, request, slug, project_id): - project_member = ProjectMember.objects.get( + project_member = ProjectMember.objects.filter( project_id=project_id, workspace__slug=slug, member=request.user, is_active=True, - ) + ).first() + + if project_member is None: + project = Project.objects.filter(pk=project_id, workspace__slug=slug).select_related("workspace").first() + is_workspace_admin = WorkspaceMember.objects.filter( + workspace__slug=slug, + member=request.user, + is_active=True, + role=ROLE.ADMIN.value, + ).exists() + if project is not None and is_workspace_admin: + ensure_project_admin_membership(project, request.user) + project_member = ProjectMember.objects.filter( + project=project, + member=request.user, + is_active=True, + ).first() + + if project_member is None: + return Response({"error": "Project member not found"}, status=status.HTTP_404_NOT_FOUND) + serializer = ProjectMemberSerializer(project_member) return Response(serializer.data, status=status.HTTP_200_OK) @@ -447,6 +471,19 @@ class UserProjectRolesEndpoint(BaseAPIView): use_read_replica = True def get(self, request, slug): + workspace_member = ( + WorkspaceMember.objects.filter( + workspace__slug=slug, + member=request.user, + is_active=True, + ) + .select_related("workspace") + .first() + ) + + if workspace_member is not None and workspace_member.role == ROLE.ADMIN.value: + ensure_workspace_admin_project_memberships(workspace_member.workspace) + project_members = ProjectMember.objects.filter( workspace__slug=slug, member_id=request.user.id, diff --git a/plane-src/apps/api/plane/authentication/nodedc_project_memberships.py b/plane-src/apps/api/plane/authentication/nodedc_project_memberships.py new file mode 100644 index 0000000..18cbdce --- /dev/null +++ b/plane-src/apps/api/plane/authentication/nodedc_project_memberships.py @@ -0,0 +1,113 @@ +ADMIN_ROLE = 20 +AUTO_ADMIN_COMMENT = "nodedc:workspace-admin" +AUTO_ADMIN_CREATED_COMMENT = f"{AUTO_ADMIN_COMMENT}:created" +AUTO_ADMIN_PREVIOUS_PREFIX = f"{AUTO_ADMIN_COMMENT}:previous:" + + +def get_auto_admin_previous_role(comment): + if not isinstance(comment, str) or not comment.startswith(AUTO_ADMIN_PREVIOUS_PREFIX): + return None + + try: + previous_role = int(comment.replace(AUTO_ADMIN_PREVIOUS_PREFIX, "", 1)) + except ValueError: + return None + + return previous_role if previous_role in {5, 15, ADMIN_ROLE} else None + + +def ensure_project_admin_membership(project, user): + from plane.db.models import ProjectMember + + project_member = ProjectMember.objects.filter( + project=project, + member=user, + deleted_at__isnull=True, + ).first() + + if project_member is None: + ProjectMember.objects.create( + workspace=project.workspace, + project=project, + member=user, + role=ADMIN_ROLE, + is_active=True, + comment=AUTO_ADMIN_CREATED_COMMENT, + ) + return 1 + + update_fields = [] + if project_member.role != ADMIN_ROLE: + project_member.comment = f"{AUTO_ADMIN_PREVIOUS_PREFIX}{project_member.role}" + project_member.role = ADMIN_ROLE + update_fields.extend(["comment", "role"]) + if not project_member.is_active: + project_member.is_active = True + update_fields.append("is_active") + + if update_fields: + update_fields.append("updated_at") + project_member.save(update_fields=update_fields) + return 1 + + return 0 + + +def revoke_auto_project_admin_memberships(workspace, user): + from plane.db.models import ProjectMember + + revoked = 0 + project_memberships = ProjectMember.objects.filter( + project__workspace=workspace, + member=user, + role=ADMIN_ROLE, + deleted_at__isnull=True, + comment__startswith=AUTO_ADMIN_COMMENT, + ) + + for project_member in project_memberships: + previous_role = get_auto_admin_previous_role(project_member.comment) + if project_member.comment == AUTO_ADMIN_CREATED_COMMENT or previous_role is None: + project_member.is_active = False + project_member.save(update_fields=["is_active", "updated_at"]) + else: + project_member.role = previous_role + project_member.comment = None + project_member.is_active = True + project_member.save(update_fields=["role", "comment", "is_active", "updated_at"]) + revoked += 1 + + return revoked + + +def ensure_user_admin_project_memberships(workspace, user): + from plane.db.models import Project + + restored = 0 + for project in Project.objects.filter(workspace=workspace, deleted_at__isnull=True).select_related("workspace"): + restored += ensure_project_admin_membership(project, user) + return restored + + +def ensure_workspace_admin_project_memberships(workspace, project=None): + from plane.db.models import WorkspaceMember + + admin_memberships = ( + WorkspaceMember.objects.filter( + workspace=workspace, + role=ADMIN_ROLE, + is_active=True, + deleted_at__isnull=True, + member__is_bot=False, + ) + .select_related("member") + .order_by("created_at") + ) + + restored = 0 + for workspace_member in admin_memberships: + if project is not None: + restored += ensure_project_admin_membership(project, workspace_member.member) + else: + restored += ensure_user_admin_project_memberships(workspace, workspace_member.member) + return restored diff --git a/plane-src/apps/api/plane/authentication/views/nodedc_workspace_adapter.py b/plane-src/apps/api/plane/authentication/views/nodedc_workspace_adapter.py index 25f6889..70d7934 100644 --- a/plane-src/apps/api/plane/authentication/views/nodedc_workspace_adapter.py +++ b/plane-src/apps/api/plane/authentication/views/nodedc_workspace_adapter.py @@ -9,6 +9,10 @@ from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.csrf import csrf_exempt +from plane.authentication.nodedc_project_memberships import ( + ensure_user_admin_project_memberships, + revoke_auto_project_admin_memberships, +) from plane.authentication.views.nodedc_logout import is_internal_logout_request_authorized from plane.app.realtime.issue_events import publish_assignee_cleanup_issue_events_on_commit from plane.app.realtime.nodedc_events import ( @@ -332,24 +336,7 @@ def serialize_project_membership(project_member, created): def restore_admin_project_memberships(workspace, user): - restored = 0 - for project_member in ProjectMember.objects.filter( - project__workspace=workspace, - member=user, - deleted_at__isnull=True, - ): - update_fields = [] - if project_member.role != ADMIN_ROLE: - project_member.role = ADMIN_ROLE - update_fields.append("role") - if not project_member.is_active: - project_member.is_active = True - update_fields.append("is_active") - if update_fields: - update_fields.append("updated_at") - project_member.save(update_fields=update_fields) - restored += 1 - return restored + return ensure_user_admin_project_memberships(workspace, user) @method_decorator(csrf_exempt, name="dispatch") @@ -441,6 +428,7 @@ class NodeDCInternalWorkspaceMembershipEnsureEndpoint(View): deleted_at__isnull=True, ).first() created = membership is None + previous_role = membership.role if membership is not None else None if membership is None: membership = WorkspaceMember.objects.create( @@ -463,6 +451,8 @@ class NodeDCInternalWorkspaceMembershipEnsureEndpoint(View): if role == ADMIN_ROLE: restore_admin_project_memberships(workspace, user) + elif previous_role == ADMIN_ROLE: + revoke_auto_project_admin_memberships(workspace, user) if set_last_workspace: profile, _ = Profile.objects.get_or_create(user=user) diff --git a/plane-src/apps/api/plane/utils/permissions/project.py b/plane-src/apps/api/plane/utils/permissions/project.py index 55550b2..b10282c 100644 --- a/plane-src/apps/api/plane/utils/permissions/project.py +++ b/plane-src/apps/api/plane/utils/permissions/project.py @@ -26,7 +26,7 @@ class ProjectBasePermission(BasePermission): return WorkspaceMember.objects.filter( workspace__slug=view.workspace_slug, member=request.user, - role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value], + role=ROLE.ADMIN.value, is_active=True, ).exists() @@ -68,7 +68,7 @@ class ProjectMemberPermission(BasePermission): return WorkspaceMember.objects.filter( workspace__slug=view.workspace_slug, member=request.user, - role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value], + role=ROLE.ADMIN.value, is_active=True, ).exists() diff --git a/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx b/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx index 7add1bb..3a02415 100644 --- a/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx +++ b/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx @@ -45,7 +45,7 @@ function AnalyticsPage({ params }: Route.ComponentProps) { // permissions const canPerformEmptyStateActions = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + [EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE ); diff --git a/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/extended-project-sidebar.tsx b/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/extended-project-sidebar.tsx index 348559c..ec05434 100644 --- a/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/extended-project-sidebar.tsx +++ b/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/extended-project-sidebar.tsx @@ -78,7 +78,7 @@ export const ExtendedProjectSidebar = observer(function ExtendedProjectSidebar() // auth const isAuthorizedUser = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + [EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE ); diff --git a/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/top-toolbar/projects-toolbar-menu.tsx b/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/top-toolbar/projects-toolbar-menu.tsx index 0f8caa0..c908482 100644 --- a/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/top-toolbar/projects-toolbar-menu.tsx +++ b/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/top-toolbar/projects-toolbar-menu.tsx @@ -9,6 +9,7 @@ import { Menu } from "@headlessui/react"; import { observer } from "mobx-react"; import { useParams, usePathname } from "next/navigation"; +import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { PlusIcon, ProjectIcon } from "@plane/propel/icons"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; @@ -16,6 +17,7 @@ import { cn, copyUrlToClipboard } from "@plane/utils"; // hooks import { useCommandPalette } from "@/hooks/store/use-command-palette"; import { useProject } from "@/hooks/store/use-project"; +import { useUserPermissions } from "@/hooks/store/user"; // components import { SidebarProjectsListItem } from "@/components/workspace/sidebar/projects-list-item"; @@ -29,6 +31,8 @@ export const ProjectsToolbarMenu = observer(function ProjectsToolbarMenu({ const { workspaceSlug } = useParams(); const { joinedProjectIds } = useProject(); const { toggleCreateProjectModal } = useCommandPalette(); + const { allowPermissions } = useUserPermissions(); + const canCreateProject = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); const handleCopyText = (projectId: string) => copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/issues`).then(() => { @@ -81,20 +85,22 @@ export const ProjectsToolbarMenu = observer(function ProjectsToolbarMenu({ /> ))} -
- - - -
+ {canCreateProject && ( +
+ + + +
+ )} diff --git a/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/page.tsx b/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/page.tsx index 86bd9f8..6017dec 100644 --- a/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/page.tsx +++ b/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/projects/page.tsx @@ -8,7 +8,7 @@ import { observer } from "mobx-react"; import Link from "next/link"; import { useTheme } from "next-themes"; // plane imports -import { PROJECT_TRACKER_ELEMENTS } from "@plane/constants"; +import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants"; import { Button, getButtonStyling } from "@plane/propel/button"; import { cn } from "@plane/utils"; // assets @@ -16,11 +16,14 @@ import ProjectDarkEmptyState from "@/app/assets/empty-state/project-settings/no- import ProjectLightEmptyState from "@/app/assets/empty-state/project-settings/no-projects-light.png?url"; // hooks import { useCommandPalette } from "@/hooks/store/use-command-palette"; +import { useUserPermissions } from "@/hooks/store/user"; function ProjectSettingsPage() { // store hooks const { resolvedTheme } = useTheme(); const { toggleCreateProjectModal } = useCommandPalette(); + const { allowPermissions } = useUserPermissions(); + const canCreateProject = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); // derived values const resolvedPath = resolvedTheme === "dark" ? ProjectDarkEmptyState : ProjectLightEmptyState; return ( @@ -35,12 +38,14 @@ function ProjectSettingsPage() { Learn more about projects - + {canCreateProject && ( + + )} ); diff --git a/plane-src/apps/web/core/components/home/widgets/empty-states/no-projects.tsx b/plane-src/apps/web/core/components/home/widgets/empty-states/no-projects.tsx index 753fd66..33c6367 100644 --- a/plane-src/apps/web/core/components/home/widgets/empty-states/no-projects.tsx +++ b/plane-src/apps/web/core/components/home/widgets/empty-states/no-projects.tsx @@ -42,7 +42,7 @@ export const NoProjectsEmptyState = observer(function NoProjectsEmptyState() { const { t } = useTranslation(); // derived values const canCreateProject = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + [EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE ); const isWorkspaceAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/empty-states/global-view.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/empty-states/global-view.tsx index 9be06fa..8fac75e 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/empty-states/global-view.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/empty-states/global-view.tsx @@ -23,6 +23,7 @@ export const GlobalViewEmptyState = observer(function GlobalViewEmptyState() { const { toggleCreateIssueModal, toggleCreateProjectModal } = useCommandPalette(); const { allowPermissions } = useUserPermissions(); // derived values + const canCreateProject = allowPermissions([EUserWorkspaceRoles.ADMIN], EUserPermissionsLevel.WORKSPACE); const hasMemberLevelPermission = allowPermissions( [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER], EUserPermissionsLevel.WORKSPACE @@ -41,7 +42,7 @@ export const GlobalViewEmptyState = observer(function GlobalViewEmptyState() { onClick: () => { toggleCreateProjectModal(true); }, - disabled: !hasMemberLevelPermission, + disabled: !canCreateProject, variant: "primary", }, ]} diff --git a/plane-src/apps/web/core/components/issues/workspace-draft/root.tsx b/plane-src/apps/web/core/components/issues/workspace-draft/root.tsx index 1624dc5..dc06e0b 100644 --- a/plane-src/apps/web/core/components/issues/workspace-draft/root.tsx +++ b/plane-src/apps/web/core/components/issues/workspace-draft/root.tsx @@ -39,10 +39,7 @@ export const WorkspaceDraftIssuesRoot = observer(function WorkspaceDraftIssuesRo const { toggleCreateProjectModal } = useCommandPalette(); const { allowPermissions } = useUserPermissions(); // derived values - const hasMemberLevelPermission = allowPermissions( - [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER], - EUserPermissionsLevel.WORKSPACE - ); + const canCreateProject = allowPermissions([EUserWorkspaceRoles.ADMIN], EUserPermissionsLevel.WORKSPACE); //swr hook for fetching issue properties useWorkspaceIssueProperties(workspaceSlug); @@ -77,7 +74,7 @@ export const WorkspaceDraftIssuesRoot = observer(function WorkspaceDraftIssuesRo onClick: () => { toggleCreateProjectModal(true); }, - disabled: !hasMemberLevelPermission, + disabled: !canCreateProject, variant: "primary", }, ]} diff --git a/plane-src/apps/web/core/components/power-k/config/creation/command.ts b/plane-src/apps/web/core/components/power-k/config/creation/command.ts index f63baa9..517b729 100644 --- a/plane-src/apps/web/core/components/power-k/config/creation/command.ts +++ b/plane-src/apps/web/core/components/power-k/config/creation/command.ts @@ -48,7 +48,7 @@ export const usePowerKCreationCommandsRecord = (): Record 0; const canCreateProject = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + [EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE ); const hasProjectMemberLevelPermissions = (ctx: TPowerKContext) => diff --git a/plane-src/apps/web/core/components/project/card-list.tsx b/plane-src/apps/web/core/components/project/card-list.tsx index 961045d..ca94b01 100644 --- a/plane-src/apps/web/core/components/project/card-list.tsx +++ b/plane-src/apps/web/core/components/project/card-list.tsx @@ -48,7 +48,7 @@ export const ProjectCardList = observer(function ProjectCardList(props: TProject // permissions const canPerformEmptyStateActions = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + [EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE ); diff --git a/plane-src/apps/web/core/components/project/create-project-modal.tsx b/plane-src/apps/web/core/components/project/create-project-modal.tsx index a1666a1..3d0f68d 100644 --- a/plane-src/apps/web/core/components/project/create-project-modal.tsx +++ b/plane-src/apps/web/core/components/project/create-project-modal.tsx @@ -5,12 +5,14 @@ */ import { useEffect, useState } from "react"; +import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; import { getAssetIdFromUrl, checkURLValidity } from "@plane/utils"; // plane ui // helpers // hooks import useKeypress from "@/hooks/use-keypress"; +import { useUserPermissions } from "@/hooks/store/user"; // plane web components import { CreateProjectForm } from "@/plane-web/components/projects/create/root"; // plane web types @@ -36,9 +38,11 @@ enum EProjectCreationSteps { export function CreateProjectModal(props: Props) { const { isOpen, onClose, setToFavorite = false, workspaceSlug, data, templateId } = props; + const { allowPermissions } = useUserPermissions(); // states const [currentStep, setCurrentStep] = useState(EProjectCreationSteps.CREATE_PROJECT); const [createdProjectId, setCreatedProjectId] = useState(null); + const canCreateProject = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE, workspaceSlug); useEffect(() => { if (isOpen) { @@ -47,6 +51,10 @@ export function CreateProjectModal(props: Props) { } }, [isOpen]); + useEffect(() => { + if (isOpen && !canCreateProject) onClose(); + }, [canCreateProject, isOpen, onClose]); + const handleNextStep = (projectId: string) => { if (!projectId) return; setCreatedProjectId(projectId); @@ -65,6 +73,8 @@ export function CreateProjectModal(props: Props) { if (isOpen) onClose(); }); + if (!canCreateProject) return null; + return (