Skip to content

Commit 8c2ce97

Browse files
fix: team booking page having same slug as the org (calcom#12213)
1 parent d3ab11e commit 8c2ce97

File tree

3 files changed

+558
-100
lines changed

3 files changed

+558
-100
lines changed

packages/lib/server/queries/teams/index.ts

Lines changed: 88 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { Prisma } from "@prisma/client";
22

33
import { getAppFromSlug } from "@calcom/app-store/utils";
4-
import { getOrgFullOrigin, getSlugOrRequestedSlug } from "@calcom/ee/organizations/lib/orgDomains";
4+
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
55
import prisma, { baseEventTypeSelect } from "@calcom/prisma";
66
import { SchedulingType } from "@calcom/prisma/enums";
7-
import { EventTypeMetaDataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils";
7+
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
88

99
import { WEBAPP_URL } from "../../../constants";
10-
import logger from "../../../logger";
10+
import { getTeam, getOrg } from "../../repository/team";
1111

1212
export type TeamWithMembers = Awaited<ReturnType<typeof getTeamWithMembers>>;
1313

@@ -24,6 +24,9 @@ export async function getTeamWithMembers(args: {
2424
isOrgView?: boolean;
2525
}) {
2626
const { id, slug, userId, orgSlug, isTeamView, isOrgView, includeTeamLogo } = args;
27+
28+
// This should improve performance saving already app data found.
29+
const appDataMap = new Map();
2730
const userSelect = Prisma.validator<Prisma.UserSelect>()({
2831
username: true,
2932
email: true,
@@ -61,115 +64,100 @@ export async function getTeamWithMembers(args: {
6164
},
6265
},
6366
});
64-
const teamSelect = Prisma.validator<Prisma.TeamSelect>()({
65-
id: true,
66-
name: true,
67-
slug: true,
68-
...(!!includeTeamLogo ? { logo: true } : {}),
69-
bio: true,
70-
hideBranding: true,
71-
hideBookATeamMember: true,
72-
isPrivate: true,
73-
metadata: true,
74-
parent: {
75-
select: {
76-
id: true,
77-
slug: true,
78-
name: true,
79-
},
80-
},
81-
children: {
82-
select: {
83-
name: true,
84-
slug: true,
67+
let lookupBy;
68+
69+
if (id) {
70+
lookupBy = { id, havingMemberWithId: userId };
71+
} else if (slug) {
72+
lookupBy = { slug, havingMemberWithId: userId };
73+
} else {
74+
throw new Error("Must provide either id or slug");
75+
}
76+
77+
const arg = {
78+
lookupBy,
79+
forOrgWithSlug: orgSlug ?? null,
80+
isOrg: !!isOrgView,
81+
teamSelect: {
82+
id: true,
83+
name: true,
84+
slug: true,
85+
...(!!includeTeamLogo ? { logo: true } : {}),
86+
bio: true,
87+
hideBranding: true,
88+
hideBookATeamMember: true,
89+
isPrivate: true,
90+
metadata: true,
91+
parent: {
92+
select: {
93+
id: true,
94+
slug: true,
95+
name: true,
96+
},
8597
},
86-
},
87-
members: {
88-
select: {
89-
accepted: true,
90-
role: true,
91-
disableImpersonation: true,
92-
user: {
93-
select: userSelect,
98+
children: {
99+
select: {
100+
name: true,
101+
slug: true,
94102
},
95103
},
96-
},
97-
theme: true,
98-
brandColor: true,
99-
darkBrandColor: true,
100-
eventTypes: {
101-
where: {
102-
hidden: false,
103-
schedulingType: {
104-
not: SchedulingType.MANAGED,
104+
members: {
105+
select: {
106+
accepted: true,
107+
role: true,
108+
disableImpersonation: true,
109+
user: {
110+
select: userSelect,
111+
},
105112
},
106113
},
107-
select: {
108-
users: {
109-
select: userSelect,
114+
theme: true,
115+
brandColor: true,
116+
darkBrandColor: true,
117+
eventTypes: {
118+
where: {
119+
hidden: false,
120+
schedulingType: {
121+
not: SchedulingType.MANAGED,
122+
},
123+
},
124+
select: {
125+
users: {
126+
select: userSelect,
127+
},
128+
metadata: true,
129+
...baseEventTypeSelect,
110130
},
111-
metadata: true,
112-
...baseEventTypeSelect,
113131
},
114-
},
115-
inviteTokens: {
116-
select: {
117-
token: true,
118-
expires: true,
119-
expiresInDays: true,
120-
identifier: true,
132+
inviteTokens: {
133+
select: {
134+
token: true,
135+
expires: true,
136+
expiresInDays: true,
137+
identifier: true,
138+
},
121139
},
122140
},
123-
});
141+
} as const;
124142

125-
const where: Prisma.TeamFindFirstArgs["where"] = {};
143+
const teamOrOrg = isOrgView ? await getOrg(arg) : await getTeam(arg);
126144

127-
if (userId) where.members = { some: { userId } };
128-
if (orgSlug && orgSlug !== slug) {
129-
where.parent = getSlugOrRequestedSlug(orgSlug);
130-
}
131-
if (id) where.id = id;
132-
if (slug) where.slug = slug;
133-
if (isOrgView) {
134-
// We must fetch only the organization here.
135-
// Note that an organization and a team that doesn't belong to an organization, both have parentId null
136-
// 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
137-
// 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
138-
where.metadata = {
139-
path: ["isOrganization"],
140-
equals: true,
141-
};
142-
}
145+
if (!teamOrOrg) return null;
143146

144-
const teams = await prisma.team.findMany({
145-
where,
146-
select: teamSelect,
147-
});
148-
149-
if (teams.length > 1) {
150-
logger.error("Found more than one team/Org. We should be doing something wrong.", {
151-
where,
152-
teams: teams.map((team) => ({ id: team.id, slug: team.slug })),
153-
});
154-
}
155-
156-
const team = teams[0];
157-
if (!team) return null;
158-
159-
// This should improve performance saving already app data found.
160-
const appDataMap = new Map();
161-
const members = team.members.map((obj) => {
162-
const { credentials, ...restUser } = obj.user;
147+
const members = teamOrOrg.members.map((m) => {
148+
const { credentials, ...restUser } = m.user;
163149
return {
164150
...restUser,
165-
role: obj.role,
166-
accepted: obj.accepted,
167-
disableImpersonation: obj.disableImpersonation,
151+
role: m.role,
152+
accepted: m.accepted,
153+
disableImpersonation: m.disableImpersonation,
168154
subteams: orgSlug
169-
? obj.user.teams.filter((obj) => obj.team.slug !== orgSlug).map((obj) => obj.team.slug)
155+
? m.user.teams
156+
.filter((membership) => membership.team.slug !== orgSlug)
157+
.map((membership) => membership.team.slug)
170158
: null,
171-
avatar: `${WEBAPP_URL}/${obj.user.username}/avatar.png`,
172-
orgOrigin: getOrgFullOrigin(obj.user.organization?.slug || ""),
159+
avatar: `${WEBAPP_URL}/${m.user.username}/avatar.png`,
160+
orgOrigin: getOrgFullOrigin(m.user.organization?.slug || ""),
173161
connectedApps: !isTeamView
174162
? credentials?.map((cred) => {
175163
const appSlug = cred.app?.slug;
@@ -193,15 +181,15 @@ export async function getTeamWithMembers(args: {
193181
};
194182
});
195183

196-
const eventTypes = team.eventTypes.map((eventType) => ({
184+
const eventTypes = teamOrOrg.eventTypes.map((eventType) => ({
197185
...eventType,
198186
metadata: EventTypeMetaDataSchema.parse(eventType.metadata),
199187
}));
200188
// Don't leak invite tokens to the frontend
201-
const { inviteTokens, ...teamWithoutInviteTokens } = team;
189+
const { inviteTokens, ...teamWithoutInviteTokens } = teamOrOrg;
202190

203191
// Don't leak stripe payment ids
204-
const teamMetadata = teamMetadataSchema.parse(team.metadata);
192+
const teamMetadata = teamOrOrg.metadata;
205193
const {
206194
paymentId: _,
207195
subscriptionId: __,
@@ -214,7 +202,7 @@ export async function getTeamWithMembers(args: {
214202
/** To prevent breaking we only return non-email attached token here, if we have one */
215203
inviteToken: inviteTokens.find(
216204
(token) =>
217-
token.identifier === `invite-link-for-teamId-${team.id}` &&
205+
token.identifier === `invite-link-for-teamId-${teamOrOrg.id}` &&
218206
token.expires > new Date(new Date().setHours(24))
219207
),
220208
metadata: restTeamMetadata,

0 commit comments

Comments
 (0)