fix: workspace settings pages authorization (#2915)

* fix: workspace settings pages authorization

* chore: user cannot add a member with a higher role than theirs

* chore: update workspace general settings auth
This commit is contained in:
Aaryan Khandelwal 2023-11-28 17:05:42 +05:30 committed by sriram veeraghanta
parent 03387848fe
commit eb366887d7
15 changed files with 317 additions and 240 deletions

View file

@ -25,7 +25,6 @@ import {
IViewIssuesFilterStore,
IViewIssuesStore,
} from "store/issues";
import { EUserWorkspaceRoles } from "layouts/settings-layout/workspace/sidebar";
import { TUnGroupedIssues } from "store/issues/types";
interface IBaseGanttRoot {
@ -46,6 +45,10 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
const { projectDetails } = useProjectDetails();
const {
user: { currentProjectRole },
} = useMobxStore();
const appliedDisplayFilters = issueFiltersStore.issueFilters?.displayFilters;
const issuesResponse = issueStore.getIssues;
@ -69,7 +72,7 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
);
};
const isAllowed = (projectDetails?.member_role || 0) >= EUserWorkspaceRoles.MEMBER;
const isAllowed = currentProjectRole && currentProjectRole >= 15;
return (
<>

View file

@ -1,10 +1,12 @@
import React, { useEffect } from "react";
import { observer } from "mobx-react-lite";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { Dialog, Transition } from "@headlessui/react";
import { Plus, X } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// ui
import { Button, CustomSelect, Input } from "@plane/ui";
// icons
import { Plus, X } from "lucide-react";
// types
import { IWorkspaceBulkInviteFormData, TUserWorkspaceRole } from "types";
// constants
@ -34,9 +36,12 @@ const defaultValues: FormValues = {
],
};
export const SendWorkspaceInvitationModal: React.FC<Props> = (props) => {
export const SendWorkspaceInvitationModal: React.FC<Props> = observer((props) => {
const { isOpen, onClose, onSubmit } = props;
// mobx store
const {
user: { currentWorkspaceRole },
} = useMobxStore();
// form info
const {
control,
@ -59,30 +64,6 @@ export const SendWorkspaceInvitationModal: React.FC<Props> = (props) => {
}, 350);
};
// const onSubmit = async (formData: FormValues) => {
// if (!workspaceSlug) return;
// return workspaceService
// .inviteWorkspace(workspaceSlug, formData, user)
// .then(async () => {
// if (onSuccess) await onSuccess();
// handleClose();
// setToastAlert({
// type: "success",
// title: "Success!",
// message: "Invitations sent successfully.",
// });
// })
// .catch((err) =>
// setToastAlert({
// type: "error",
// title: "Error!",
// message: `${err.error ?? "Something went wrong. Please try again."}`,
// })
// );
// };
const appendField = () => {
append({ email: "", role: 15 });
};
@ -181,11 +162,14 @@ export const SendWorkspaceInvitationModal: React.FC<Props> = (props) => {
width="w-full"
input
>
{Object.entries(ROLE).map(([key, value]) => (
<CustomSelect.Option key={key} value={parseInt(key)}>
{value}
</CustomSelect.Option>
))}
{Object.entries(ROLE).map(([key, value]) => {
if (currentWorkspaceRole && currentWorkspaceRole >= parseInt(key))
return (
<CustomSelect.Option key={key} value={parseInt(key)}>
{value}
</CustomSelect.Option>
);
})}
</CustomSelect>
)}
/>
@ -230,4 +214,4 @@ export const SendWorkspaceInvitationModal: React.FC<Props> = (props) => {
</Dialog>
</Transition.Root>
);
};
});

View file

