fix: Orgs/create child teams CAL-1986 (#9631)

* Re-applied valid changes

* Update packages/trpc/server/middlewares/sessionMiddleware.ts

* Type fix

* Type fix

---------

Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
Joe Au-Yeung 2023-07-07 12:48:51 -04:00 committed by GitHub
parent 936432a730
commit 75f76c130a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 79 additions and 57 deletions

View File

@ -1,41 +1,47 @@
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import type { GetServerSidePropsContext } from "next";
import { TeamsListing } from "@calcom/features/ee/teams/components";
import Shell from "@calcom/features/shell/Shell";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui";
import { Plus } from "@calcom/ui/components/icon";
import PageWrapper from "@components/PageWrapper";
import { ssrInit } from "@server/lib/ssr";
function Teams() {
const { t } = useLocale();
const [user] = trpc.viewer.me.useSuspenseQuery();
return (
<Shell
heading={t("teams")}
hideHeadingOnMobile
subtitle={t("create_manage_teams_collaborative")}
CTA={
<Button
variant="fab"
StartIcon={Plus}
type="button"
href={`${WEBAPP_URL}/settings/teams/new?returnTo=${WEBAPP_URL}/teams`}>
{t("new")}
</Button>
(!user.organizationId || user.organization.isOrgAdmin) && (
<Button
variant="fab"
StartIcon={Plus}
type="button"
href={`${WEBAPP_URL}/settings/teams/new?returnTo=${WEBAPP_URL}/teams`}>
{t("new")}
</Button>
)
}>
<TeamsListing />
</Shell>
);
}
export const getStaticProps = async () => {
return {
props: {
...(await serverSideTranslations("en", ["common"])),
},
};
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
await ssr.viewer.me.prefetch();
return { props: { trpcState: ssr.dehydrate() } };
};
Teams.requiresLicense = false;

View File

@ -1923,6 +1923,7 @@
"crm": "CRM",
"messaging": "Messaging",
"sender_id_info": "Name or number shown as the sender of an SMS (some countries do not allow alphanumeric sender IDs)",
"org_admins_can_create_new_teams": "Only the admin of your organization can create new teams",
"google_new_spam_policy": "Googles new spam policy could prevent you from receiving any email and calendar notifications about this booking.",
"resolve": "Resolve",
"no_organization_slug": "There was an error creating teams for this organization. Missing URL slug.",

View File

@ -1,10 +1,10 @@
import { useRouter } from "next/router";
import { useEffect, useMemo, useState } from "react";
import { WEBAPP_URL, APP_NAME } from "@calcom/lib/constants";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Alert, Button, ButtonGroup, Label, showToast } from "@calcom/ui";
import { Alert, Label, showToast, ButtonGroup, Button } from "@calcom/ui";
import { EyeOff, Mail, RefreshCcw, UserPlus, Users, Video } from "@calcom/ui/components/icon";
import { UpgradeTip } from "../../../tips";
@ -26,6 +26,8 @@ export function TeamsListing() {
},
});
const { data: user } = trpc.viewer.me.useQuery();
const { mutate: inviteMemberByToken } = trpc.viewer.teams.inviteMemberByToken.useMutation({
onSuccess: (teamName) => {
trpcContext.viewer.teams.list.invalidate();
@ -102,16 +104,20 @@ export function TeamsListing() {
features={features}
background="/tips/teams"
buttons={
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
<ButtonGroup>
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
{t("create_team")}
</Button>
<Button color="minimal" href="https://go.cal.com/teams-video" target="_blank">
{t("learn_more")}
</Button>
</ButtonGroup>
</div>
!user?.organizationId || user?.organization.isOrgAdmin ? (
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
<ButtonGroup>
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
{t("create_team")}
</Button>
<Button color="minimal" href="https://go.cal.com/teams-video" target="_blank">
{t("learn_more")}
</Button>
</ButtonGroup>
</div>
) : (
<p>{t("org_admins_can_create_new_teams")}</p>
)
}>
{teams.length > 0 ? <TeamList teams={teams} /> : <></>}
</UpgradeTip>

View File

@ -284,6 +284,9 @@ const SettingsSidebarContainer = ({
{teams &&
teamMenuState &&
teams.map((team, index: number) => {
if (!teamMenuState[index]) {
return null;
}
if (teamMenuState.some((teamState) => teamState.teamId === team.id))
return (
<Collapsible

View File

@ -2,6 +2,7 @@ import type { Session } from "next-auth";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { defaultAvatarSrc } from "@calcom/lib/defaultAvatarImage";
import { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
import type { Maybe } from "@trpc/server";
@ -77,6 +78,13 @@ export async function getUserFromSession(ctx: TRPCContextInner, session: Maybe<S
id: true,
slug: true,
metadata: true,
members: {
select: { userId: true },
where: {
userId: session.user.id,
OR: [{ role: MembershipRole.ADMIN }, { role: MembershipRole.OWNER }],
},
},
},
},
},
@ -98,10 +106,17 @@ export async function getUserFromSession(ctx: TRPCContextInner, session: Maybe<S
// This helps to prevent reaching the 4MB payload limit by avoiding base64 and instead passing the avatar url
user.avatar = rawAvatar ? `${WEBAPP_URL}/${user.username}/avatar.png` : defaultAvatarSrc({ email });
const locale = user?.locale || ctx.locale;
const isOrgAdmin = !!user.organization?.members.length;
// Want to reduce the amount of data being sent
if (isOrgAdmin && user.organization?.members) {
user.organization.members = [];
}
return {
...user,
organization: {
...user.organization,
isOrgAdmin,
metadata: orgMetadata,
},
id,

View File

@ -1,5 +1,3 @@
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
@ -17,21 +15,36 @@ type CreateOptions = {
};
export const createHandler = async ({ ctx, input }: CreateOptions) => {
const { user } = ctx;
const { slug, name, logo } = input;
const currentOrgId = ctx.user.organization?.id;
const isOrgChildTeam = !!user.organizationId;
// For orgs we want to create teams under the org
if (user.organizationId && !user.organization.isOrgAdmin) {
throw new TRPCError({ code: "FORBIDDEN", message: "org_admins_can_create_new_teams" });
}
const slugCollisions = await prisma.team.findFirst({
where: {
slug: slug,
parentId: currentOrgId
? {
equals: currentOrgId,
}
: null,
// If this is under an org, check that the team doesn't already exist
...(isOrgChildTeam && { parentId: user.organizationId }),
},
});
if (slugCollisions) throw new TRPCError({ code: "BAD_REQUEST", message: "team_url_taken" });
if (user.organizationId) {
const nameCollisions = await prisma.user.findFirst({
where: {
organizationId: user.organization.id,
username: slug,
},
});
if (nameCollisions) throw new TRPCError({ code: "BAD_REQUEST", message: "team_slug_exists_as_user" });
}
// Ensure that the user is not duplicating a requested team
const duplicatedRequest = await prisma.team.findFirst({
where: {
@ -51,25 +64,6 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => {
return duplicatedRequest;
}
let parentId: number | null = null;
// If the user in session is part of an org. check permissions
if (ctx.user.organization?.id) {
if (!isOrganisationAdmin(ctx.user.id, ctx.user.organization.id)) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const nameCollisions = await prisma.user.findFirst({
where: {
organizationId: ctx.user.organization.id,
username: slug,
},
});
if (nameCollisions) throw new TRPCError({ code: "BAD_REQUEST", message: "team_slug_exists_as_user" });
parentId = ctx.user.organization.id;
}
const createTeam = await prisma.team.create({
data: {
name,
@ -84,10 +78,7 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => {
metadata: {
requestedSlug: slug,
},
...(!IS_TEAM_BILLING_ENABLED && { slug }),
parent: {
connect: parentId ? { id: parentId } : undefined,
},
...(isOrgChildTeam && { parentId: user.organizationId }),
},
});