diff --git a/apps/api/plane/ee/views/app/teamspace/base.py b/apps/api/plane/ee/views/app/teamspace/base.py index d4e86328b4..2eb223629d 100644 --- a/apps/api/plane/ee/views/app/teamspace/base.py +++ b/apps/api/plane/ee/views/app/teamspace/base.py @@ -14,7 +14,7 @@ import random import json # Django imports -from django.db.models import OuterRef, Subquery, Exists +from django.db.models import OuterRef, Subquery, Exists, Q from django.db.models.functions import Coalesce from django.contrib.postgres.aggregates import ArrayAgg from django.core.serializers.json import DjangoJSONEncoder @@ -27,7 +27,7 @@ from rest_framework.response import Response # Module imports from plane.ee.views.base import BaseAPIView from plane.ee.permissions import WorkspaceUserPermission -from plane.db.models import Workspace +from plane.db.models import Workspace, WorkspaceMember from plane.ee.models import Teamspace, TeamspaceProject, TeamspaceMember from plane.ee.serializers import TeamspaceSerializer from plane.payment.flags.flag import FeatureFlag @@ -41,6 +41,28 @@ class TeamspaceBaseEndpoint(BaseAPIView): def team_space_id(self): return self.kwargs.get("team_space_id") + def is_admin_or_teamspace_lead(self, request, slug, team_space_id): + """Check if user is workspace admin, teamspace lead, or teamspace member""" + # Check if workspace admin + is_admin = WorkspaceMember.objects.filter( + member=request.user, + workspace__slug=slug, + is_active=True, + role=ROLE.ADMIN.value, + ).exists() + + if is_admin: + return True + + # Check if teamspace lead + is_lead = Teamspace.objects.filter( + pk=team_space_id, + workspace__slug=slug, + lead=request.user, + ).exists() + + return is_lead + class TeamspaceEndpoint(TeamspaceBaseEndpoint): permission_classes = [WorkspaceUserPermission] @@ -68,9 +90,15 @@ class TeamspaceEndpoint(TeamspaceBaseEndpoint): .annotate( is_member=Exists( TeamspaceMember.objects.filter(team_space=OuterRef("pk"), member_id=self.request.user.id) - ) + ), + is_workspace_admin=Exists( + WorkspaceMember.objects.filter( + workspace__slug=slug, member=self.request.user, role=ROLE.ADMIN.value, is_active=True + ) + ), ) - .get(workspace__slug=slug, pk=team_space_id, is_member=True) + .filter(Q(is_member=True) | Q(is_workspace_admin=True)) + .get(workspace__slug=slug, pk=team_space_id) ) def get_team_spaces(self, slug): @@ -180,10 +208,17 @@ class TeamspaceEndpoint(TeamspaceBaseEndpoint): except Exception as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) - @allow_permission(level="WORKSPACE", allowed_roles=[ROLE.ADMIN]) + @allow_permission(level="WORKSPACE", allowed_roles=[ROLE.ADMIN, ROLE.MEMBER]) @check_feature_flag(FeatureFlag.TEAMSPACES) def patch(self, request, slug, team_space_id): try: + # # Check if user is workspace admin or teamspace lead + if not self.is_admin_or_teamspace_lead(request, slug, team_space_id): + return Response( + {"error": "You don't have permission to edit this teamspace."}, + status=status.HTTP_403_FORBIDDEN, + ) + # Get team space by pk team_space = self.get_team_space(slug, team_space_id) # Get workspace @@ -256,17 +291,17 @@ class TeamspaceEndpoint(TeamspaceBaseEndpoint): except Teamspace.DoesNotExist: return Response({"error": "Team space not found"}, status=status.HTTP_404_NOT_FOUND) - @allow_permission(level="WORKSPACE", allowed_roles=[ROLE.ADMIN]) + @allow_permission(level="WORKSPACE", allowed_roles=[ROLE.ADMIN, ROLE.MEMBER]) @check_feature_flag(FeatureFlag.TEAMSPACES) def delete(self, request, slug, team_space_id): """ Delete team space by pk """ try: - # The current deleting user should be part of the team space - if not TeamspaceMember.objects.filter(team_space_id=team_space_id, member_id=request.user).exists(): + # # Check if user is workspace admin or teamspace lead + if not self.is_admin_or_teamspace_lead(request, slug, team_space_id): return Response( - {"error": "You are not part of the team space"}, + {"error": "You don't have permission to delete this teamspace."}, status=status.HTTP_403_FORBIDDEN, ) diff --git a/apps/web/ee/components/teamspaces/actions/projects/button.tsx b/apps/web/ee/components/teamspaces/actions/projects/button.tsx index 5e5e823bea..2a2d77f360 100644 --- a/apps/web/ee/components/teamspaces/actions/projects/button.tsx +++ b/apps/web/ee/components/teamspaces/actions/projects/button.tsx @@ -42,7 +42,7 @@ type UpdateTeamspaceProjectsButtonProps = { }) => ReactNode; }; -const TOOLTIP_CONTENT = "Contact teamspace admin"; +const TOOLTIP_CONTENT = "You don't have permission to add project."; export const UpdateTeamspaceProjectsButton = observer(function UpdateTeamspaceProjectsButton( props: UpdateTeamspaceProjectsButtonProps diff --git a/apps/web/ee/components/teamspaces/overview/root.tsx b/apps/web/ee/components/teamspaces/overview/root.tsx index 5788506aa7..e4b3afbcb3 100644 --- a/apps/web/ee/components/teamspaces/overview/root.tsx +++ b/apps/web/ee/components/teamspaces/overview/root.tsx @@ -18,7 +18,7 @@ import { EUserPermissionsLevel } from "@plane/constants"; import { EUserWorkspaceRoles } from "@plane/types"; import { ContentWrapper, ERowVariant } from "@plane/ui"; // hooks -import { useUserPermissions } from "@/hooks/store/user"; +import { useUser, useUserPermissions } from "@/hooks/store/user"; // plane web imports import { useTeamspaces } from "@/plane-web/hooks/store"; // local imports @@ -34,10 +34,13 @@ export const TeamsOverviewRoot = observer(function TeamsOverviewRoot(props: TTea // router const { workspaceSlug } = useParams(); // hooks + const { data: currentUser } = useUser(); const { allowPermissions } = useUserPermissions(); - const { isCurrentUserMemberOfTeamspace } = useTeamspaces(); + const { isCurrentUserMemberOfTeamspace, getTeamspaceById } = useTeamspaces(); // derived values + const teamspace = getTeamspaceById(teamspaceId); const isTeamspaceMember = isCurrentUserMemberOfTeamspace(teamspaceId); + const isTeamspaceLead = currentUser?.id === teamspace?.lead_id; const hasAdminLevelPermissions = allowPermissions( [EUserWorkspaceRoles.ADMIN], EUserPermissionsLevel.WORKSPACE, @@ -49,7 +52,7 @@ export const TeamsOverviewRoot = observer(function TeamsOverviewRoot(props: TTea