From 0dfa06e55b34a409fb62edea87cf96814484203a Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 16 Jun 2023 19:06:34 +0530 Subject: [PATCH] fix: lower role user cannot invite higher role user (#1302) --- .../project/send-project-invitation-modal.tsx | 21 +- .../send-workspace-invitation-modal.tsx | 26 +- .../app/contexts/workspace-member.context.tsx | 61 ++++ .../workspace-authorization-wrapper.tsx | 86 +++--- .../projects/[projectId]/settings/members.tsx | 280 +++++++++--------- .../[workspaceSlug]/settings/members.tsx | 280 +++++++++--------- 6 files changed, 416 insertions(+), 338 deletions(-) create mode 100644 apps/app/contexts/workspace-member.context.tsx diff --git a/apps/app/components/project/send-project-invitation-modal.tsx b/apps/app/components/project/send-project-invitation-modal.tsx index e08b92e8c..204633a85 100644 --- a/apps/app/components/project/send-project-invitation-modal.tsx +++ b/apps/app/components/project/send-project-invitation-modal.tsx @@ -9,11 +9,13 @@ import { useForm, Controller } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; // ui import { CustomSelect, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; -// hooks -import useToast from "hooks/use-toast"; // services import projectService from "services/project.service"; import workspaceService from "services/workspace.service"; +// contexts +import { useProjectMyMembership } from "contexts/project-member.context"; +// hooks +import useToast from "hooks/use-toast"; // types import { ICurrentUserResponse, IProjectMemberInvitation } from "types"; // fetch-keys @@ -46,6 +48,7 @@ const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, member const { workspaceSlug, projectId } = router.query; const { setToastAlert } = useToast(); + const { memberDetails } = useProjectMyMembership(); const { data: people } = useSWR( workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug as string) : null, @@ -202,11 +205,15 @@ const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, member input width="w-full" > - {Object.entries(ROLE).map(([key, label]) => ( - - {label} - - ))} + {Object.entries(ROLE).map(([key, label]) => { + if (parseInt(key) > (memberDetails?.role ?? 5)) return null; + + return ( + + {label} + + ); + })} )} /> diff --git a/apps/app/components/workspace/send-workspace-invitation-modal.tsx b/apps/app/components/workspace/send-workspace-invitation-modal.tsx index 52dc74149..56d9385b1 100644 --- a/apps/app/components/workspace/send-workspace-invitation-modal.tsx +++ b/apps/app/components/workspace/send-workspace-invitation-modal.tsx @@ -1,17 +1,22 @@ import React from "react"; + import { mutate } from "swr"; + +// react-hook-form import { Controller, useForm } from "react-hook-form"; // headless import { Dialog, Transition } from "@headlessui/react"; // services import workspaceService from "services/workspace.service"; -// ui -import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/ui"; +// contexts +import { useWorkspaceMyMembership } from "contexts/workspace-member.context"; // hooks import useToast from "hooks/use-toast"; +// ui +import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/ui"; // types import { ICurrentUserResponse, IWorkspaceMemberInvitation } from "types"; -// fetch keys +// fetch-keys import { WORKSPACE_INVITATIONS } from "constants/fetch-keys"; // constants import { ROLE } from "constants/workspace"; @@ -37,6 +42,7 @@ const SendWorkspaceInvitationModal: React.FC = ({ user, }) => { const { setToastAlert } = useToast(); + const { memberDetails } = useWorkspaceMyMembership(); const { control, @@ -145,11 +151,15 @@ const SendWorkspaceInvitationModal: React.FC = ({ width="w-full" input > - {Object.entries(ROLE).map(([key, value]) => ( - - {value} - - ))} + {Object.entries(ROLE).map(([key, value]) => { + if (parseInt(key) > (memberDetails?.role ?? 5)) return null; + + return ( + + {value} + + ); + })} )} /> diff --git a/apps/app/contexts/workspace-member.context.tsx b/apps/app/contexts/workspace-member.context.tsx new file mode 100644 index 000000000..5f34bd28b --- /dev/null +++ b/apps/app/contexts/workspace-member.context.tsx @@ -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({} as ContextType); + +type Props = { + children: React.ReactNode; +}; + +export const WorkspaceMemberProvider: React.FC = (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 ( + + {children} + + ); +}; + +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, + }, + }; +}; diff --git a/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx b/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx index e67424758..201dd5a72 100644 --- a/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx +++ b/apps/app/layouts/auth-layout/workspace-authorization-wrapper.tsx @@ -7,6 +7,8 @@ import useSWR from "swr"; // services import workspaceServices from "services/workspace.service"; +// contexts +import { WorkspaceMemberProvider } from "contexts/workspace-member.context"; // layouts import AppSidebar from "layouts/app-layout/app-sidebar"; import AppHeader from "layouts/app-layout/app-header"; @@ -78,48 +80,50 @@ export const WorkspaceAuthorizationLayout: React.FC = ({ return ( - -
- - {settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? ( - - - - Go to workspace - - - - } - type="workspace" - /> - ) : ( -
- {!noHeader && ( - - )} -
-
- {children} + + +
+ + {settingsLayout && (memberType?.isGuest || memberType?.isViewer) ? ( + + + + Go to workspace + + + + } + type="workspace" + /> + ) : ( +
+ {!noHeader && ( + + )} +
+
+ {children} +
-
-
- )} -
+ + )} + +
); }; diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx index 92f490687..a0d53df35 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/members.tsx @@ -89,7 +89,17 @@ const MembersSettings: NextPage = () => { const currentUser = projectMembers?.find((item) => item.member.id === user?.id); return ( - <> + + + + + } + > { @@ -136,149 +146,137 @@ const MembersSettings: NextPage = () => { members={members} user={user} /> - - - - - } - > -
- -
-
-

Members

- -
- {!projectMembers || !projectInvitations ? ( - - - - - - - ) : ( -
- {members.length > 0 - ? members.map((member) => ( -
-
-
- {member.avatar && member.avatar !== "" ? ( - {member.first_name} - ) : member.first_name !== "" ? ( - member.first_name.charAt(0) - ) : ( - member.email.charAt(0) - )} -
-
-

- {member.first_name} {member.last_name} -

-

{member.email}

-
-
-
- {!member.member && ( -
- Pending -
+
+ +
+
+

Members

+ +
+ {!projectMembers || !projectInvitations ? ( + + + + + + + ) : ( +
+ {members.length > 0 + ? members.map((member) => ( +
+
+
+ {member.avatar && member.avatar !== "" ? ( + {member.first_name} + ) : member.first_name !== "" ? ( + member.first_name.charAt(0) + ) : ( + member.email.charAt(0) )} - { - if (!activeWorkspace || !projectDetails) return; - - mutateMembers( - (prevData: any) => - prevData.map((m: any) => - m.id === member.id ? { ...m, role: value } : m - ), - false - ); - - projectService - .updateProjectMember( - activeWorkspace.slug, - projectDetails.id, - member.id, - { - role: value, - } - ) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: - "An error occurred while updating member role. Please try again.", - }); - }); - }} - position="right" - disabled={ - member.memberId === user?.id || - !member.member || - (currentUser && - currentUser.role !== 20 && - currentUser.role < member.role) - } - > - {Object.keys(ROLE).map((key) => { - if ( - currentUser && - currentUser.role !== 20 && - currentUser.role < parseInt(key) - ) - return null; - - return ( - - <>{ROLE[parseInt(key) as keyof typeof ROLE]} - - ); - })} - - - { - if (member.member) setSelectedRemoveMember(member.id); - else setSelectedInviteRemoveMember(member.id); - }} - > - - - Remove member - - - +
+
+

+ {member.first_name} {member.last_name} +

+

{member.email}

- )) - : null} -
- )} -
-
- - +
+ {!member.member && ( +
+ Pending +
+ )} + { + if (!activeWorkspace || !projectDetails) return; + + mutateMembers( + (prevData: any) => + prevData.map((m: any) => + m.id === member.id ? { ...m, role: value } : m + ), + false + ); + + projectService + .updateProjectMember( + activeWorkspace.slug, + projectDetails.id, + member.id, + { + role: value, + } + ) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: + "An error occurred while updating member role. Please try again.", + }); + }); + }} + position="right" + disabled={ + member.memberId === user?.id || + !member.member || + (currentUser && + currentUser.role !== 20 && + currentUser.role < member.role) + } + > + {Object.keys(ROLE).map((key) => { + if ( + currentUser && + currentUser.role !== 20 && + currentUser.role < parseInt(key) + ) + return null; + + return ( + + <>{ROLE[parseInt(key) as keyof typeof ROLE]} + + ); + })} + + + { + if (member.member) setSelectedRemoveMember(member.id); + else setSelectedInviteRemoveMember(member.id); + }} + > + + + Remove member + + + +
+
+ )) + : null} +
+ )} +
+
+
); }; diff --git a/apps/app/pages/[workspaceSlug]/settings/members.tsx b/apps/app/pages/[workspaceSlug]/settings/members.tsx index e01082fd3..da8fd52cb 100644 --- a/apps/app/pages/[workspaceSlug]/settings/members.tsx +++ b/apps/app/pages/[workspaceSlug]/settings/members.tsx @@ -85,7 +85,17 @@ const MembersSettings: NextPage = () => { const currentUser = workspaceMembers?.find((item) => item.member?.id === user?.id); return ( - <> + + + + + } + > { @@ -137,149 +147,137 @@ const MembersSettings: NextPage = () => { members={members} user={user} /> - - - - - } - > -
- -
-
-

Members

- -
- {!workspaceMembers || !workspaceInvitations ? ( - - - - - - - ) : ( -
- {members.length > 0 - ? members.map((member) => ( -
-
-
- {member.avatar && member.avatar !== "" ? ( - {member.first_name} - ) : member.first_name !== "" ? ( - member.first_name.charAt(0) - ) : ( - member.email.charAt(0) - )} -
-
-

- {member.first_name} {member.last_name} -

-

{member.email}

-
+
+ +
+
+

Members

+ +
+ {!workspaceMembers || !workspaceInvitations ? ( + + + + + + + ) : ( +
+ {members.length > 0 + ? members.map((member) => ( +
+
+
+ {member.avatar && member.avatar !== "" ? ( + {member.first_name} + ) : member.first_name !== "" ? ( + member.first_name.charAt(0) + ) : ( + member.email.charAt(0) + )}
-
- {!member?.status && ( -
-

Pending

-
- )} - {member?.status && !member?.accountCreated && ( -
-

Account not created

-
- )} - { - if (!workspaceSlug) return; - - mutateMembers( - (prevData) => - prevData?.map((m) => - m.id === member.id ? { ...m, role: value } : m - ), - false - ); - - workspaceService - .updateWorkspaceMember(workspaceSlug?.toString(), member.id, { - role: value, - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: - "An error occurred while updating member role. Please try again.", - }); - }); - }} - position="right" - disabled={ - member.memberId === currentUser?.member.id || - !member.status || - (currentUser && - currentUser.role !== 20 && - currentUser.role < member.role) - } - > - {Object.keys(ROLE).map((key) => { - if ( - currentUser && - currentUser.role !== 20 && - currentUser.role < parseInt(key) - ) - return null; - - return ( - - <>{ROLE[parseInt(key) as keyof typeof ROLE]} - - ); - })} - - - { - if (member.member) { - setSelectedRemoveMember(member.id); - } else { - setSelectedInviteRemoveMember(member.id); - } - }} - > - Remove member - - +
+

+ {member.first_name} {member.last_name} +

+

{member.email}

- )) - : null} -
- )} -
-
- - +
+ {!member?.status && ( +
+

Pending

+
+ )} + {member?.status && !member?.accountCreated && ( +
+

Account not created

+
+ )} + { + if (!workspaceSlug) return; + + mutateMembers( + (prevData) => + prevData?.map((m) => + m.id === member.id ? { ...m, role: value } : m + ), + false + ); + + workspaceService + .updateWorkspaceMember(workspaceSlug?.toString(), member.id, { + role: value, + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: + "An error occurred while updating member role. Please try again.", + }); + }); + }} + position="right" + disabled={ + member.memberId === currentUser?.member.id || + !member.status || + (currentUser && + currentUser.role !== 20 && + currentUser.role < member.role) + } + > + {Object.keys(ROLE).map((key) => { + if ( + currentUser && + currentUser.role !== 20 && + currentUser.role < parseInt(key) + ) + return null; + + return ( + + <>{ROLE[parseInt(key) as keyof typeof ROLE]} + + ); + })} + + + { + if (member.member) { + setSelectedRemoveMember(member.id); + } else { + setSelectedInviteRemoveMember(member.id); + } + }} + > + Remove member + + +
+
+ )) + : null} +
+ )} +
+
+
); };