diff --git a/packages/features/ee/teams/components/MemberInvitationModal.tsx b/packages/features/ee/teams/components/MemberInvitationModal.tsx index 6d555450b8..79b5cc03b6 100644 --- a/packages/features/ee/teams/components/MemberInvitationModal.tsx +++ b/packages/features/ee/teams/components/MemberInvitationModal.tsx @@ -1,4 +1,5 @@ import { BuildingIcon, PaperclipIcon, UserIcon, Users } from "lucide-react"; +import { useSession } from "next-auth/react"; import { Trans } from "next-i18next"; import { useMemo, useState, useRef } from "react"; import type { FormEvent } from "react"; @@ -70,6 +71,11 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps) const { t } = useLocale(); const { disableCopyLink = false, isOrg = false } = props; const trpcContext = trpc.useContext(); + const session = useSession(); + const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { + enabled: !!session.data?.user?.org, + }); + const isOrgOwner = currentOrg && currentOrg.user.role === MembershipRole.OWNER; const [modalImportMode, setModalInputMode] = useState( props?.orgMembers && props.orgMembers?.length > 0 ? "ORGANIZATION" : "INDIVIDUAL" @@ -96,12 +102,19 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps) }; const options: MembershipRoleOption[] = useMemo(() => { - return [ + const options: MembershipRoleOption[] = [ { value: MembershipRole.MEMBER, label: t("member") }, { value: MembershipRole.ADMIN, label: t("admin") }, { value: MembershipRole.OWNER, label: t("owner") }, ]; - }, [t]); + + // Adjust options for organizations where the user isn't the owner + if (isOrg && !isOrgOwner) { + return options.filter((option) => option.value !== MembershipRole.OWNER); + } + + return options; + }, [t, isOrgOwner, isOrg]); const toggleGroupOptions = useMemo(() => { const array = [ diff --git a/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts b/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts index 35e882bdf8..6417c563a6 100644 --- a/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts @@ -2,9 +2,13 @@ import { updateQuantitySubscriptionFromStripe } from "@calcom/features/ee/teams/ import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants"; import { getTranslation } from "@calcom/lib/server/i18n"; +import { isOrganisationOwner } from "@calcom/lib/server/queries/organisations"; import { prisma } from "@calcom/prisma"; +import { MembershipRole } from "@calcom/prisma/enums"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; +import { TRPCError } from "@trpc/server"; + import type { TInviteMemberInputSchema } from "./inviteMember.schema"; import { checkPermissions, @@ -40,6 +44,14 @@ export const inviteMemberHandler = async ({ ctx, input }: InviteMemberOptions) = isOrg: input.isOrg, }); + // Only owners can award owner role in an organization. + if ( + input.isOrg && + input.role === MembershipRole.OWNER && + !(await isOrganisationOwner(ctx.user.id, input.teamId)) + ) + throw new TRPCError({ code: "UNAUTHORIZED" }); + const team = await getTeamOrThrow(input.teamId, input.isOrg); const { autoAcceptEmailDomain, orgVerified } = getIsOrgVerified(input.isOrg, team); const usernameOrEmailsToInvite = await getUsernameOrEmailsToInvite(input.usernameOrEmail);