Compare commits

...

8 Commits

Author SHA1 Message Date
gakshita
f922d1627b fix: page level permissions fixed 2025-03-26 13:02:50 +05:30
NarayanBavisetti
5f9d3ef6a5 chore: removed extra validations 2025-03-25 15:27:37 +05:30
NarayanBavisetti
db792f7f4f chore: added restricition for private projects 2025-03-25 15:25:13 +05:30
gakshita
2607ce5e24 fix: type 2025-03-25 14:03:34 +05:30
gakshita
bdca9f687f fix: refactor 2025-03-24 20:02:21 +05:30
gakshita
19341d6b64 fix: refactor 2025-03-24 18:46:02 +05:30
sangeethailango
aa388975d2 chore: return network value 2025-03-24 17:24:41 +05:30
gakshita
eef66573e6 fix: private project join issue 2025-03-24 14:46:38 +05:30
14 changed files with 106 additions and 21 deletions

View File

@@ -179,6 +179,7 @@ class ProjectViewSet(BaseViewSet):
"inbox_view",
"guest_view_all_features",
"project_lead",
"network",
"created_at",
"updated_at",
"created_by",

View File

@@ -16,17 +16,17 @@ from rest_framework.permissions import AllowAny
# 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 @@ class UserProjectInvitationsViewset(BaseViewSet):
.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,22 @@ class UserProjectInvitationsViewset(BaseViewSet):
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
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

View File

@@ -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 @@ from .base import BaseModel
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": {

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -1,13 +1,13 @@
"use client";
import { AppHeader, ContentWrapper } from "@/components/core";
import WorkspaceAccessWrapper from "@/layouts/access/workspace-wrapper";
import { WorkspaceActiveCycleHeader } from "./header";
export default function WorkspaceActiveCycleLayout({ children }: { children: React.ReactNode }) {
return (
<>
<WorkspaceAccessWrapper pageKey="active_cycles">
<AppHeader header={<WorkspaceActiveCycleHeader />} />
<ContentWrapper>{children}</ContentWrapper>
</>
</WorkspaceAccessWrapper>
);
}

View File

@@ -1,13 +1,14 @@
"use client";
import { AppHeader, ContentWrapper } from "@/components/core";
import WorkspaceAccessWrapper from "@/layouts/access/workspace-wrapper";
import { WorkspaceAnalyticsHeader } from "./header";
export default function WorkspaceAnalyticsLayout({ children }: { children: React.ReactNode }) {
return (
<>
<WorkspaceAccessWrapper pageKey="analytics">
<AppHeader header={<WorkspaceAnalyticsHeader />} />
<ContentWrapper>{children}</ContentWrapper>
</>
</WorkspaceAccessWrapper>
);
}

View File

@@ -1,13 +1,14 @@
"use client";
import { AppHeader, ContentWrapper } from "@/components/core";
import WorkspaceAccessWrapper from "@/layouts/access/workspace-wrapper";
import { WorkspaceDraftHeader } from "./header";
export default function WorkspaceDraftLayout({ children }: { children: React.ReactNode }) {
return (
<>
<WorkspaceAccessWrapper pageKey="drafts">
<AppHeader header={<WorkspaceDraftHeader />} />
<ContentWrapper>{children}</ContentWrapper>
</>
</WorkspaceAccessWrapper>
);
}

View File

