fix: workspace member invite to avoid lower permission user to invite higher permission member (#1309)

This commit is contained in:
pablohashescobar 2023-06-16 19:52:24 +05:30 committed by GitHub
parent 02111d779b
commit 4c0857233e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 421 additions and 338 deletions

View File

@ -195,6 +195,11 @@ class InviteWorkspaceEndpoint(BaseAPIView):
{"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST {"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST
) )
# check for role level
requesting_user = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user)
if len([email for email in emails if int(email.get("role", 10)) > requesting_user.role]):
return Response({"error": "You cannot invite a user with higher role"}, status=status.HTTP_400_BAD_REQUEST)
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
# Check if user is already a member of workspace # Check if user is already a member of workspace

View File

@ -9,11 +9,13 @@ import { useForm, Controller } from "react-hook-form";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// ui // ui
import { CustomSelect, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; import { CustomSelect, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
// hooks
import useToast from "hooks/use-toast";
// services // services
import projectService from "services/project.service"; import projectService from "services/project.service";
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
// contexts
import { useProjectMyMembership } from "contexts/project-member.context";
// hooks
import useToast from "hooks/use-toast";
// types // types
import { ICurrentUserResponse, IProjectMemberInvitation } from "types"; import { ICurrentUserResponse, IProjectMemberInvitation } from "types";
// fetch-keys // fetch-keys
@ -46,6 +48,7 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { memberDetails } = useProjectMyMembership();
const { data: people } = useSWR( const { data: people } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug as string) : null, workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug as string) : null,
@ -202,11 +205,15 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
input input
width="w-full" width="w-full"
> >
{Object.entries(ROLE).map(([key, label]) => ( {Object.entries(ROLE).map(([key, label]) => {
if (parseInt(key) > (memberDetails?.role ?? 5)) return null;
return (
<CustomSelect.Option key={key} value={key}> <CustomSelect.Option key={key} value={key}>
{label} {label}
</CustomSelect.Option> </CustomSelect.Option>
))} );
})}
</CustomSelect> </CustomSelect>
)} )}
/> />

View File

@ -1,17 +1,22 @@
import React from "react"; import React from "react";
import { mutate } from "swr"; import { mutate } from "swr";
// react-hook-form
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// headless // headless
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// services // services
import workspaceService from "services/workspace.service"; import workspaceService from "services/workspace.service";
// ui // contexts
import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/ui"; import { useWorkspaceMyMembership } from "contexts/workspace-member.context";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// ui
import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/ui";
// types // types
import { ICurrentUserResponse, IWorkspaceMemberInvitation } from "types"; import { ICurrentUserResponse, IWorkspaceMemberInvitation } from "types";
// fetch keys // fetch-keys
import { WORKSPACE_INVITATIONS } from "constants/fetch-keys"; import { WORKSPACE_INVITATIONS } from "constants/fetch-keys";
// constants // constants
import { ROLE } from "constants/workspace"; import { ROLE } from "constants/workspace";
@ -37,6 +42,7 @@ const SendWorkspaceInvitationModal: React.FC<Props> = ({
user, user,
}) => { }) => {
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { memberDetails } = useWorkspaceMyMembership();
const { const {
control, control,
@ -145,11 +151,15 @@ const SendWorkspaceInvitationModal: React.FC<Props> = ({
width="w-full" width="w-full"
input input
> >
{Object.entries(ROLE).map(([key, value]) => ( {Object.entries(ROLE).map(([key, value]) => {
if (parseInt(key) > (memberDetails?.role ?? 5)) return null;
return (
<CustomSelect.Option key={key} value={key}> <CustomSelect.Option key={key} value={key}>
{value} {value}
</CustomSelect.Option> </CustomSelect.Option>
))} );
})}
</CustomSelect> </CustomSelect>
)} )}
/> />

View File

@ -0,0 +1,61 @@
import { createContext, useContext } from "react";
// next
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import workspaceService from "services/workspace.service";
// types
import { IWorkspaceMember } from "types";
// fetch-keys
import { WORKSPACE_MEMBERS_ME } from "constants/fetch-keys";
type ContextType = {
loading: boolean;
memberDetails?: IWorkspaceMember;
error: any;
};
export const WorkspaceMemberContext = createContext<ContextType>({} as ContextType);
type Props = {
children: React.ReactNode;
};
export const WorkspaceMemberProvider: React.FC<Props> = (props) => {
const { children } = props;
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: memberDetails, error } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS_ME(workspaceSlug.toString()) : null,
workspaceSlug ? () => workspaceService.workspaceMemberMe(workspaceSlug.toString()) : null
);
const loading = !memberDetails && !error;
return (
<WorkspaceMemberContext.Provider value={{ loading, memberDetails, error }}>
{children}
</WorkspaceMemberContext.Provider>
);
};
export const useWorkspaceMyMembership = () => {
const context = useContext(WorkspaceMemberContext);
if (context === undefined)
throw new Error(`useWorkspaceMember must be used within a WorkspaceMemberProvider.`);
return {
...context,
memberRole: {
isOwner: context.memberDetails?.role === 20,
isMember: context.memberDetails?.role === 15,
isViewer: context.memberDetails?.role === 10,
isGuest: context.memberDetails?.role === 5,
},
};
};