@ -1,7 +1,9 @@
import { useState, FC } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { mutate } from "swr";
import { ChevronDown, Dot, XCircle } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
@ -10,12 +12,10 @@ import useToast from "hooks/use-toast";
import { ConfirmWorkspaceMemberRemove } from "components/workspace";
// ui
import { CustomSelect, Tooltip } from "@plane/ui";
// icons
import { ChevronDown, Dot, XCircle } from "lucide-react";
// types
import { TUserWorkspaceRole } from "types";
// constants
import { ROLE } from "constants/workspace";
import { EUserWorkspaceRoles, ROLE } from "constants/workspace";
type Props = {
member: {
@ -33,7 +33,7 @@ type Props = {
};
};
export const WorkspaceMembersListItem: FC<Props> = (props) => {
export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
const { member } = props;
// router
const router = useRouter();
@ -43,7 +43,6 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
workspaceMember: { removeMember, updateMember, deleteWorkspaceInvitation },
user: { currentWorkspaceMemberInfo, currentWorkspaceRole, currentUser, currentUserSettings, leaveWorkspace },
} = useMobxStore();
const isAdmin = currentWorkspaceRole === 20;
// states
const [removeMemberModal, setRemoveMemberModal] = useState(false);
// hooks
@ -53,10 +52,7 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
if (!workspaceSlug || !currentUserSettings) return;
await leaveWorkspace(workspaceSlug.toString())
.then(() => {
if (currentUserSettings.workspace?.invites > 0) router.push("/invitations");
else router.push("/create-workspace");
})
.then(() => router.push("/profile"))
.catch((err) =>
setToastAlert({
type: "error",
@ -114,6 +110,20 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
} else await handleRemoveInvitation();
};
// is the member current logged in user
const isCurrentUser = member.memberId === currentWorkspaceMemberInfo?.member;
// is the current logged in user admin
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
// role change access-
// 1. user cannot change their own role
// 2. only admin or member can change role
// 3. user cannot change role of higher role
const hasRoleChangeAccess =
currentWorkspaceRole &&
!isCurrentUser &&
[EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER].includes(currentWorkspaceRole) &&
member.role <= currentWorkspaceRole;
if (!currentWorkspaceMemberInfo) return null;
return (
@ -180,12 +190,12 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
<div className="flex item-center gap-1 px-2 py-0.5 rounded">
<span
className={`flex items-center text-xs font-medium rounded ${
member.memberId !== currentWorkspaceMemberInfo.member ? "" : "text-custom-sidebar-text-400"
hasRoleChangeAccess ? "" : "text-custom-sidebar-text-400"
}`}
>
{ROLE[member.role as keyof typeof ROLE]}
</span>
{member.memberId !== currentWorkspaceMemberInfo.member && (
{hasRoleChangeAccess && (
<span className="grid place-items-center">
<ChevronDown className="h-3 w-3" />
</span>
@ -206,11 +216,7 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
});
});
}}
disabled={
member.memberId === currentWorkspaceMemberInfo.member ||
!member.status ||
Boolean(currentWorkspaceRole && currentWorkspaceRole !== 20 && currentWorkspaceRole < member.role)
}
disabled={!hasRoleChangeAccess}
placement="bottom-end"
>
{Object.keys(ROLE).map((key) => {
@ -224,23 +230,24 @@ export const WorkspaceMembersListItem: FC<Props> = (props) => {
);
})}
</CustomSelect>
{isAdmin && (
<Tooltip
tooltipContent={
member.memberId === currentWorkspaceMemberInfo.member ? "Leave workspace" : "Remove member"
<Tooltip
tooltipContent={isCurrentUser ? "Leave workspace" : "Remove member"}
disabled={!isAdmin && !isCurrentUser}
>
<button
type="button"
onClick={() => setRemoveMemberModal(true)}
className={
isAdmin || isCurrentUser
? "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto"
: "opacity-0 pointer-events-none"
}
>
<button
type="button"
onClick={() => setRemoveMemberModal(true)}
className="opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto"
>
<XCircle className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={2} />
</button>
</Tooltip>
)}
<XCircle className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={2} />
</button>
</Tooltip>
</div>
</div>
</>
);
};
});

View file

