From f31c45a153d9d0670083da62e33a884e8533b179 Mon Sep 17 00:00:00 2001 From: Lauris Skraucis Date: Wed, 23 Apr 2025 13:29:26 +0200 Subject: [PATCH] fix: platform org members cannot access their public booking page (#20821) * fix: platform member event pages * chore: update slots docs * refactor: user repository call * refactor: remove newline --- .../controllers/slots.controller.ts | 2 +- apps/api/v2/swagger/documentation.json | 2 +- .../lib/[user]/[type]/getServerSideProps.ts | 8 +++--- .../server/lib/[user]/getServerSideProps.ts | 25 +++++++++++++++--- docs/api-reference/v2/openapi.json | 2 +- .../features/eventtypes/lib/getPublicEvent.ts | 8 +++--- packages/lib/server/repository/user.ts | 26 +++++++++++++++++++ 7 files changed, 60 insertions(+), 13 deletions(-) diff --git a/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/slots.controller.ts b/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/slots.controller.ts index f4b6108cdb..72945173ee 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/slots.controller.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/slots.controller.ts @@ -72,7 +72,7 @@ export class SlotsController_2024_09_04 { And 3 ways to get available slots for team event type: - 1. By team event type id. Example '/v2/slots?teamEventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome' + 1. By team event type id. Example '/v2/slots?eventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome' 2. By team event type slug + team slug. Example '/v2/slots?eventTypeSlug=intro&teamSlug=team-slug&start=2050-09-05&end=2050-09-06' diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 2d59fdd640..869fd889f1 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -9942,7 +9942,7 @@ "get": { "operationId": "SlotsController_2024_09_04_getAvailableSlots", "summary": "Find out when is an event type ready to be booked.", - "description": "\n There are 4 ways to get available slots for event type of an individual user:\n \n 1. By event type id. Event type id can be of user and team event types. Example '/v2/slots?eventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome'\n\n 2. By event type slug + username. Example '/v2/slots?eventTypeSlug=intro&username=bob&start=2050-09-05&end=2050-09-06'\n\n 3. By event type slug + username + organization slug when searching within an organization. Example '/v2/slots?organizationSlug=org-slug&eventTypeSlug=intro&username=bob&start=2050-09-05&end=2050-09-06'\n\n 4. By usernames only (used for dynamic event type - there is no specific event but you want to know when 2 or more people are available). Example '/v2/slots?usernames=alice,bob&username=bob&organizationSlug=org-slug&start=2050-09-05&end=2050-09-06'. As you see you also need to provide the slug of the organization to which each user in the 'usernames' array belongs.\n\n And 3 ways to get available slots for team event type:\n\n 1. By team event type id. Example '/v2/slots?teamEventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome'\n\n 2. By team event type slug + team slug. Example '/v2/slots?eventTypeSlug=intro&teamSlug=team-slug&start=2050-09-05&end=2050-09-06'\n\n 3. By team event type slug + team slug + organization slug when searching within an organization. Example '/v2/slots?organizationSlug=org-slug&eventTypeSlug=intro&teamSlug=team-slug&start=2050-09-05&end=2050-09-06'\n\n All of them require \"start\" and \"end\" query parameters which define the time range for which available slots should be checked.\n Optional parameters are:\n - timeZone: Time zone in which the available slots should be returned. Defaults to UTC.\n - duration: Only use for event types that allow multiple durations or for dynamic event types. If not passed for multiple duration event types defaults to default duration. For dynamic event types defaults to 30 aka each returned slot is 30 minutes long. So duration=60 means that returned slots will be each 60 minutes long.\n - slotFormat: Format of the slots. By default return is an object where each key is date and value is array of slots as string. If you want to get start and end of each slot use \"range\" as value.\n ", + "description": "\n There are 4 ways to get available slots for event type of an individual user:\n \n 1. By event type id. Event type id can be of user and team event types. Example '/v2/slots?eventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome'\n\n 2. By event type slug + username. Example '/v2/slots?eventTypeSlug=intro&username=bob&start=2050-09-05&end=2050-09-06'\n\n 3. By event type slug + username + organization slug when searching within an organization. Example '/v2/slots?organizationSlug=org-slug&eventTypeSlug=intro&username=bob&start=2050-09-05&end=2050-09-06'\n\n 4. By usernames only (used for dynamic event type - there is no specific event but you want to know when 2 or more people are available). Example '/v2/slots?usernames=alice,bob&username=bob&organizationSlug=org-slug&start=2050-09-05&end=2050-09-06'. As you see you also need to provide the slug of the organization to which each user in the 'usernames' array belongs.\n\n And 3 ways to get available slots for team event type:\n\n 1. By team event type id. Example '/v2/slots?eventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome'\n\n 2. By team event type slug + team slug. Example '/v2/slots?eventTypeSlug=intro&teamSlug=team-slug&start=2050-09-05&end=2050-09-06'\n\n 3. By team event type slug + team slug + organization slug when searching within an organization. Example '/v2/slots?organizationSlug=org-slug&eventTypeSlug=intro&teamSlug=team-slug&start=2050-09-05&end=2050-09-06'\n\n All of them require \"start\" and \"end\" query parameters which define the time range for which available slots should be checked.\n Optional parameters are:\n - timeZone: Time zone in which the available slots should be returned. Defaults to UTC.\n - duration: Only use for event types that allow multiple durations or for dynamic event types. If not passed for multiple duration event types defaults to default duration. For dynamic event types defaults to 30 aka each returned slot is 30 minutes long. So duration=60 means that returned slots will be each 60 minutes long.\n - slotFormat: Format of the slots. By default return is an object where each key is date and value is array of slots as string. If you want to get start and end of each slot use \"range\" as value.\n ", "parameters": [ { "name": "cal-api-version", diff --git a/apps/web/server/lib/[user]/[type]/getServerSideProps.ts b/apps/web/server/lib/[user]/[type]/getServerSideProps.ts index dfd879e911..9cec8a58e6 100644 --- a/apps/web/server/lib/[user]/[type]/getServerSideProps.ts +++ b/apps/web/server/lib/[user]/[type]/getServerSideProps.ts @@ -16,6 +16,8 @@ import { BookingStatus, RedirectType } from "@calcom/prisma/client"; import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect"; +import { getUsersInOrgContext } from "@server/lib/[user]/getServerSideProps"; + type Props = { eventData: NonNullable>>; booking?: GetBookingType; @@ -228,10 +230,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) { } } - const [user] = await UserRepository.findUsersByUsername({ - usernameList: [username], - orgSlug: isValidOrgDomain ? currentOrgDomain : null, - }); + const [user] = await getUsersInOrgContext([username], isValidOrgDomain ? currentOrgDomain : null); if (!user) { return { @@ -240,6 +239,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) { } const org = isValidOrgDomain ? currentOrgDomain : null; + // We use this to both prefetch the query on the server, // as well as to check if the event exist, so we can show a 404 otherwise. const eventData = await EventRepository.getPublicEvent( diff --git a/apps/web/server/lib/[user]/getServerSideProps.ts b/apps/web/server/lib/[user]/getServerSideProps.ts index bf65feca62..9b39ecdf69 100644 --- a/apps/web/server/lib/[user]/getServerSideProps.ts +++ b/apps/web/server/lib/[user]/getServerSideProps.ts @@ -92,10 +92,10 @@ export const getServerSideProps: GetServerSideProps = async (cont } } - const usersInOrgContext = await UserRepository.findUsersByUsername({ + const usersInOrgContext = await getUsersInOrgContext( usernameList, - orgSlug: isValidOrgDomain ? currentOrgDomain : null, - }); + isValidOrgDomain ? currentOrgDomain : null + ); const isDynamicGroup = usersInOrgContext.length > 1; log.debug(safeStringify({ usersInOrgContext, isValidOrgDomain, currentOrgDomain, isDynamicGroup })); @@ -197,3 +197,22 @@ export const getServerSideProps: GetServerSideProps = async (cont }, }; }; + +export async function getUsersInOrgContext(usernameList: string[], orgSlug: string | null) { + const usersInOrgContext = await UserRepository.findUsersByUsername({ + usernameList, + orgSlug, + }); + + if (usersInOrgContext.length) { + return usersInOrgContext; + } + + // note(Lauris): platform members (people who run platform) are part of platform organization while + // the platform organization does not have a domain. In this case there is no org domain but also platform member + // "User.organization" is not null so "UserRepository.findUsersByUsername" returns empty array and we do this as a last resort + // call to find platform member. + return await UserRepository.findPlatformMembersByUsernames({ + usernameList, + }); +} diff --git a/docs/api-reference/v2/openapi.json b/docs/api-reference/v2/openapi.json index 87d4a4e626..14f1eda236 100644 --- a/docs/api-reference/v2/openapi.json +++ b/docs/api-reference/v2/openapi.json @@ -9472,7 +9472,7 @@ "get": { "operationId": "SlotsController_2024_09_04_getAvailableSlots", "summary": "Find out when is an event type ready to be booked.", - "description": "\n There are 4 ways to get available slots for event type of an individual user:\n \n 1. By event type id. Event type id can be of user and team event types. Example '/v2/slots?eventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome'\n\n 2. By event type slug + username. Example '/v2/slots?eventTypeSlug=intro&username=bob&start=2050-09-05&end=2050-09-06'\n\n 3. By event type slug + username + organization slug when searching within an organization. Example '/v2/slots?organizationSlug=org-slug&eventTypeSlug=intro&username=bob&start=2050-09-05&end=2050-09-06'\n\n 4. By usernames only (used for dynamic event type - there is no specific event but you want to know when 2 or more people are available). Example '/v2/slots?usernames=alice,bob&username=bob&organizationSlug=org-slug&start=2050-09-05&end=2050-09-06'. As you see you also need to provide the slug of the organization to which each user in the 'usernames' array belongs.\n\n And 3 ways to get available slots for team event type:\n\n 1. By team event type id. Example '/v2/slots?teamEventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome'\n\n 2. By team event type slug + team slug. Example '/v2/slots?eventTypeSlug=intro&teamSlug=team-slug&start=2050-09-05&end=2050-09-06'\n\n 3. By team event type slug + team slug + organization slug when searching within an organization. Example '/v2/slots?organizationSlug=org-slug&eventTypeSlug=intro&teamSlug=team-slug&start=2050-09-05&end=2050-09-06'\n\n All of them require \"start\" and \"end\" query parameters which define the time range for which available slots should be checked.\n Optional parameters are:\n - timeZone: Time zone in which the available slots should be returned. Defaults to UTC.\n - duration: Only use for event types that allow multiple durations or for dynamic event types. If not passed for multiple duration event types defaults to default duration. For dynamic event types defaults to 30 aka each returned slot is 30 minutes long. So duration=60 means that returned slots will be each 60 minutes long.\n - slotFormat: Format of the slots. By default return is an object where each key is date and value is array of slots as string. If you want to get start and end of each slot use \"range\" as value.\n ", + "description": "\n There are 4 ways to get available slots for event type of an individual user:\n \n 1. By event type id. Event type id can be of user and team event types. Example '/v2/slots?eventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome'\n\n 2. By event type slug + username. Example '/v2/slots?eventTypeSlug=intro&username=bob&start=2050-09-05&end=2050-09-06'\n\n 3. By event type slug + username + organization slug when searching within an organization. Example '/v2/slots?organizationSlug=org-slug&eventTypeSlug=intro&username=bob&start=2050-09-05&end=2050-09-06'\n\n 4. By usernames only (used for dynamic event type - there is no specific event but you want to know when 2 or more people are available). Example '/v2/slots?usernames=alice,bob&username=bob&organizationSlug=org-slug&start=2050-09-05&end=2050-09-06'. As you see you also need to provide the slug of the organization to which each user in the 'usernames' array belongs.\n\n And 3 ways to get available slots for team event type:\n\n 1. By team event type id. Example '/v2/slots?eventTypeId=10&start=2050-09-05&end=2050-09-06&timeZone=Europe/Rome'\n\n 2. By team event type slug + team slug. Example '/v2/slots?eventTypeSlug=intro&teamSlug=team-slug&start=2050-09-05&end=2050-09-06'\n\n 3. By team event type slug + team slug + organization slug when searching within an organization. Example '/v2/slots?organizationSlug=org-slug&eventTypeSlug=intro&teamSlug=team-slug&start=2050-09-05&end=2050-09-06'\n\n All of them require \"start\" and \"end\" query parameters which define the time range for which available slots should be checked.\n Optional parameters are:\n - timeZone: Time zone in which the available slots should be returned. Defaults to UTC.\n - duration: Only use for event types that allow multiple durations or for dynamic event types. If not passed for multiple duration event types defaults to default duration. For dynamic event types defaults to 30 aka each returned slot is 30 minutes long. So duration=60 means that returned slots will be each 60 minutes long.\n - slotFormat: Format of the slots. By default return is an object where each key is date and value is array of slots as string. If you want to get start and end of each slot use \"range\" as value.\n ", "parameters": [ { "name": "cal-api-version", diff --git a/packages/features/eventtypes/lib/getPublicEvent.ts b/packages/features/eventtypes/lib/getPublicEvent.ts index 68c7a1139e..c032097651 100644 --- a/packages/features/eventtypes/lib/getPublicEvent.ts +++ b/packages/features/eventtypes/lib/getPublicEvent.ts @@ -373,9 +373,11 @@ export const getPublicEvent = async ( some: { username, isPlatformManaged: false, - movedToProfile: { - organization: { - isPlatform: true, + profiles: { + some: { + organization: { + isPlatform: true, + }, }, }, }, diff --git a/packages/lib/server/repository/user.ts b/packages/lib/server/repository/user.ts index 54b581e802..4c7c7b9006 100644 --- a/packages/lib/server/repository/user.ts +++ b/packages/lib/server/repository/user.ts @@ -161,6 +161,32 @@ export class UserRepository { }); } + static async findPlatformMembersByUsernames({ usernameList }: { usernameList: string[] }) { + return ( + await prisma.user.findMany({ + select: userSelect, + where: { + username: { + in: usernameList, + }, + isPlatformManaged: false, + profiles: { + some: { + organization: { + isPlatform: true, + }, + }, + }, + }, + }) + ).map((user) => { + return { + ...user, + profile: ProfileRepository.buildPersonalProfileFromUser({ user }), + }; + }); + } + static async _getWhereClauseForFindingUsersByUsername({ orgSlug, usernameList,