@@ -14,6 +14,7 @@ import { USER_PROFILE_PROJECT_SEGREGATION } from "@/constants/fetch-keys";
import { useUserPermissions } from "@/hooks/store";
import useSize from "@/hooks/use-window-size";
// local components
import WorkspaceAccessWrapper from "@/layouts/access/workspace-wrapper";
import { UserService } from "@/services/user.service";
import { UserProfileHeader } from "./header";
import { ProfileIssuesMobileHeader } from "./mobile-header";
@@ -57,7 +58,7 @@ const UseProfileLayout: React.FC<Props> = observer((props) => {
const currentTab = tabsList.find((tab) => pathname === `/${workspaceSlug}/profile/${userId}${tab.selected}`);
return (
<>
<WorkspaceAccessWrapper pageKey="your_work">
{/* Passing the type prop from the current route value as we need the header as top most component.
TODO: We are depending on the route path to handle the mobile header type. If the path changes, this logic will break. */}
<div className="h-full w-full flex flex-col md:flex-row overflow-hidden">
@@ -90,7 +91,7 @@ const UseProfileLayout: React.FC<Props> = observer((props) => {
</div>
{isSmallerScreen && <ProfileSidebar userProjectsData={userProjectsData} />}
</div>
</>
</WorkspaceAccessWrapper>
);
});

View File

@@ -4,13 +4,15 @@ import { ReactNode } from "react";
// components
import { AppHeader, ContentWrapper } from "@/components/core";
// local components
import WorkspaceAccessWrapper from "@/layouts/access/workspace-wrapper";
import { ProjectsListHeader } from "@/plane-web/components/projects/header";
import { ProjectsListMobileHeader } from "@/plane-web/components/projects/mobile-header";
export default function ProjectListLayout({ children }: { children: ReactNode }) {
return (
<>
<WorkspaceAccessWrapper pageKey="archives">
<AppHeader header={<ProjectsListHeader />} mobileHeader={<ProjectsListMobileHeader />} />
<ContentWrapper>{children}</ContentWrapper>
</>
</WorkspaceAccessWrapper>
);
}

View File

@@ -0,0 +1 @@
export * from "./workspace-wrapper";

View File

@@ -0,0 +1,23 @@
import { useParams } from "next/navigation";
import { NotAuthorizedView } from "@/components/auth-screens";
import { useUserPermissions } from "@/hooks/store";
interface IWorkspaceAuthWrapper {
children: React.ReactNode;
pageKey: string;
}
const WorkspaceAccessWrapper = ({ children, ...props }: IWorkspaceAuthWrapper) => {
const { pageKey } = props;
// router
const { workspaceSlug } = useParams();
// store
const { hasPageAccess } = useUserPermissions();
// derived values
const isAuthorized = hasPageAccess(workspaceSlug?.toString() ?? "", pageKey);
// render
if (!isAuthorized) return <NotAuthorizedView />;
return <>{children}</>;
};
export default WorkspaceAccessWrapper;

View File

@@ -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<IProjectAuthWrapper> = 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<IProjectAuthWrapper> = observer((props) => {
);
// check if the user don't have permission to access the project
if (projectExists && projectId && hasPermissionToCurrentProject === false) return <JoinProject />;
if (
((projectExists?.network && projectExists?.network !== EProjectNetwork.PRIVATE) || isWorkspaceAdmin) &&
projectId &&
hasPermissionToCurrentProject === false
)
return <JoinProject />;
// check if the project info is not found.
if (loader === "loaded" && !projectExists && projectId && !!hasPermissionToCurrentProject === false)
if (loader === "loaded" && projectId && !!hasPermissionToCurrentProject === false)
return (
<div className="grid h-screen place-items-center bg-custom-background-100">
<DetailedEmptyState

View File

@@ -10,6 +10,7 @@ import {
EUserPermissionsLevel,
TUserPermissions,
TUserPermissionsLevel,
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS,
} from "@plane/constants";
import { IProjectMember, IUserProjectsRole, IWorkspaceMemberMe } from "@plane/types";
// plane web types
@@ -54,6 +55,7 @@ export interface IUserPermissionStore {
fetchUserProjectPermissions: (workspaceSlug: string) => Promise<IUserProjectsRole | undefined>;
joinProject: (workspaceSlug: string, projectId: string) => Promise<void | undefined>;
leaveProject: (workspaceSlug: string, projectId: string) => Promise<void>;
hasPageAccess: (workspaceSlug: string, key: string) => boolean;
}
export class UserPermissionStore implements IUserPermissionStore {
@@ -108,6 +110,20 @@ export class UserPermissionStore implements IUserPermissionStore {
}
);
/**
* @description Returns whether the user has the permission to access a page
* @param { string } page
* @returns { boolean }
*/
hasPageAccess = computedFn((workspaceSlug: string, key: string): boolean => {
if (!workspaceSlug || !key) return false;
const settings = WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS.find((item) => item.key === key);
if (settings) {
return this.allowPermissions(settings.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug);
}
return false;
});
// action helpers
/**
* @description Returns whether the user has the permission to perform an action