diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index 00e1ad1eb13..b4f0b3aad10 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -179,6 +179,7 @@ def list(self, request, slug): "inbox_view", "guest_view_all_features", "project_lead", + "network", "created_at", "updated_at", "created_by", diff --git a/apiserver/plane/app/views/project/invite.py b/apiserver/plane/app/views/project/invite.py index e4d46e89f4e..51eb997f661 100644 --- a/apiserver/plane/app/views/project/invite.py +++ b/apiserver/plane/app/views/project/invite.py @@ -16,17 +16,17 @@ # Module imports from .base import BaseViewSet, BaseAPIView from plane.app.serializers import ProjectMemberInviteSerializer - from plane.app.permissions import allow_permission, ROLE - from plane.db.models import ( ProjectMember, Workspace, ProjectMemberInvite, User, WorkspaceMember, + Project, IssueUserProperty, ) +from plane.db.models.project import ProjectNetwork class ProjectInvitationsViewset(BaseViewSet): @@ -128,6 +128,7 @@ def get_queryset(self): .select_related("workspace", "workspace__owner", "project") ) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") def create(self, request, slug): project_ids = request.data.get("project_ids", []) @@ -136,11 +137,20 @@ def create(self, request, slug): member=request.user, workspace__slug=slug, is_active=True ) - if workspace_member.role not in [ROLE.ADMIN.value, ROLE.MEMBER.value]: - return Response( - {"error": "You do not have permission to join the project"}, - status=status.HTTP_403_FORBIDDEN, - ) + # Get all the projects + projects = Project.objects.filter( + id__in=project_ids, workspace__slug=slug + ).only("id", "network") + # Check if user has permission to join each project + for project in projects: + if ( + project.network == ProjectNetwork.SECRET.value + and workspace_member.role != ROLE.ADMIN.value + ): + return Response( + {"error": "Only workspace admins can join private project"}, + status=status.HTTP_403_FORBIDDEN, + ) workspace_role = workspace_member.role workspace = workspace_member.workspace diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index c97c550ee21..c4d097ac8f3 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -1,6 +1,7 @@ # Python imports import pytz from uuid import uuid4 +from enum import Enum # Django imports from django.conf import settings @@ -17,6 +18,15 @@ ROLE_CHOICES = ((20, "Admin"), (15, "Member"), (5, "Guest")) +class ProjectNetwork(Enum): + SECRET = 0 + PUBLIC = 2 + + @classmethod + def choices(cls): + return [(0, "Secret"), (2, "Public")] + + def get_default_props(): return { "filters": { diff --git a/packages/types/src/enums.ts b/packages/types/src/enums.ts index 854c0c61405..53138a1d798 100644 --- a/packages/types/src/enums.ts +++ b/packages/types/src/enums.ts @@ -6,6 +6,12 @@ export enum EUserPermissions { export type TUserPermissions = EUserPermissions.ADMIN | EUserPermissions.MEMBER | EUserPermissions.GUEST; +// project network +export enum EProjectNetwork { + PRIVATE = 0, + PUBLIC = 2, +} + // project pages export enum EPageAccess { PUBLIC = 0, diff --git a/packages/types/src/project/projects.d.ts b/packages/types/src/project/projects.d.ts index 40562d362d5..e1d9117a1be 100644 --- a/packages/types/src/project/projects.d.ts +++ b/packages/types/src/project/projects.d.ts @@ -27,6 +27,7 @@ export interface IPartialProject { inbox_view: boolean; guest_view_all_features?: boolean; project_lead?: IUserLite | string | null; + network?: number; // Timestamps created_at?: Date; updated_at?: Date; @@ -50,7 +51,6 @@ export interface IProject extends IPartialProject { anchor?: string | null; is_favorite?: boolean; members?: string[]; - network?: number; timezone?: string; } diff --git a/web/core/layouts/auth-layout/project-wrapper.tsx b/web/core/layouts/auth-layout/project-wrapper.tsx index e79bdd1897a..c356cb883ba 100644 --- a/web/core/layouts/auth-layout/project-wrapper.tsx +++ b/web/core/layouts/auth-layout/project-wrapper.tsx @@ -7,6 +7,7 @@ import useSWR from "swr"; import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // components +import { EProjectNetwork } from "@plane/types/src/enums"; import { JoinProject } from "@/components/auth-screens"; import { LogoSpinner } from "@/components/common"; import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; @@ -70,6 +71,11 @@ export const ProjectAuthWrapper: FC = observer((props) => { workspaceSlug.toString(), projectId?.toString() ); + const isWorkspaceAdmin = allowPermissions( + [EUserPermissions.ADMIN], + EUserPermissionsLevel.WORKSPACE, + workspaceSlug.toString() + ); // Initialize module timeline chart useEffect(() => { @@ -168,10 +174,15 @@ export const ProjectAuthWrapper: FC = observer((props) => { ); // check if the user don't have permission to access the project - if (projectExists && projectId && hasPermissionToCurrentProject === false) return ; + if ( + ((projectExists?.network && projectExists?.network !== EProjectNetwork.PRIVATE) || isWorkspaceAdmin) && + projectId && + hasPermissionToCurrentProject === false + ) + return ; // check if the project info is not found. - if (loader === "loaded" && !projectExists && projectId && !!hasPermissionToCurrentProject === false) + if (loader === "loaded" && projectId && !!hasPermissionToCurrentProject === false) return (