@ -9,18 +9,14 @@ import { WorkspaceMembersListItem } from "components/workspace";
// ui
import { Loader } from "@plane/ui";
export const WorkspaceMembersList: FC<{ searchQuery: string }> = observer(({ searchQuery }) => {
export const WorkspaceMembersList: FC<{ searchQuery: string }> = observer((props) => {
const { searchQuery } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
// store
const {
workspaceMember: {
workspaceMembers,
workspaceMembersWithInvitations,
workspaceMemberInvitations,
fetchWorkspaceMemberInvitations,
},
user: { currentWorkspaceMemberInfo },
workspaceMember: { workspaceMembersWithInvitations, fetchWorkspaceMemberInvitations },
} = useMobxStore();
// fetching workspace invitations
useSWR(
@ -36,12 +32,7 @@ export const WorkspaceMembersList: FC<{ searchQuery: string }> = observer(({ sea
return `${email}${displayName}${fullName}`.includes(searchQuery.toLowerCase());
});
if (
!workspaceMembers ||
!workspaceMemberInvitations ||
!workspaceMembersWithInvitations ||
!currentWorkspaceMemberInfo
)
if (!workspaceMembersWithInvitations)
return (
<Loader className="space-y-5">
<Loader.Item height="40px" />

View file

@ -14,12 +14,12 @@ import { DeleteWorkspaceModal } from "components/workspace";
import { WorkspaceImageUploadModal } from "components/core";
// ui
import { Button, CustomSelect, Input, Spinner } from "@plane/ui";
// helpers
import { copyUrlToClipboard } from "helpers/string.helper";
// types
import { IWorkspace } from "types";
// constants
import { ORGANIZATION_SIZE } from "constants/workspace";
import { trackEvent } from "helpers/event-tracker.helper";
import { copyUrlToClipboard } from "helpers/string.helper";
import { EUserWorkspaceRoles, ORGANIZATION_SIZE } from "constants/workspace";
const defaultValues: Partial<IWorkspace> = {
name: "",
@ -40,9 +40,9 @@ export const WorkspaceDetails: FC = observer(() => {
const {
workspace: { currentWorkspace, updateWorkspace },
user: { currentWorkspaceRole },
trackEvent: { postHogEventTracker }
trackEvent: { postHogEventTracker },
} = useMobxStore();
const isAdmin = currentWorkspaceRole === 20;
// hooks
const { setToastAlert } = useToast();
// form info
@ -67,28 +67,22 @@ export const WorkspaceDetails: FC = observer(() => {
await updateWorkspace(currentWorkspace.slug, payload)
.then((res) => {
postHogEventTracker(
'WORKSPACE_UPDATE',
{
...res,
state: "SUCCESS"
}
)
postHogEventTracker("WORKSPACE_UPDATE", {
...res,
state: "SUCCESS",
});
setToastAlert({
title: "Success",
type: "success",
message: "Workspace updated successfully",
});
}).catch((err) => {
postHogEventTracker(
'WORKSPACE_UPDATE',
{
state: "FAILED"
}
);
console.error(err)
}
);
})
.catch((err) => {
postHogEventTracker("WORKSPACE_UPDATE", {
state: "FAILED",
});
console.error(err);
});
};
const handleRemoveLogo = () => {
@ -136,6 +130,8 @@ export const WorkspaceDetails: FC = observer(() => {
if (currentWorkspace) reset({ ...currentWorkspace });
}, [currentWorkspace, reset]);
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
if (!currentWorkspace)
return (
<div className="grid place-items-center h-full w-full px-4 sm:px-0">
@ -192,11 +188,10 @@ export const WorkspaceDetails: FC = observer(() => {
<button type="button" onClick={handleCopyUrl} className="text-sm tracking-tight">{`${
typeof window !== "undefined" && window.location.origin.replace("http://", "").replace("https://", "")
}/${currentWorkspace.slug}`}</button>
<div className="flex item-center gap-2.5">
{isAdmin && (
<button
className="flex items-center gap-1.5 text-xs text-left text-custom-primary-100 font-medium"
onClick={() => setIsImageUploadModalOpen(true)}
disabled={!isAdmin}
>
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
<>
@ -207,14 +202,14 @@ export const WorkspaceDetails: FC = observer(() => {
"Upload logo"
)}
</button>
</div>
)}
</div>
</div>
<div className="flex flex-col gap-8 my-10">
<div className="grid grid-col grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 items-center justify-between gap-10 w-full">
<div className="flex flex-col gap-1 ">
<h4 className="text-sm">Workspace Name</h4>
<div className="flex flex-col gap-1">
<h4 className="text-sm">Workspace name</h4>
<Controller
control={control}
name="name"
@ -243,7 +238,7 @@ export const WorkspaceDetails: FC = observer(() => {
</div>
<div className="flex flex-col gap-1 ">
<h4 className="text-sm">Company Size</h4>
<h4 className="text-sm">Company size</h4>
<Controller
name="organization_size"
control={control}
@ -277,9 +272,10 @@ export const WorkspaceDetails: FC = observer(() => {
id="url"
name="url"
type="url"
value={`${typeof window !== "undefined" &&
value={`${
typeof window !== "undefined" &&
window.location.origin.replace("http://", "").replace("https://", "")
}/${currentWorkspace.slug}`}
}/${currentWorkspace.slug}`}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.url)}
@ -291,11 +287,13 @@ export const WorkspaceDetails: FC = observer(() => {
</div>
</div>
<div className="flex items-center justify-between py-2">
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isAdmin}>
{isSubmitting ? "Updating..." : "Update Workspace"}
</Button>
</div>
{isAdmin && (
<div className="flex items-center justify-between py-2">
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Updating..." : "Update Workspace"}
</Button>
</div>
)}
</div>
{isAdmin && (
<Disclosure as="div" className="border-t border-custom-border-100">