Revert "chore: Chore team metadata table + isOrganization migration from metadata (#12828)"

This reverts commit aabfd3b3f6.
This commit is contained in:
zomars
2024-01-16 12:20:51 -07:00
parent 8f0ff58bcf
commit 2408338ed4
46 changed files with 249 additions and 323 deletions
+10 -16
View File
@@ -696,7 +696,7 @@ describe("orgMigration", () => {
});
describe("when user email matches orgAutoAcceptEmail", () => {
const orgSettings = {
const orgMetadata = {
orgAutoAcceptEmail: "org1.com",
} as const;
@@ -725,10 +725,8 @@ describe("orgMigration", () => {
const dbOrg = await createOrg({
slug: data.targetOrg.slug,
name: data.targetOrg.name,
organizationSettings: {
create: {
...orgSettings,
},
metadata: {
...orgMetadata,
},
});
@@ -786,10 +784,8 @@ describe("orgMigration", () => {
const dbOrg = await createOrg({
slug: data.targetOrg.slug,
name: data.targetOrg.name,
organizationSettings: {
create: {
...orgSettings,
},
metadata: {
...orgMetadata,
},
});
@@ -841,11 +837,7 @@ describe("orgMigration", () => {
name: data.targetOrg.name,
metadata: {
requestedSlug: data.targetOrg.requestedSlug,
},
organizationSettings: {
create: {
...orgSettings,
},
...orgMetadata,
},
});
@@ -909,7 +901,9 @@ describe("orgMigration", () => {
id: data.targetOrg.id,
slug: data.targetOrg.slug,
name: data.targetOrg.name,
isOrganization: true,
metadata: {
isOrganization: true,
},
},
});
@@ -1570,9 +1564,9 @@ async function createOrg(
return await prismock.team.create({
data: {
...data,
isOrganization: true,
metadata: {
...(data.metadata || {}),
isOrganization: true,
},
},
});
+8 -15
View File
@@ -1,6 +1,5 @@
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { isOrganization } from "@calcom/lib/entityPermissionUtils";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
@@ -47,7 +46,7 @@ export async function moveUserToOrg({
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
if (!isOrganization({ team })) {
if (!teamMetadata?.isOrganization) {
throw new Error(`Team with ID:${targetOrgId} is not an Org`);
}
@@ -61,7 +60,7 @@ export async function moveUserToOrg({
if (!targetOrgUsername) {
targetOrgUsername = getOrgUsernameFromEmail(
userToMoveToOrg.email,
team.organizationSettings?.orgAutoAcceptEmail || ""
targetOrganization.metadata.orgAutoAcceptEmail || ""
);
}
@@ -196,7 +195,7 @@ export async function moveTeamToOrg({
const teamMetadata = teamMetadataSchema.parse(possibleOrg?.metadata);
if (!isOrganization({ team: possibleOrg })) {
if (!teamMetadata?.isOrganization) {
throw new Error(`${targetOrg.id} is not an Org`);
}
@@ -205,7 +204,7 @@ export async function moveTeamToOrg({
await addTeamRedirect({
oldTeamSlug,
teamSlug: updatedTeam.slug,
orgSlug: targetOrganization.slug || orgMetadata?.requestedSlug || null,
orgSlug: targetOrganization.slug || orgMetadata.requestedSlug || null,
});
await setOrgSlugIfNotSet({ slug: targetOrganization.slug }, orgMetadata, targetOrg.id);
if (moveMembers) {
@@ -329,14 +328,13 @@ async function setOrgSlugIfNotSet(
},
orgMetadata: {
requestedSlug?: string | undefined;
} | null,
},
targetOrgId: number
) {
if (targetOrganization.slug) {
return;
}
if (!orgMetadata?.requestedSlug) {
if (!orgMetadata.requestedSlug) {
throw new HttpError({
statusCode: 400,
message: `Org with id: ${targetOrgId} doesn't have a slug. Tried using requestedSlug but that's also not present. So, all migration done but failed to set the Organization slug. Please set it manually`,
@@ -373,9 +371,6 @@ async function getTeamOrThrowError(targetOrgId: number) {
where: {
id: targetOrgId,
},
include: {
organizationSettings: true,
},
});
if (!team) {
@@ -625,7 +620,6 @@ async function moveTeamsWithoutMembersToOrg({
id: true,
slug: true,
metadata: true,
isOrganization: true,
},
});
@@ -637,7 +631,7 @@ async function moveTeamsWithoutMembersToOrg({
};
})
// Remove Orgs from the list
.filter((team) => !isOrganization({ team }));
.filter((team) => !team.metadata?.isOrganization);
const teamIdsToBeMovedToOrg = teamsToBeMovedToOrg.map((t) => t.id);
@@ -790,7 +784,6 @@ async function removeTeamsWithoutItsMemberFromOrg({ userToRemoveFromOrg }: { use
id: true,
slug: true,
metadata: true,
isOrganization: true,
},
});
@@ -802,7 +795,7 @@ async function removeTeamsWithoutItsMemberFromOrg({ userToRemoveFromOrg }: { use
};
})
// Remove Orgs from the list
.filter((team) => !isOrganization({ team }));
.filter((team) => !team.metadata?.isOrganization);
const teamIdsToBeRemovedFromOrg = teamsToBeRemovedFromOrg.map((t) => t.id);
+8 -8
View File
@@ -610,16 +610,14 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
team: {
select: {
metadata: true,
isOrganization: true,
parentId: true,
parent: {
select: {
slug: true,
organizationSettings: true,
metadata: true,
},
},
slug: true,
organizationSettings: true,
},
},
},
@@ -668,8 +666,8 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
};
const isATeamInOrganization = tokenTeam?.parentId !== null;
const isOrganization = tokenTeam.metadata?.isOrganization;
// Detect if the team is an org by either the metadata flag or if it has a parent team
const isOrganization = tokenTeam.isOrganization;
const isOrganizationOrATeamInOrganization = isOrganization || isATeamInOrganization;
// If we are dealing with an org, the slug may come from the team itself or its parent
const orgSlug = isOrganizationOrATeamInOrganization
@@ -686,7 +684,9 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const isValidEmail = checkValidEmail(verificationToken.identifier);
const isOrgInviteByLink = isOrganizationOrATeamInOrganization && !isValidEmail;
const parentOrgSettings = tokenTeam?.parent?.organizationSettings ?? null;
const parentMetaDataForSubteam = tokenTeam?.parent?.metadata
? teamMetadataSchema.parse(tokenTeam.parent.metadata)
: null;
return {
props: {
@@ -699,15 +699,15 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
? getOrgUsernameFromEmail(
verificationToken.identifier,
(isOrganization
? tokenTeam.organizationSettings?.orgAutoAcceptEmail
: parentOrgSettings?.orgAutoAcceptEmail) || ""
? tokenTeam.metadata?.orgAutoAcceptEmail
: parentMetaDataForSubteam?.orgAutoAcceptEmail) || ""
)
: slugify(username),
}
: null,
orgSlug,
orgAutoAcceptEmail: isOrgInviteByLink
? tokenTeam?.organizationSettings?.orgAutoAcceptEmail ?? parentOrgSettings?.orgAutoAcceptEmail ?? null
? tokenTeam?.metadata?.orgAutoAcceptEmail ?? parentMetaDataForSubteam?.orgAutoAcceptEmail ?? null
: null,
},
};
+5 -5
View File
@@ -74,7 +74,7 @@ function TeamPage({
return (
<div className="flex h-full min-h-[100dvh] items-center justify-center">
<UnpublishedEntity
{...(team?.isOrganization || team.parentId ? { orgSlug: slug } : { teamSlug: slug })}
{...(metadata?.isOrganization || team.parentId ? { orgSlug: slug } : { teamSlug: slug })}
name={teamName}
/>
</div>
@@ -178,7 +178,7 @@ function TeamPage({
title: markdownStrippedBio,
profile: {
name: `${team.name}`,
image: `${WEBAPP_URL}/${team.isOrganization ? "org" : "team"}/${team.slug}/avatar.png`,
image: `${WEBAPP_URL}/${team.metadata?.isOrganization ? "org" : "team"}/${team.slug}/avatar.png`,
},
}}
/>
@@ -190,7 +190,7 @@ function TeamPage({
imageSrc={
isValidOrgDomain
? `/org/${currentOrgDomain}/avatar.png`
: `${WEBAPP_URL}/${team.isOrganization ? "org" : "team"}/${team.slug}/avatar.png`
: `${WEBAPP_URL}/${team.metadata?.isOrganization ? "org" : "team"}/${team.slug}/avatar.png`
}
size="lg"
/>
@@ -208,7 +208,7 @@ function TeamPage({
</>
)}
</div>
{team?.isOrganization ? (
{metadata?.isOrganization ? (
<SubTeams />
) : (
<>
@@ -311,7 +311,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
// Taking care of sub-teams and orgs
if (
(!isValidOrgDomain && team?.parent) ||
(!isValidOrgDomain && !!team?.isOrganization) ||
(!isValidOrgDomain && !!metadata?.isOrganization) ||
!isOrganizationFeatureEnabled
) {
return { notFound: true } as const;
+1 -1
View File
@@ -43,8 +43,8 @@ async function createOrgInDb({
data: {
name: name,
slug: slug,
isOrganization: true,
metadata: {
isOrganization: true,
...(requestedSlug
? {
requestedSlug,
+13 -13
View File
@@ -118,21 +118,18 @@ const createTeamAndAddUser = async (
const slug = `${isOrg ? "org" : "team"}-${workerInfo.workerIndex}-${Date.now()}`;
const data: PrismaType.TeamCreateInput = {
name: `user-id-${user.id}'s ${isOrg ? "Org" : "Team"}`,
isOrganization: isOrg,
};
data.metadata = {
...(isUnpublished ? { requestedSlug: slug } : {}),
...(isOrg
? {
isOrganization: true,
isOrganizationVerified: !!isOrgVerified,
orgAutoAcceptEmail: user.email.split("@")[1],
isOrganizationConfigured: false,
}
: {}),
};
if (isOrg) {
data.organizationSettings = {
create: {
isOrganizationVerified: !!isOrgVerified,
orgAutoAcceptEmail: user.email.split("@")[1],
isOrganizationConfigured: false,
},
};
}
data.slug = !isUnpublished ? slug : undefined;
if (isOrg && hasSubteam) {
const team = await createTeamAndAddUser({ user }, workerInfo);
@@ -537,7 +534,7 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
},
};
})
.find((membership) => !membership.team.isOrganization);
.find((membership) => !membership.team?.metadata?.isOrganization);
if (!membership) {
throw new Error("No team found for user");
}
@@ -548,7 +545,10 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
where: {
userId: user.id,
team: {
isOrganization: true,
metadata: {
path: ["isOrganization"],
equals: true,
},
},
},
include: { team: { include: { children: true } } },
@@ -61,10 +61,20 @@ const checkIfUserShouldBelongToOrg = async (idP: IdentityProvider, email: string
if (!ORGANIZATIONS_AUTOLINK || idP !== "GOOGLE") return { orgUsername, orgId: undefined };
const existingOrg = await prisma.team.findFirst({
where: {
organizationSettings: {
isOrganizationVerified: true,
orgAutoAcceptEmail: apexDomain,
},
AND: [
{
metadata: {
path: ["isOrganizationVerified"],
equals: true,
},
},
{
metadata: {
path: ["orgAutoAcceptEmail"],
equals: apexDomain,
},
},
],
},
select: {
id: true,
@@ -14,7 +14,7 @@ import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
import { validateAndGetCorrectedUsernameAndEmail } from "@calcom/lib/validateUsername";
import { prisma } from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
import { signupSchema } from "@calcom/prisma/zod-utils";
import { signupSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { joinAnyChildTeamOnOrgInvite } from "../utils/organization";
import {
@@ -129,6 +129,8 @@ async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) {
},
});
if (team) {
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
const user = await prisma.user.upsert({
where: { email },
update: {
@@ -147,6 +149,7 @@ async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) {
// Wrapping in a transaction as if one fails we want to rollback the whole thing to preventa any data inconsistencies
const { membership } = await createOrUpdateMemberships({
teamMetadata,
user,
team,
});
@@ -12,6 +12,7 @@ import { validateAndGetCorrectedUsernameAndEmail } from "@calcom/lib/validateUse
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
import { signupSchema } from "@calcom/prisma/zod-utils";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { joinAnyChildTeamOnOrgInvite } from "../utils/organization";
import {
@@ -68,6 +69,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
});
if (team) {
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
const user = await prisma.user.upsert({
where: { email: userEmail },
update: {
@@ -85,6 +88,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
const { membership } = await createOrUpdateMemberships({
teamMetadata,
user,
team,
});
@@ -1,16 +1,21 @@
import type z from "zod";
import { prisma } from "@calcom/prisma";
import type { Team, User } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import type { teamMetadataSchema } from "@calcom/prisma/zod-utils";
export const createOrUpdateMemberships = async ({
teamMetadata,
user,
team,
}: {
user: Pick<User, "id">;
team: Pick<Team, "id" | "parentId" | "isOrganization">;
team: Pick<Team, "id" | "parentId">;
teamMetadata: z.infer<typeof teamMetadataSchema>;
}) => {
return await prisma.$transaction(async (tx) => {
if (team.isOrganization) {
if (teamMetadata?.isOrganization) {
await tx.user.update({
where: {
id: user.id,
@@ -4,6 +4,7 @@ import z from "zod";
import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
const querySchema = z.object({
org: z.string({ required_error: "org slug is required" }),
@@ -19,16 +20,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
} = parsedQuery;
if (!slug) return res.status(400).json({ message: "Org is needed" });
const org = await prisma.team.findFirst({
where: { slug },
select: { children: true, isOrganization: true },
});
const org = await prisma.team.findFirst({ where: { slug }, select: { children: true, metadata: true } });
if (!org) return res.status(400).json({ message: "Org doesn't exist" });
const isOrganization = org.isOrganization;
const metadata = teamMetadataSchema.parse(org?.metadata);
if (!isOrganization) return res.status(400).json({ message: "Team is not an org" });
if (!metadata?.isOrganization) return res.status(400).json({ message: "Team is not an org" });
return res.status(200).json({ slugs: org.children.map((ch) => ch.slug) });
}
@@ -139,7 +139,10 @@ export function whereClauseForOrgWithSlugOrRequestedSlug(slug: string) {
},
},
],
isOrganization: true,
metadata: {
path: ["isOrganization"],
equals: true,
},
} satisfies Prisma.TeamWhereInput;
}
@@ -6,7 +6,7 @@ import { z } from "zod";
import NoSSR from "@calcom/core/components/NoSSR";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import type { orgSettingsSchema } from "@calcom/prisma/zod-utils";
import type { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import { Button, Form, Meta, TextField, showToast } from "@calcom/ui";
@@ -43,7 +43,7 @@ const OrgEditView = ({ orgId }: { orgId: number }) => {
type FormValues = {
name: Team["name"];
slug: Team["slug"];
organizationSettings: z.infer<typeof orgSettingsSchema>;
metadata: z.infer<typeof teamMetadataSchema>;
};
const OrgForm = ({
@@ -81,10 +81,6 @@ const OrgForm = ({
mutation.mutate({
id: org.id,
...values,
organizationSettings: {
...org.organizationSettings,
orgAutoAcceptEmail: values.organizationSettings?.orgAutoAcceptEmail,
},
});
};
@@ -100,7 +96,7 @@ const OrgForm = ({
label="Domain for which invitations are auto-accepted"
placeholder="abc.com"
required
{...form.register("organizationSettings.orgAutoAcceptEmail")}
{...form.register("metadata.orgAutoAcceptEmail")}
/>
<Button type="submit" color="primary" loading={mutation.isLoading}>
{t("save")}
@@ -107,7 +107,7 @@ function AdminOrgTable() {
</Cell>
<Cell>
<div className="space-x-2">
{!org.organizationSettings?.isOrganizationVerified ? (
{!org.metadata?.isOrganizationVerified ? (
<Badge variant="red">{t("unverified")}</Badge>
) : (
<Badge variant="blue">{t("verified")}</Badge>
@@ -116,7 +116,7 @@ function AdminOrgTable() {
</Cell>
<Cell>
<div className="space-x-2">
{org.organizationSettings?.isOrganizationConfigured ? (
{org.metadata?.isOrganizationConfigured ? (
<Badge variant="blue">{t("dns_configured")}</Badge>
) : (
<Badge variant="red">{t("dns_missing")}</Badge>
@@ -136,7 +136,7 @@ function AdminOrgTable() {
<div className="flex w-full justify-end">
<DropdownActions
actions={[
...(!org.organizationSettings?.isOrganizationVerified
...(!org.metadata?.isOrganizationVerified
? [
{
id: "verify",
@@ -150,7 +150,7 @@ function AdminOrgTable() {
},
]
: []),
...(!org.organizationSettings?.isOrganizationConfigured
...(!org.metadata?.isOrganizationConfigured
? [
{
id: "dns",
@@ -158,7 +158,7 @@ function AdminOrgTable() {
onClick: () => {
updateMutation.mutate({
id: org.id,
organizationSettings: {
metadata: {
isOrganizationConfigured: true,
},
});
+1 -1
View File
@@ -87,7 +87,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!session) return { message: "Team upgraded successfully" };
const redirectUrl = team?.isOrganization
const redirectUrl = metadata?.data?.isOrganization
? `${WEBAPP_URL}/settings/organizations/profile?upgraded=true`
: `${WEBAPP_URL}/settings/teams/${team.id}/profile?upgraded=true`;
@@ -5,6 +5,7 @@ import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
@@ -46,7 +47,9 @@ const useCurrentUserId = () => {
};
const checkIsOrg = (team: Props["team"]) => {
return team.isOrganization;
const metadata = teamMetadataSchema.safeParse(team.metadata);
if (metadata.success && metadata.data?.isOrganization) return true;
return false;
};
export default function MemberListItem(props: Props) {
@@ -6,6 +6,7 @@ import { classNames } from "@calcom/lib";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import type { RouterOutputs } from "@calcom/trpc/react";
import { AnimatedPopover, Avatar, Divider, Tooltip, VerticalDivider } from "@calcom/ui";
@@ -87,7 +88,7 @@ export const TeamsFilter = ({
/>
<Divider />
{teams
?.filter((team) => !team?.isOrganization)
?.filter((team) => !teamMetadataSchema.parse(team.metadata)?.isOrganization)
.map((team) => (
<FilterCheckboxField
key={team.id}
+3 -2
View File
@@ -1,5 +1,6 @@
import type { Membership, Team } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
export const enum ENTITY_PERMISSION_LEVEL {
NONE,
@@ -19,8 +20,8 @@ export function canEditEntity(
);
}
export function isOrganization({ team }: { team: Pick<Team, "isOrganization"> }) {
return team.isOrganization;
export function isOrganization({ team }: { team: { metadata: Team["metadata"] } }) {
return teamMetadataSchema.parse(team.metadata)?.isOrganization;
}
export function getEntityPermissionLevel(
@@ -83,7 +83,6 @@ export async function getTeamWithMembers(args: {
id: true,
name: true,
slug: true,
isOrganization: true,
...(!!includeTeamLogo ? { logo: true } : {}),
bio: true,
hideBranding: true,
+23 -13
View File
@@ -30,7 +30,9 @@ describe("getOrg", () => {
id: 101,
name: "Test Team",
slug: "test-slug",
isOrganization: true,
metadata: {
isOrganization: true,
},
},
]);
@@ -50,16 +52,18 @@ describe("getOrg", () => {
expect(firstFindManyCallArguments[0]).toEqual({
where: {
slug: "test-slug",
isOrganization: true,
metadata: {
path: ["isOrganization"],
equals: true,
},
},
select: {
id: true,
slug: true,
metadata: true,
isOrganization: true,
},
});
expect(org?.isOrganization).toBe(true);
expect(org?.metadata?.isOrganization).toBe(true);
});
it("should not return an org result if metadata.isOrganization isn't true", async () => {
@@ -89,13 +93,15 @@ describe("getOrg", () => {
expect(firstFindManyCallArguments[0]).toEqual({
where: {
slug: "test-slug",
isOrganization: true,
metadata: {
path: ["isOrganization"],
equals: true,
},
},
select: {
id: true,
slug: true,
metadata: true,
isOrganization: true,
},
});
expect(org).toBe(null);
@@ -165,7 +171,6 @@ describe("getTeam", () => {
slug: true,
name: true,
metadata: true,
isOrganization: true,
},
});
expect(team).not.toBeNull();
@@ -180,7 +185,9 @@ describe("getTeam", () => {
id: 101,
name: "Test Team",
slug: "test-slug",
isOrganization: true,
metadata: {
isOrganization: true,
},
},
]);
@@ -207,7 +214,6 @@ describe("getTeam", () => {
slug: true,
name: true,
metadata: true,
isOrganization: true,
},
});
expect(team).toBe(null);
@@ -254,7 +260,10 @@ describe("getTeam", () => {
},
},
],
isOrganization: true,
metadata: {
path: ["isOrganization"],
equals: true,
},
},
},
select: {
@@ -262,7 +271,6 @@ describe("getTeam", () => {
name: true,
slug: true,
metadata: true,
isOrganization: true,
},
});
});
@@ -286,7 +294,10 @@ describe("getTeam", () => {
where: {
slug: "test-team",
parent: {
isOrganization: true,
metadata: {
path: ["isOrganization"],
equals: true,
},
OR: [
{
slug: "test-org",
@@ -305,7 +316,6 @@ describe("getTeam", () => {
slug: true,
name: true,
metadata: true,
isOrganization: true,
},
});
});
+12 -22
View File
@@ -7,9 +7,8 @@ import prisma from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
type TeamGetPayloadWithParsedMetadata<TeamSelect extends Prisma.TeamSelect> =
| (Omit<Prisma.TeamGetPayload<{ select: TeamSelect }>, "metadata" | "isOrganization"> & {
| (Omit<Prisma.TeamGetPayload<{ select: TeamSelect }>, "metadata"> & {
metadata: z.infer<typeof teamMetadataSchema>;
isOrganization: boolean;
})
| null;
@@ -50,8 +49,7 @@ async function getTeamOrOrg<TeamSelect extends Prisma.TeamSelect>({
teamSelect = {
...teamSelect,
metadata: true,
isOrganization: true,
} satisfies TeamSelect;
};
if (lookupBy.havingMemberWithId) where.members = { some: { userId: lookupBy.havingMemberWithId } };
if ("id" in lookupBy) {
@@ -65,7 +63,10 @@ async function getTeamOrOrg<TeamSelect extends Prisma.TeamSelect>({
// Note that an organization and a team that doesn't belong to an organization, both have parentId null
// If the organization has null slug(but requestedSlug is 'test') and the team also has slug 'test', we can't distinguish them without explicitly checking the metadata.isOrganization
// Note that, this isn't possible now to have same requestedSlug as the slug of a team not part of an organization. This is legacy teams handling mostly. But it is still safer to be sure that you are fetching an Organization only in case of isOrgView
where.isOrganization = true;
where.metadata = {
path: ["isOrganization"],
equals: true,
};
// We must fetch only the team here.
} else {
if (forOrgWithSlug) {
@@ -82,32 +83,21 @@ async function getTeamOrOrg<TeamSelect extends Prisma.TeamSelect>({
// teamSelect extends Prisma.TeamSelect but still teams doesn't contain a valid team as per TypeScript and thus it doesn't consider it having team.metadata, team.id and other fields
// This is the reason below code is using a lot of assertions.
const select = teamSelect;
const teams = await prisma.team.findMany({
where,
select: teamSelect,
});
const teamsWithParsedMetadata = teams
.map((team) => {
const teamX = team;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts types are way too complciated for this now
const parsedMetadata = teamMetadataSchema.parse(team.metadata ?? {});
return {
...team,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore It does exist
isOrganization: team.isOrganization as boolean,
metadata: parsedMetadata,
};
})
.map((team) => ({
...team,
// Using Type assertion here because we know that the metadata is present and Prisma and TypeScript aren't playing well together
metadata: teamMetadataSchema.parse((team as { metadata: z.infer<typeof teamMetadataSchema> }).metadata),
}))
// In cases where there are many teams with the same slug, we need to find out the one and only one that matches our criteria
.filter((team) => {
// We need an org if isOrgView otherwise we need a team
return isOrg ? team.isOrganization : !team.isOrganization;
return isOrg ? team.metadata?.isOrganization : !team.metadata?.isOrganization;
});
if (teamsWithParsedMetadata.length > 1) {
+7 -6
View File
@@ -1,5 +1,6 @@
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
import prisma from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
export const getUsernameForOrgMember = async ({
email,
@@ -93,12 +94,10 @@ export const validateAndGetCorrectedUsernameInTeam = async (
},
select: {
metadata: true,
isOrganization: true,
parentId: true,
organizationSettings: true,
parent: {
select: {
organizationSettings: true,
metadata: true,
},
},
},
@@ -108,15 +107,17 @@ export const validateAndGetCorrectedUsernameInTeam = async (
teamId,
team,
});
const organization = team?.isOrganization ? team : team?.parent;
const teamData = { ...team, metadata: teamMetadataSchema.parse(team?.metadata) };
const organization = teamData.metadata?.isOrganization ? teamData : teamData.parent;
if (organization) {
const orgMetadata = teamMetadataSchema.parse(organization.metadata);
// Organization context -> org-context username check
const orgId = team?.parentId || teamId;
const orgId = teamData.parentId || teamId;
return validateAndGetCorrectedUsernameAndEmail({
username,
email,
organizationId: orgId,
orgAutoAcceptEmail: organization?.organizationSettings?.orgAutoAcceptEmail || "",
orgAutoAcceptEmail: orgMetadata?.orgAutoAcceptEmail || "",
isSignup,
});
} else {
@@ -1,43 +0,0 @@
ALTER TABLE "Team" ADD COLUMN "isOrganization" BOOLEAN NOT NULL DEFAULT false;
-- CreateTable
CREATE TABLE "OrganizationSettings" (
"id" SERIAL NOT NULL,
"organizationId" INTEGER NOT NULL,
"isOrganizationConfigured" BOOLEAN NOT NULL DEFAULT false,
"isOrganizationVerified" BOOLEAN NOT NULL DEFAULT false,
"orgAutoAcceptEmail" TEXT NOT NULL,
CONSTRAINT "OrganizationSettings_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "OrganizationSettings_organizationId_key" ON "OrganizationSettings"("organizationId");
-- CreateIndex
CREATE UNIQUE INDEX "Team_slug_isOrganization_key" ON "Team"("slug", "isOrganization");
-- AddForeignKey
ALTER TABLE "OrganizationSettings" ADD CONSTRAINT "OrganizationSettings_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- -- Set team field to notify if it is an organization -> Easier than metadata.parse(X).isOrganization
UPDATE "Team"
SET "isOrganization" = (metadata ->> 'isOrganization')::BOOLEAN
WHERE
metadata ->> 'isOrganization' = 'true';
-- -- Insert data into org settings
INSERT INTO "OrganizationSettings" ("organizationId", "isOrganizationConfigured", "orgAutoAcceptEmail", "isOrganizationVerified")
SELECT
t.id,
(t.metadata ->> 'isOrganizationConfigured')::BOOLEAN AS "isOrganizationConfigured",
t.metadata ->> 'orgAutoAcceptEmail' AS "orgAutoAcceptEmail",
(t.metadata ->> 'isOrganizationVerified')::BOOLEAN AS "isOrganizationVerified"
FROM (
SELECT
id,
metadata
FROM
"Team"
WHERE
metadata ->> 'isOrganization' = 'true'
) AS t;
-12
View File
@@ -315,22 +315,10 @@ model Team {
apiKeys ApiKey[]
credentials Credential[]
accessCodes AccessCode[]
isOrganization Boolean @default(false)
organizationSettings OrganizationSettings?
instantMeetingTokens InstantMeetingToken[]
pendingPayment Boolean @default(false)
@@unique([slug, parentId])
@@unique([slug, isOrganization])
}
model OrganizationSettings {
id Int @id @default(autoincrement())
organization Team @relation(fields: [organizationId], references: [id], onDelete: Cascade)
organizationId Int @unique
isOrganizationConfigured Boolean @default(false)
isOrganizationVerified Boolean @default(false)
orgAutoAcceptEmail String
}
enum MembershipRole {
+4 -7
View File
@@ -332,13 +332,6 @@ export const userMetadata = z
})
.nullable();
export const orgSettingsSchema = z
.object({
isOrganizationVerified: z.boolean().optional(),
isOrganizationConfigured: z.boolean().optional(),
orgAutoAcceptEmail: z.string().optional(),
})
.nullable();
export type userMetadataType = z.infer<typeof userMetadata>;
export const teamMetadataSchema = z
@@ -347,6 +340,10 @@ export const teamMetadataSchema = z
paymentId: z.string(),
subscriptionId: z.string().nullable(),
subscriptionItemId: z.string().nullable(),
isOrganization: z.boolean().nullable(),
isOrganizationVerified: z.boolean().nullable(),
isOrganizationConfigured: z.boolean().nullable(),
orgAutoAcceptEmail: z.string().nullable(),
migratedToOrgFrom: z
.object({
teamSlug: z.string().or(z.null()).optional(),
@@ -1,4 +1,4 @@
import { withRoleCanCreateEntity } from "@calcom/lib/entityPermissionUtils";
import { isOrganization, withRoleCanCreateEntity } from "@calcom/lib/entityPermissionUtils";
import { getTeamAvatarUrl, getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import type { PrismaClient } from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
@@ -34,7 +34,6 @@ export const teamsAndUserProfilesQuery = async ({ ctx }: TeamsAndUserProfileOpti
team: {
select: {
id: true,
isOrganization: true,
name: true,
slug: true,
metadata: true,
@@ -56,7 +55,7 @@ export const teamsAndUserProfilesQuery = async ({ ctx }: TeamsAndUserProfileOpti
}
const nonOrgTeams = user.teams
.filter((membership) => !membership.team.isOrganization)
.filter((membership) => !isOrganization({ team: membership.team }))
.map((membership) => ({
...membership,
team: {
@@ -11,7 +11,8 @@ import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import type { PrismaClient } from "@calcom/prisma";
import { baseEventTypeSelect } from "@calcom/prisma";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import { EventTypeMetaDataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@trpc/server";
@@ -112,7 +113,6 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
team: {
select: {
id: true,
isOrganization: true,
name: true,
slug: true,
parentId: true,
@@ -240,7 +240,8 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
eventTypeGroups,
memberships
.filter((mmship) => {
if (mmship.team.isOrganization) {
const metadata = mmship.team.metadata;
if (metadata?.isOrganization) {
return false;
} else {
if (!input?.filters || !hasFilter(input?.filters)) {
@@ -23,7 +23,6 @@ export const adminGetHandler = async ({ input }: AdminGetOptions) => {
name: true,
slug: true,
metadata: true,
isOrganization: true,
members: {
where: {
role: "OWNER",
@@ -38,13 +37,6 @@ export const adminGetHandler = async ({ input }: AdminGetOptions) => {
},
},
},
organizationSettings: {
select: {
isOrganizationConfigured: true,
isOrganizationVerified: true,
orgAutoAcceptEmail: true,
},
},
},
});
@@ -55,7 +47,7 @@ export const adminGetHandler = async ({ input }: AdminGetOptions) => {
});
}
const parsedMetadata = teamMetadataSchema.parse(org.metadata);
if (!org?.isOrganization) {
if (!parsedMetadata?.isOrganization) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Organization not found",
@@ -12,14 +12,20 @@ type AdminGetAllOptions = {
export const adminGetUnverifiedHandler = async ({}: AdminGetAllOptions) => {
const allOrgs = await prisma.team.findMany({
where: {
isOrganization: true,
AND: [
{
metadata: {
path: ["isOrganization"],
equals: true,
},
},
],
},
select: {
id: true,
name: true,
slug: true,
metadata: true,
organizationSettings: true,
members: {
where: {
role: "OWNER",
@@ -17,14 +17,11 @@ type AdminUpdateOptions = {
};
export const adminUpdateHandler = async ({ input }: AdminUpdateOptions) => {
const { id, organizationSettings, ...restInput } = input;
const { id, ...restInput } = input;
const existingOrg = await prisma.team.findUnique({
where: {
id: id,
},
include: {
organizationSettings: true,
},
});
if (!existingOrg) {
@@ -35,8 +32,10 @@ export const adminUpdateHandler = async ({ input }: AdminUpdateOptions) => {
}
const { mergeMetadata } = getMetadataHelpers(teamMetadataSchema.unwrap(), existingOrg.metadata);
const data: Prisma.TeamUpdateArgs["data"] = restInput;
const data: Prisma.TeamUpdateArgs["data"] = {
...restInput,
metadata: mergeMetadata({ ...restInput.metadata }),
};
if (restInput.slug) {
await throwIfSlugConflicts({ id, slug: restInput.slug });
@@ -50,33 +49,13 @@ export const adminUpdateHandler = async ({ input }: AdminUpdateOptions) => {
data.metadata = mergeMetadata({
// If we save slug, we don't need the requestedSlug anymore
requestedSlug: undefined,
...input.metadata,
});
}
const updatedOrganisation = await prisma.$transaction(async (tx) => {
const updatedOrganisation = await tx.team.update({
where: { id },
data,
});
if (organizationSettings || existingOrg.organizationSettings) {
await tx.organizationSettings.update({
where: {
organizationId: updatedOrganisation.id,
},
data: {
isOrganizationConfigured:
organizationSettings?.isOrganizationConfigured ||
existingOrg.organizationSettings?.isOrganizationConfigured,
isOrganizationVerified:
organizationSettings?.isOrganizationVerified ||
existingOrg.organizationSettings?.isOrganizationVerified,
orgAutoAcceptEmail:
organizationSettings?.orgAutoAcceptEmail || existingOrg.organizationSettings?.orgAutoAcceptEmail,
},
});
}
return updatedOrganisation;
const updatedOrganisation = await prisma.team.update({
where: { id },
data,
});
return updatedOrganisation;
@@ -1,12 +1,12 @@
import { z } from "zod";
import { orgSettingsSchema } from "@calcom/prisma/zod-utils";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
export const ZAdminUpdate = z.object({
id: z.number(),
name: z.string().optional(),
slug: z.string().nullish(),
organizationSettings: orgSettingsSchema.unwrap().optional(),
metadata: teamMetadataSchema.optional(),
});
export type TAdminUpdate = z.infer<typeof ZAdminUpdate>;
@@ -1,4 +1,5 @@
import { prisma } from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@trpc/server";
@@ -16,7 +17,10 @@ export const adminVerifyHandler = async ({ input }: AdminVerifyOptions) => {
const foundOrg = await prisma.team.findFirst({
where: {
id: input.orgId,
isOrganization: true,
metadata: {
path: ["isOrganization"],
equals: true,
},
},
include: {
members: {
@@ -38,12 +42,17 @@ export const adminVerifyHandler = async ({ input }: AdminVerifyOptions) => {
const acceptedEmailDomain = foundOrg.members[0].user.email.split("@")[1];
await prisma.organizationSettings.update({
const existingMetadataParsed = teamMetadataSchema.parse(foundOrg.metadata);
await prisma.team.update({
where: {
organizationId: input.orgId,
id: input.orgId,
},
data: {
isOrganizationVerified: true,
metadata: {
...existingMetadataParsed,
isOrganizationVerified: true,
},
},
});
@@ -33,7 +33,7 @@ export async function checkIfOrgNeedsUpgradeHandler({ ctx }: GetUpgradeableOptio
/** We only need to return teams that don't have a `subscriptionId` on their metadata */
teams = teams.filter((m) => {
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
if (m.team.isOrganization) return false;
if (metadata.success && !metadata.data?.isOrganization) return false;
if (metadata.success && metadata.data?.subscriptionId) return false;
return true;
});
@@ -123,16 +123,12 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => {
create: {
name,
...(!IS_TEAM_BILLING_ENABLED ? { slug } : {}),
isOrganization: true,
metadata: {
...(IS_TEAM_BILLING_ENABLED ? { requestedSlug: slug } : {}),
},
organizationSettings: {
create: {
isOrganizationVerified: true,
isOrganizationConfigured,
orgAutoAcceptEmail: emailDomain,
},
isOrganization: true,
isOrganizationVerified: true,
isOrganizationConfigured,
orgAutoAcceptEmail: emailDomain,
},
},
},
@@ -30,7 +30,7 @@ export const ZUpdateInputSchema = z.object({
timeZone: z.string().optional(),
weekStart: z.string().optional(),
timeFormat: z.number().optional(),
metadata: teamMetadataSchema.unwrap().optional(),
metadata: teamMetadataSchema.unwrap().pick({ isOrganizationConfigured: true }).optional(),
});
export type TUpdateInputSchema = z.infer<typeof ZUpdateInputSchema>;
@@ -3,6 +3,7 @@ import { randomBytes } from "crypto";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
import { prisma } from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@calcom/trpc/server";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
@@ -20,7 +21,8 @@ export const createInviteHandler = async ({ ctx, input }: CreateInviteOptions) =
const membership = await isTeamAdmin(ctx.user.id, teamId);
if (!membership || !membership?.team) throw new TRPCError({ code: "UNAUTHORIZED" });
const isOrganizationOrATeamInOrganization = !!(membership.team?.parentId || membership.team.isOrganization);
const teamMetadata = teamMetadataSchema.parse(membership.team.metadata);
const isOrganizationOrATeamInOrganization = !!(membership.team?.parentId || teamMetadata?.isOrganization);
if (input.token) {
const existingToken = await prisma.verificationToken.findFirst({
@@ -4,6 +4,7 @@ import { deleteDomain } from "@calcom/lib/domainManager/organization";
import { isTeamOwner } from "@calcom/lib/server/queries/teams";
import { closeComDeleteTeam } from "@calcom/lib/sync/SyncServiceManager";
import { prisma } from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@trpc/server";
@@ -35,7 +36,9 @@ export const deleteHandler = async ({ ctx, input }: DeleteOptions) => {
},
});
if (deletedTeam?.isOrganization && deletedTeam.slug) deleteDomain(deletedTeam.slug);
const deletedTeamMetadata = teamMetadataSchema.parse(deletedTeam.metadata);
if (deletedTeamMetadata?.isOrganization && deletedTeam.slug) deleteDomain(deletedTeam.slug);
// Sync Services: Close.cm
closeComDeleteTeam(deletedTeam);
@@ -37,7 +37,7 @@ export const getUpgradeableHandler = async ({ ctx }: GetUpgradeableOptions) => {
teams = teams.filter((m) => {
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
if (metadata.success && metadata.data?.subscriptionId) return false;
if (m.team.isOrganization) return false; // We also dont return ORGs as it will be handled in OrgUpgradeBanner
if (metadata.success && metadata.data?.isOrganization) return false; // We also dont return ORGs as it will be handled in OrgUpgradeBanner
if (m.team.children.length > 0) return false; // We also dont return ORGs as it will be handled in OrgUpgradeBanner
return true;
});
@@ -37,23 +37,23 @@ export const inviteMemberHandler = async ({ ctx, input }: InviteMemberOptions) =
await checkRateLimitAndThrowError({
identifier: `invitedBy:${ctx.user.id}`,
});
const team = await getTeamOrThrow(input.teamId);
const isOrg = team.isOrganization;
if (isOrg && input.role === MembershipRole.OWNER && !(await isOrganisationOwner(ctx.user.id, input.teamId)))
throw new TRPCError({ code: "UNAUTHORIZED" });
await checkPermissions({
userId: ctx.user.id,
teamId:
ctx.user.organization.id && ctx.user.organization.isOrgAdmin ? ctx.user.organization.id : input.teamId,
isOrg,
isOrg: input.isOrg,
});
const { autoAcceptEmailDomain, orgVerified } = getIsOrgVerified(isOrg, team);
// 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);
const orgConnectInfoByUsernameOrEmail = usernameOrEmailsToInvite.reduce((acc, usernameOrEmail) => {
return {
@@ -63,13 +63,13 @@ export const inviteMemberHandler = async ({ ctx, input }: InviteMemberOptions) =
orgAutoAcceptDomain: autoAcceptEmailDomain,
usersEmail: usernameOrEmail,
team,
isOrg: isOrg,
isOrg: input.isOrg,
}),
};
}, {} as Record<string, ReturnType<typeof getOrgConnectionInfo>>);
const existingUsersWithMembersips = await getUsersToInvite({
usernamesOrEmails: usernameOrEmailsToInvite,
isInvitedToOrg: isOrg,
isInvitedToOrg: input.isOrg,
team,
});
const existingUsersEmailsAndUsernames = existingUsersWithMembersips.reduce(
@@ -153,7 +153,7 @@ export const inviteMemberHandler = async ({ ctx, input }: InviteMemberOptions) =
currentUserTeamName: team?.name,
existingUsersWithMembersips: regularUsers,
language: translation,
isOrg: isOrg,
isOrg: input.isOrg,
teamId: team.id,
currentUserParentTeamName: team?.parent?.name,
});
@@ -61,8 +61,6 @@ const mockedTeam: TeamWithParent = {
parent: null,
isPrivate: false,
logoUrl: "",
isOrganization: false,
calVideoLogo: "",
};
const mockUser: Invitee = {
@@ -210,10 +208,7 @@ describe("Invite Member Utils", () => {
describe("getIsOrgVerified", () => {
it("should return the correct values when isOrg is true and teamMetadata.orgAutoAcceptEmail is true", () => {
const team = {
organizationSettings: {
id: 1,
teamId: 1,
isOrganizationConfigured: false,
metadata: {
isOrganizationVerified: true,
orgAutoAcceptEmail: "example.com",
},
@@ -232,10 +227,7 @@ describe("Invite Member Utils", () => {
metadata: {},
parent: {
...mockedTeam,
organizationSettings: {
id: 1,
teamId: 1,
isOrganizationConfigured: false,
metadata: {
isOrganizationVerified: false,
orgAutoAcceptEmail: "example.com",
},
@@ -8,9 +8,10 @@ import { isTeamAdmin } from "@calcom/lib/server/queries";
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
import slugify from "@calcom/lib/slugify";
import { prisma } from "@calcom/prisma";
import type { Membership, OrganizationSettings, Team } from "@calcom/prisma/client";
import type { Membership, Team } from "@calcom/prisma/client";
import { Prisma, type User } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@trpc/server";
@@ -53,26 +54,18 @@ export function checkInputEmailIsValid(email: string) {
});
}
export async function getTeamOrThrow(teamId: number) {
export async function getTeamOrThrow(teamId: number, isOrg?: boolean) {
const team = await prisma.team.findFirst({
where: {
id: teamId,
},
include: {
organizationSettings: true,
parent: {
include: {
organizationSettings: true,
},
},
parent: true,
},
});
if (!team)
throw new TRPCError({
code: "NOT_FOUND",
message: `Team not found`,
});
throw new TRPCError({ code: "NOT_FOUND", message: `${isOrg ? "Organization" : "Team"} not found` });
return team;
}
@@ -392,29 +385,27 @@ export async function sendVerificationEmail({
}
}
type TeamAndOrganizationSettings = Team & {
organizationSettings?: OrganizationSettings | null;
};
export function getIsOrgVerified(
isOrg: boolean,
team: TeamAndOrganizationSettings & {
parent: TeamAndOrganizationSettings | null;
team: Team & {
parent: Team | null;
}
) {
const parentSettings = team.parent?.organizationSettings;
const teamMetadata = teamMetadataSchema.parse(team.metadata);
const orgMetadataSafeParse = teamMetadataSchema.safeParse(team.parent?.metadata);
const orgMetadataIfExists = orgMetadataSafeParse.success ? orgMetadataSafeParse.data : null;
if (isOrg && team.organizationSettings?.orgAutoAcceptEmail) {
if (isOrg && teamMetadata?.orgAutoAcceptEmail) {
return {
isInOrgScope: true,
orgVerified: team.organizationSettings.isOrganizationVerified,
autoAcceptEmailDomain: team.organizationSettings.orgAutoAcceptEmail,
orgVerified: teamMetadata.isOrganizationVerified,
autoAcceptEmailDomain: teamMetadata.orgAutoAcceptEmail,
};
} else if (parentSettings?.orgAutoAcceptEmail) {
} else if (orgMetadataIfExists?.orgAutoAcceptEmail) {
return {
isInOrgScope: true,
orgVerified: parentSettings.isOrganizationVerified,
autoAcceptEmailDomain: parentSettings.orgAutoAcceptEmail,
orgVerified: orgMetadataIfExists.isOrganizationVerified,
autoAcceptEmailDomain: orgMetadataIfExists.orgAutoAcceptEmail,
};
}
@@ -1,4 +1,5 @@
import { prisma } from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "../../../trpc";
@@ -19,15 +20,9 @@ export const listHandler = async ({ ctx }: ListOptions) => {
},
include: {
team: {
select: {
id: true,
name: true,
slug: true,
logo: true,
isOrganization: true,
include: {
inviteTokens: true,
parent: true,
parentId: true,
},
},
},
@@ -36,7 +31,8 @@ export const listHandler = async ({ ctx }: ListOptions) => {
return memberships
.filter((mmship) => {
return !mmship.team.isOrganization;
const metadata = teamMetadataSchema.parse(mmship.team.metadata);
return !metadata?.isOrganization;
})
.map(({ team: { inviteTokens, ..._team }, ...membership }) => ({
role: membership.role,
@@ -1,5 +1,6 @@
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "../../../trpc";
@@ -32,7 +33,8 @@ export const listOwnedTeamsHandler = async ({ ctx }: ListOptions) => {
return user?.teams
?.filter((m) => {
return !m.team.isOrganization;
const metadata = teamMetadataSchema.parse(m.team.metadata);
return !metadata?.isOrganization;
})
?.map(({ team }) => team);
};
@@ -4,6 +4,7 @@ import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
import { isTeamAdmin, isTeamOwner } from "@calcom/lib/server/queries/teams";
import { closeComDeleteTeamMembership } from "@calcom/lib/sync/SyncServiceManager";
import type { PrismaClient } from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
@@ -74,17 +75,20 @@ export const removeMemberHandler = async ({ ctx, input }: RemoveMemberOptions) =
const orgInfo = await ctx.prisma.team.findUnique({
where: { id: input.teamId },
select: {
isOrganization: true,
organizationSettings: true,
metadata: true,
},
});
const orgMetadata = orgInfo?.organizationSettings;
if (!foundUser || !orgInfo) throw new TRPCError({ code: "NOT_FOUND" });
if (orgInfo?.isOrganization && orgMetadata?.isOrganizationVerified && orgMetadata?.orgAutoAcceptEmail) {
if (foundUser.email.endsWith(orgMetadata.orgAutoAcceptEmail)) {
const parsedMetadata = teamMetadataSchema.parse(orgInfo.metadata);
if (
parsedMetadata?.isOrganization &&
parsedMetadata.isOrganizationVerified &&
parsedMetadata.orgAutoAcceptEmail
) {
if (foundUser.email.endsWith(parsedMetadata.orgAutoAcceptEmail)) {
await ctx.prisma.user.delete({
where: { id: input.memberId },
});
@@ -15,7 +15,7 @@ type InviteMemberOptions = {
};
export const resendInvitationHandler = async ({ ctx, input }: InviteMemberOptions) => {
const team = await getTeamOrThrow(input.teamId);
const team = await getTeamOrThrow(input.teamId, input.isOrg);
await checkPermissions({
userId: ctx.user.id,
@@ -2,6 +2,7 @@ import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server";
import { prisma } from "@calcom/prisma";
import type { Webhook } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
@@ -67,7 +68,6 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
team: {
select: {
id: true,
isOrganization: true,
name: true,
slug: true,
parentId: true,
@@ -115,7 +115,8 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
const teamWebhookGroups: WebhookGroup[] = user.teams
.filter((mmship) => {
return !mmship.team.isOrganization;
const metadata = teamMetadataSchema.parse(mmship.team.metadata);
return !metadata?.isOrganization;
})
.map((membership) => {
const orgMembership = teamMemberships.find(