mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
Compare commits
8 Commits
fix-activi
...
fix-page-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f922d1627b | ||
|
|
5f9d3ef6a5 | ||
|
|
db792f7f4f | ||
|
|
2607ce5e24 | ||
|
|
bdca9f687f | ||
|
|
19341d6b64 | ||
|
|
aa388975d2 | ||
|
|
eef66573e6 |
@@ -179,6 +179,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
"inbox_view",
|
||||
"guest_view_all_features",
|
||||
"project_lead",
|
||||
"network",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
2
packages/types/src/project/projects.d.ts
vendored
2
packages/types/src/project/projects.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
1
web/core/layouts/access/index.ts
Normal file
1
web/core/layouts/access/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./workspace-wrapper";
|
||||
23
web/core/layouts/access/workspace-wrapper.tsx
Normal file
23
web/core/layouts/access/workspace-wrapper.tsx
Normal 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user