View File

@ -7,6 +7,8 @@ import useSWR from "swr";
// services // services
import workspaceServices from "services/workspace.service"; import workspaceServices from "services/workspace.service";
// contexts
import { WorkspaceMemberProvider } from "contexts/workspace-member.context";
// layouts // layouts
import AppSidebar from "layouts/app-layout/app-sidebar"; import AppSidebar from "layouts/app-layout/app-sidebar";
import AppHeader from "layouts/app-layout/app-header"; import AppHeader from "layouts/app-layout/app-header";
@ -78,6 +80,7 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
return ( return (
<UserAuthorizationLayout> <UserAuthorizationLayout>
<WorkspaceMemberProvider>
<CommandPalette /> <CommandPalette />
<div className="relative flex h-screen w-full overflow-hidden"> <div className="relative flex h-screen w-full overflow-hidden">
<AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} /> <AppSidebar toggleSidebar={toggleSidebar} setToggleSidebar={setToggleSidebar} />
@ -120,6 +123,7 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
</main> </main>
)} )}
</div> </div>
</WorkspaceMemberProvider>
</UserAuthorizationLayout> </UserAuthorizationLayout>
); );
}; };

View File

@ -89,7 +89,17 @@ const MembersSettings: NextPage = () => {
const currentUser = projectMembers?.find((item) => item.member.id === user?.id); const currentUser = projectMembers?.find((item) => item.member.id === user?.id);
return ( return (
<> <ProjectAuthorizationWrapper
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
title={`${projectDetails?.name ?? "Project"}`}
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
/>
<BreadcrumbItem title="Members Settings" />
</Breadcrumbs>
}
>
<ConfirmProjectMemberRemove <ConfirmProjectMemberRemove
isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)} isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)}
onClose={() => { onClose={() => {
@ -136,17 +146,6 @@ const MembersSettings: NextPage = () => {
members={members} members={members}
user={user} user={user}
/> />
<ProjectAuthorizationWrapper
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
title={`${projectDetails?.name ?? "Project"}`}
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
/>
<BreadcrumbItem title="Members Settings" />
</Breadcrumbs>
}
>
<div className="p-8"> <div className="p-8">
<SettingsHeader /> <SettingsHeader />
<section className="space-y-5"> <section className="space-y-5">
@ -278,7 +277,6 @@ const MembersSettings: NextPage = () => {
</section> </section>
</div> </div>
</ProjectAuthorizationWrapper> </ProjectAuthorizationWrapper>
</>
); );
}; };

View File

@ -85,7 +85,17 @@ const MembersSettings: NextPage = () => {
const currentUser = workspaceMembers?.find((item) => item.member?.id === user?.id); const currentUser = workspaceMembers?.find((item) => item.member?.id === user?.id);
return ( return (
<> <WorkspaceAuthorizationLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
title={`${activeWorkspace?.name ?? "Workspace"}`}
link={`/${workspaceSlug}`}
/>
<BreadcrumbItem title="Members Settings" />
</Breadcrumbs>
}
>
<ConfirmWorkspaceMemberRemove <ConfirmWorkspaceMemberRemove
isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)} isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)}
onClose={() => { onClose={() => {
@ -137,17 +147,6 @@ const MembersSettings: NextPage = () => {
members={members} members={members}
user={user} user={user}
/> />
<WorkspaceAuthorizationLayout
breadcrumbs={
<Breadcrumbs>
<BreadcrumbItem
title={`${activeWorkspace?.name ?? "Workspace"}`}
link={`/${workspaceSlug}`}
/>
<BreadcrumbItem title="Members Settings" />
</Breadcrumbs>
}
>
<div className="p-8"> <div className="p-8">
<SettingsHeader /> <SettingsHeader />
<section className="space-y-5"> <section className="space-y-5">
@ -279,7 +278,6 @@ const MembersSettings: NextPage = () => {
</section> </section>
</div> </div>
</WorkspaceAuthorizationLayout> </WorkspaceAuthorizationLayout>
</>
); );
}; };