Skip to content

Commit 3bdf4a8

Browse files
sean-brydonkodiakhq[bot]zomars
authored
Prevent team members from creating events (#3498)
* Delete CreateEventTypeBtn.tsx * Adding Permission to creation router * Disable button if membership role is member * Fix linting error * Update explicit role selection Co-authored-by: Omar López <zomars@me.com> * Type fix Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Omar López <zomars@me.com>
1 parent 7a30690 commit 3bdf4a8

File tree

2 files changed

+92
-36
lines changed

2 files changed

+92
-36
lines changed

apps/web/components/eventtype/CreateEventType.tsx

+91-35
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { ChevronDownIcon, PlusIcon } from "@heroicons/react/solid";
22
import { zodResolver } from "@hookform/resolvers/zod";
33
import { SchedulingType } from "@prisma/client";
4+
import { useSession } from "next-auth/react";
45
import { useRouter } from "next/router";
56
import { useEffect } from "react";
67
import { useForm } from "react-hook-form";
78
import type { z } from "zod";
89

10+
import classNames from "@calcom/lib/classNames";
911
import { WEBAPP_URL } from "@calcom/lib/constants";
1012
import { useLocale } from "@calcom/lib/hooks/useLocale";
1113
import showToast from "@calcom/lib/notification";
@@ -37,7 +39,7 @@ export interface EventTypeParent {
3739
image?: string | null;
3840
}
3941

40-
interface Props {
42+
interface CreateEventTypeBtnProps {
4143
// set true for use on the team settings page
4244
canAddEvents: boolean;
4345
// set true when in use on the team settings page
@@ -46,7 +48,7 @@ interface Props {
4648
options: EventTypeParent[];
4749
}
4850

49-
export default function CreateEventTypeButton(props: Props) {
51+
export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
5052
const { t } = useLocale();
5153
const router = useRouter();
5254

@@ -142,39 +144,13 @@ export default function CreateEventTypeButton(props: Props) {
142144
<Dialog
143145
name="new-eventtype"
144146
clearQueryParamsOnClose={["eventPage", "teamId", "type", "description", "title", "length", "slug"]}>
145-
{!hasTeams || props.isIndividualTeam ? (
146-
<Button
147-
onClick={() => openModal(props.options[0])}
148-
data-testid="new-event-type"
149-
StartIcon={PlusIcon}
150-
disabled={!props.canAddEvents}>
151-
{t("new_event_type_btn")}
152-
</Button>
153-
) : (
154-
<Dropdown>
155-
<DropdownMenuTrigger asChild>
156-
<Button EndIcon={ChevronDownIcon}>{t("new_event_type_btn")}</Button>
157-
</DropdownMenuTrigger>
158-
<DropdownMenuContent align="end">
159-
<DropdownMenuLabel>{t("new_event_subtitle")}</DropdownMenuLabel>
160-
<DropdownMenuSeparator className="h-px bg-gray-200" />
161-
{props.options.map((option) => (
162-
<DropdownMenuItem
163-
key={option.slug}
164-
className="cursor-pointer px-3 py-2 hover:bg-neutral-100 focus:outline-none"
165-
onSelect={() => openModal(option)}>
166-
<Avatar
167-
alt={option.name || ""}
168-
imageSrc={option.image || `${WEBAPP_URL}/${option.slug}/avatar.png`} // if no image, use default avatar
169-
size={6}
170-
className="inline ltr:mr-2 rtl:ml-2"
171-
/>
172-
{option.name ? option.name : option.slug}
173-
</DropdownMenuItem>
174-
))}
175-
</DropdownMenuContent>
176-
</Dropdown>
177-
)}
147+
<CreateEventTypeTrigger
148+
hasTeams={hasTeams}
149+
canAddEvents={props.canAddEvents}
150+
isIndividualTeam={props.isIndividualTeam}
151+
openModal={openModal}
152+
options={props.options}
153+
/>
178154

179155
<DialogContent className="overflow-y-auto">
180156
<div className="mb-4">
@@ -290,3 +266,83 @@ export default function CreateEventTypeButton(props: Props) {
290266
</Dialog>
291267
);
292268
}
269+
270+
type CreateEventTypeTrigger = {
271+
isIndividualTeam?: boolean;
272+
// EventTypeParent can be a profile (as first option) or a team for the rest.
273+
options: EventTypeParent[];
274+
hasTeams: boolean;
275+
// set true for use on the team settings page
276+
canAddEvents: boolean;
277+
openModal: (option: EventTypeParent) => void;
278+
};
279+
280+
export function CreateEventTypeTrigger(props: CreateEventTypeTrigger) {
281+
const { t } = useLocale();
282+
283+
return (
284+
<>
285+
{!props.hasTeams || props.isIndividualTeam ? (
286+
<Button
287+
onClick={() => props.openModal(props.options[0])}
288+
data-testid="new-event-type"
289+
StartIcon={PlusIcon}
290+
disabled={!props.canAddEvents}>
291+
{t("new_event_type_btn")}
292+
</Button>
293+
) : (
294+
<Dropdown>
295+
<DropdownMenuTrigger asChild>
296+
<Button EndIcon={ChevronDownIcon}>{t("new_event_type_btn")}</Button>
297+
</DropdownMenuTrigger>
298+
<DropdownMenuContent align="end">
299+
<DropdownMenuLabel>{t("new_event_subtitle")}</DropdownMenuLabel>
300+
<DropdownMenuSeparator className="h-px bg-gray-200" />
301+
{props.options.map((option) => (
302+
<CreateEventTeamsItem
303+
key={option.slug}
304+
option={option}
305+
openModal={() => props.openModal(option)}
306+
/>
307+
))}
308+
</DropdownMenuContent>
309+
</Dropdown>
310+
)}
311+
</>
312+
);
313+
}
314+
315+
function CreateEventTeamsItem(props: {
316+
openModal: (option: EventTypeParent) => void;
317+
option: EventTypeParent;
318+
}) {
319+
const session = useSession();
320+
const membershipQuery = trpc.useQuery([
321+
"viewer.teams.getMembershipbyUser",
322+
{
323+
memberId: session.data?.user.id as number,
324+
teamId: props.option.teamId as number,
325+
},
326+
]);
327+
328+
const isDisabled = membershipQuery.data?.role === "MEMBER";
329+
330+
return (
331+
<DropdownMenuItem
332+
key={props.option.slug}
333+
className={classNames(
334+
"cursor-pointer px-3 py-2 focus:outline-none",
335+
isDisabled ? "cursor-default !text-gray-300" : "hover:bg-neutral-100"
336+
)}
337+
disabled={isDisabled}
338+
onSelect={() => props.openModal(props.option)}>
339+
<Avatar
340+
alt={props.option.name || ""}
341+
imageSrc={props.option.image}
342+
size={6}
343+
className="inline ltr:mr-2 rtl:ml-2"
344+
/>
345+
{props.option.name ? props.option.name : props.option.slug}
346+
</DropdownMenuItem>
347+
);
348+
}

packages/trpc/server/routers/viewer/eventTypes.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export const eventTypesRouter = createProtectedRouter()
139139
},
140140
});
141141

142-
if (!hasMembership) {
142+
if (!hasMembership?.role || !["ADMIN", "OWNER"].includes(hasMembership.role)) {
143143
console.warn(`User ${userId} does not have permission to create this new event type`);
144144
throw new TRPCError({ code: "UNAUTHORIZED" });
145145
}

0 commit comments

Comments
 (0)