From 829edec0ea3de5aaaa127b52a84681e7ec089c8d Mon Sep 17 00:00:00 2001 From: Lauris Skraucis Date: Fri, 7 Nov 2025 20:17:16 +0100 Subject: [PATCH] refactor: v2 api event-types/:eventTypeId access (#24969) * refactor: EventTypeAccess service * feat: event-types/:id system admin access and team event access * fix: implement cubic feedback * fix: e2e * fix: e2e * fix: oasdiff ignore non-breaking change --------- Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> --- .github/oasdiff-err-ignore.txt | 2 + .../2024-08-13/services/bookings.service.ts | 54 +- .../event-types.controller.e2e-spec.ts | 136 ++- .../controllers/event-types.controller.ts | 28 +- .../event-types.module.ts | 11 + .../event-types.repository.ts | 13 + .../outputs/get-event-type.output.ts | 17 +- .../services/event-types.service.ts | 34 +- .../services/event-type-access.service.ts | 78 ++ docs/api-reference/v2/openapi.json | 840 +++++++++--------- 10 files changed, 725 insertions(+), 488 deletions(-) create mode 100644 apps/api/v2/src/modules/event-types/services/event-type-access.service.ts diff --git a/.github/oasdiff-err-ignore.txt b/.github/oasdiff-err-ignore.txt index e69de29bb2..e3e210756f 100644 --- a/.github/oasdiff-err-ignore.txt +++ b/.github/oasdiff-err-ignore.txt @@ -0,0 +1,2 @@ +GET /v2/event-types/{eventTypeId} added to the 'data' response property 'oneOf' list for the response status '200' +GET /v2/event-types/{eventTypeId} added '#/components/schemas/EventTypeOutput_2024_06_14, #/components/schemas/TeamEventTypeOutput_2024_06_14' to the 'data' response property 'oneOf' list for the response status '200' diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index aa4c0c82fb..526a6733ff 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -1,5 +1,5 @@ -import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository"; import { CalendarLink } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output"; +import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository"; import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service"; import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service"; @@ -13,9 +13,8 @@ import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/ge import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; import { BillingService } from "@/modules/billing/services/billing.service"; import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository"; +import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service"; import { KyselyReadService } from "@/modules/kysely/kysely-read.service"; -import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; -import { MembershipsService } from "@/modules/memberships/services/memberships.service"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service"; import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; @@ -109,12 +108,11 @@ export class BookingsService_2024_08_13 { private readonly organizationsRepository: OrganizationsRepository, private readonly teamsRepository: TeamsRepository, private readonly teamsEventTypesRepository: TeamsEventTypesRepository, - private readonly membershipsRepository: MembershipsRepository, - private readonly membershipsService: MembershipsService, private readonly errorsBookingsService: ErrorsBookingsService_2024_08_13, private readonly regularBookingService: RegularBookingService, private readonly recurringBookingService: RecurringBookingService, - private readonly instantBookingCreateService: InstantBookingCreateService + private readonly instantBookingCreateService: InstantBookingCreateService, + private readonly eventTypeAccessService: EventTypeAccessService ) {} async createBooking(request: Request, body: CreateBookingInput, authUser: AuthOptionalUser) { @@ -128,7 +126,7 @@ export class BookingsService_2024_08_13 { this.errorsBookingsService.handleEventTypeToBeBookedNotFound(body); } const userIsEventTypeAdminOrOwner = authUser - ? await this.userIsEventTypeAdminOrOwner(authUser, eventType) + ? await this.eventTypeAccessService.userIsEventTypeAdminOrOwner(authUser, eventType) : false; await this.checkBookingRequiresAuthenticationSetting(eventType, authUser, userIsEventTypeAdminOrOwner); @@ -197,44 +195,6 @@ export class BookingsService_2024_08_13 { } } - async userIsEventTypeAdminOrOwner(authUser: ApiAuthGuardUser, eventType: EventType) { - const authUserId = authUser.id; - const authUserRole = authUser.role; - const eventTypeId = eventType.id; - const teamId = eventType.teamId; - const eventTypeOwnerId = eventType.userId || null; - - if (authUserRole === "ADMIN") return true; - - if (eventTypeOwnerId === authUserId) return true; - - if (eventTypeId) { - const [isUserHost, isUserAssigned] = await Promise.all([ - this.eventTypesRepository.isUserHostOfEventType(authUserId, eventTypeId), - this.eventTypesRepository.isUserAssignedToEventType(authUserId, eventTypeId), - ]); - - if (isUserHost || isUserAssigned) return true; - } - - if (teamId) { - const membership = await this.membershipsRepository.getUserAdminOrOwnerTeamMembership( - authUserId, - teamId - ); - if (membership) return true; - } - - if ( - eventTypeOwnerId && - (await this.membershipsService.isUserOrgAdminOrOwnerOfAnotherUser(authUserId, eventTypeOwnerId)) - ) { - return true; - } - - return false; - } - async getBookedEventType(body: CreateBookingInput) { if (body.eventTypeId) { return await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(body.eventTypeId); @@ -616,7 +576,7 @@ export class BookingsService_2024_08_13 { const booking = await this.bookingsRepository.getByUidWithAttendeesWithBookingSeatAndUserAndEvent(uid); const userIsEventTypeAdminOrOwner = authUser && booking?.eventType - ? await this.userIsEventTypeAdminOrOwner(authUser, booking.eventType) + ? await this.eventTypeAccessService.userIsEventTypeAdminOrOwner(authUser, booking.eventType) : false; if (booking) { @@ -805,7 +765,7 @@ export class BookingsService_2024_08_13 { const userIsEventTypeAdminOrOwner = authUser && databaseBooking.eventType - ? await this.userIsEventTypeAdminOrOwner(authUser, databaseBooking.eventType) + ? await this.eventTypeAccessService.userIsEventTypeAdminOrOwner(authUser, databaseBooking.eventType) : false; const isRecurring = !!databaseBooking.recurringEventId; const isSeated = !!databaseBooking.eventType?.seatsPerTimeSlot; diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts index 28ff995998..3762f15834 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts @@ -22,17 +22,31 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository import { randomString } from "test/utils/randomString"; import { withApiAuth } from "test/utils/withApiAuth"; - - import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants"; -import { BookingWindowPeriodInputTypeEnum_2024_06_14, BookerLayoutsInputEnum_2024_06_14, ConfirmationPolicyEnum, NoticeThresholdUnitEnum, FrequencyInput } from "@calcom/platform-enums"; +import { + BookingWindowPeriodInputTypeEnum_2024_06_14, + BookerLayoutsInputEnum_2024_06_14, + ConfirmationPolicyEnum, + NoticeThresholdUnitEnum, + FrequencyInput, +} from "@calcom/platform-enums"; import { SchedulingType } from "@calcom/platform-libraries"; -import { type ApiSuccessResponse, type CreateEventTypeInput_2024_06_14, type EventTypeOutput_2024_06_14, type GuestsDefaultFieldOutput_2024_06_14, type NameDefaultFieldInput_2024_06_14, type NotesDefaultFieldInput_2024_06_14, type SplitNameDefaultFieldOutput_2024_06_14, type UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types"; +import { + BaseConfirmationPolicy_2024_06_14, + TeamEventTypeOutput_2024_06_14, + type ApiSuccessResponse, + type CreateEventTypeInput_2024_06_14, + type EventTypeOutput_2024_06_14, + type GuestsDefaultFieldOutput_2024_06_14, + type NameDefaultFieldInput_2024_06_14, + type NotesDefaultFieldInput_2024_06_14, + type SplitNameDefaultFieldOutput_2024_06_14, + type UpdateEventTypeInput_2024_06_14, +} from "@calcom/platform-types"; import { FAILED_RECURRING_EVENT_TYPE_WITH_BOOKER_LIMITS_ERROR_MESSAGE } from "@calcom/platform-types/event-types/event-types_2024_06_14/inputs/validators/CantHaveRecurrenceAndBookerActiveBookingsLimit"; import { REQUIRES_AT_LEAST_ONE_PROPERTY_ERROR } from "@calcom/platform-types/utils/RequiresOneOfPropertiesWhenNotDisabled"; import type { PlatformOAuthClient, Team, User, Schedule, EventType } from "@calcom/prisma/client"; - const orderBySlug = (a: { slug: string }, b: { slug: string }) => { if (a.slug < b.slug) return -1; if (a.slug > b.slug) return 1; @@ -83,6 +97,8 @@ describe("Event types Endpoints", () => { let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; let apiKeyString: string; let apiKeyOrgUser: string; + let systemAdminUser: User; + let systemAdminApiKeyString: string; const userEmail = `event-types-2024-06-14-user-${randomString()}@api.com`; const falseTestEmail = `event-types-2024-06-14-false-user-${randomString()}@api.com`; @@ -196,6 +212,17 @@ describe("Event types Endpoints", () => { const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null); apiKeyString = `cal_test_${keyString}`; + systemAdminUser = await userRepositoryFixture.create({ + email: `event-types-2024-06-14-system-admin-${randomString()}@api.com`, + username: `event-types-2024-06-14-system-admin-${randomString()}`, + role: "ADMIN", + }); + const { keyString: adminKeyString } = await apiKeysRepositoryFixture.createApiKey( + systemAdminUser.id, + null + ); + systemAdminApiKeyString = `cal_test_${adminKeyString}`; + orgUser = await userRepositoryFixture.create({ email: `event-types-2024-06-14-org-user-${randomString()}@example.com`, name: `event-types-2024-06-14-org-user-${randomString()}`, @@ -1375,6 +1402,99 @@ describe("Event types Endpoints", () => { expect(fetchedEventType.color).toEqual(eventType.color); }); + it("system admin can access another user's event type by id", async () => { + return request(app.getHttpServer()) + .get(`/api/v2/event-types/${orgUserEventType1.id}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + .set("Authorization", `Bearer ${systemAdminApiKeyString}`) + .expect(200) + .then((response) => { + const responseBody: ApiSuccessResponse = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data.id).toEqual(orgUserEventType1.id); + expect(responseBody.data.ownerId).toEqual(orgUser.id); + }); + }); + + it("user can access a team event type as team member (using user's API key)", async () => { + const team = await teamRepositoryFixture.create({ + name: `event-types-2024-06-14-team-${randomString()}`, + isOrganization: false, + }); + + await membershipsRepositoryFixture.create({ + role: "ADMIN", + user: { connect: { id: user.id } }, + team: { connect: { id: team.id } }, + accepted: true, + }); + + const teamEventType = await eventTypesRepositoryFixture.createTeamEventType({ + title: `event-types-2024-06-14-team-event-${randomString()}`, + slug: `event-types-2024-06-14-team-event-${randomString()}`, + length: 60, + locations: [], + schedulingType: "COLLECTIVE", + team: { connect: { id: team.id } }, + }); + + return request(app.getHttpServer()) + .get(`/api/v2/event-types/${teamEventType.id}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + .set("Authorization", `Bearer ${apiKeyString}`) + .expect(200) + .then(async (response) => { + const responseBody: ApiSuccessResponse = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data.id).toEqual(teamEventType.id); + expect(responseBody.data.teamId).toEqual(team.id); + await teamRepositoryFixture.delete(team.id); + }); + }); + + it("user can access a team event type as HOST even if membership role is MEMBER (using user's API key)", async () => { + const team = await teamRepositoryFixture.create({ + name: `event-types-2024-06-14-host-team-${randomString()}`, + isOrganization: false, + }); + + await membershipsRepositoryFixture.create({ + role: "MEMBER", + user: { connect: { id: user.id } }, + team: { connect: { id: team.id } }, + accepted: true, + }); + + const teamEventType = await eventTypesRepositoryFixture.createTeamEventType({ + title: `event-types-2024-06-14-host-event-${randomString()}`, + slug: `event-types-2024-06-14-host-event-${randomString()}`, + length: 60, + locations: [], + schedulingType: "COLLECTIVE", + team: { connect: { id: team.id } }, + hosts: { + create: [ + { + user: { connect: { id: user.id } }, + }, + ], + }, + }); + + return request(app.getHttpServer()) + .get(`/api/v2/event-types/${teamEventType.id}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + .set("Authorization", `Bearer ${apiKeyString}`) + .expect(200) + .then(async (response) => { + const responseBody: ApiSuccessResponse = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data.id).toEqual(teamEventType.id); + expect(responseBody.data.teamId).toEqual(team.id); + await teamRepositoryFixture.delete(team.id); + }); + }); + it(`/GET/event-types by username and eventSlug`, async () => { const response = await request(app.getHttpServer()) .get(`/api/v2/event-types?username=${username}&eventSlug=${eventType.slug}`) @@ -1780,7 +1900,7 @@ describe("Event types Endpoints", () => { .then(async (response) => { const responseBody: ApiSuccessResponse = response.body; const updatedEventType = responseBody.data; - const policy = updatedEventType.confirmationPolicy as any; + const policy = updatedEventType.confirmationPolicy as BaseConfirmationPolicy_2024_06_14; expect(policy?.type).toEqual(ConfirmationPolicyEnum.ALWAYS); expect(policy?.blockUnconfirmedBookingsInBooker).toEqual(false); expect(policy?.noticeThreshold).toBeUndefined(); @@ -1808,7 +1928,7 @@ describe("Event types Endpoints", () => { .then(async (response) => { const responseBody: ApiSuccessResponse = response.body; const updatedEventType = responseBody.data; - const policy = updatedEventType.confirmationPolicy as any; + const policy = updatedEventType.confirmationPolicy as BaseConfirmationPolicy_2024_06_14; expect(policy?.type).toEqual(ConfirmationPolicyEnum.TIME); expect(policy?.noticeThreshold).toEqual({ unit: NoticeThresholdUnitEnum.MINUTES, @@ -2793,4 +2913,4 @@ describe("Event types Endpoints", () => { await app.close(); }); }); -}); \ No newline at end of file +}); diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts index b4c8d88a2a..78fa6ee2e6 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts @@ -23,6 +23,8 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; +import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; +import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer"; import { UserWithProfile } from "@/modules/users/users.repository"; import { Controller, @@ -73,7 +75,8 @@ export class EventTypesController_2024_06_14 { private readonly eventTypesService: EventTypesService_2024_06_14, private readonly inputEventTypesService: InputEventTypesService_2024_06_14, private readonly eventTypeResponseTransformPipe: EventTypeResponseTransformPipe, - private readonly outputEventTypesService: OutputEventTypesService_2024_06_14 + private readonly outputEventTypesService: OutputEventTypesService_2024_06_14, + private readonly outputTeamEventTypesResponsePipe: OutputTeamEventTypesResponsePipe ) {} @Post("/") @@ -107,21 +110,36 @@ export class EventTypesController_2024_06_14 { @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) @ApiOperation({ summary: "Get an event type", - description: `Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.`, + description: `Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint. + + Access control: This endpoint fetches an event type by ID and returns it only if the authenticated user is authorized. Authorization is granted to: + - System admins + - The event type owner + - Hosts of the event type or users assigned to the event type + - Team admins/owners of the team that owns the team event type + - Organization admins/owners of the event type owner's organization + - Organization admins/owners of the team's parent organization + + Note: Update and delete endpoints remain restricted to the event type owner only.`, }) async getEventTypeById( @Param("eventTypeId") eventTypeId: string, - @GetUser() user: UserWithProfile + @GetUser() user: ApiAuthGuardUser ): Promise { - const eventType = await this.eventTypesService.getUserEventType(user.id, Number(eventTypeId)); + const eventType = await this.eventTypesService.getEventTypeByIdIfAuthorized(user, Number(eventTypeId)); if (!eventType) { throw new NotFoundException(`Event type with id ${eventTypeId} not found`); } + const responseEventType = + "hosts" in eventType + ? await this.outputTeamEventTypesResponsePipe.transform(eventType) + : this.eventTypeResponseTransformPipe.transform(eventType); + return { status: SUCCESS_STATUS, - data: this.eventTypeResponseTransformPipe.transform(eventType), + data: responseEventType, }; } diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.module.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.module.ts index 0f7bd4683b..1ea3596491 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.module.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.module.ts @@ -10,10 +10,15 @@ import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository"; import { AppsRepository } from "@/modules/apps/apps.repository"; import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; +import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service"; import { MembershipsModule } from "@/modules/memberships/memberships.module"; +import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer"; +import { OutputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/output.service"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { RedisModule } from "@/modules/redis/redis.module"; import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module"; +import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; +import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; import { TokensModule } from "@/modules/tokens/tokens.module"; import { UsersService } from "@/modules/users/services/users.service"; import { UsersRepository } from "@/modules/users/users.repository"; @@ -26,6 +31,8 @@ import { Module } from "@nestjs/common"; EventTypesService_2024_06_14, InputEventTypesService_2024_06_14, OutputEventTypesService_2024_06_14, + EventTypeAccessService, + TeamsRepository, UsersRepository, UsersService, SchedulesRepository_2024_06_11, @@ -35,6 +42,9 @@ import { Module } from "@nestjs/common"; CredentialsRepository, AppsRepository, CalendarsRepository, + OutputTeamEventTypesResponsePipe, + OutputOrganizationsEventTypesService, + TeamsEventTypesRepository, ], controllers: [EventTypesController_2024_06_14], exports: [ @@ -42,6 +52,7 @@ import { Module } from "@nestjs/common"; EventTypesRepository_2024_06_14, InputEventTypesService_2024_06_14, OutputEventTypesService_2024_06_14, + EventTypeAccessService, ], }) export class EventTypesModule_2024_06_14 {} diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts index b9c7eea8bd..4f1588db8c 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts @@ -93,6 +93,19 @@ export class EventTypesRepository_2024_06_14 { }); } + async getEventTypeByIdWithHosts(eventTypeId: number) { + return this.dbRead.prisma.eventType.findUnique({ + where: { id: eventTypeId }, + include: { + users: true, + schedule: true, + destinationCalendar: true, + calVideoSettings: true, + hosts: true, + }, + }); + } + async getEventTypeByIdIncludeUsersAndTeam(eventTypeId: number) { const eventType = await this.dbRead.prisma.eventType.findUnique({ where: { id: eventTypeId }, diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/get-event-type.output.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/get-event-type.output.ts index b42034fd9c..c8e40b4df9 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/get-event-type.output.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/outputs/get-event-type.output.ts @@ -1,19 +1,22 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger"; import { Type } from "class-transformer"; -import { IsIn, ValidateNested } from "class-validator"; +import { IsIn } from "class-validator"; import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; -import { EventTypeOutput_2024_06_14 } from "@calcom/platform-types"; +import { EventTypeOutput_2024_06_14, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types"; +@ApiExtraModels(EventTypeOutput_2024_06_14, TeamEventTypeOutput_2024_06_14) export class GetEventTypeOutput_2024_06_14 { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsIn([SUCCESS_STATUS, ERROR_STATUS]) status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; @ApiProperty({ - type: EventTypeOutput_2024_06_14, + oneOf: [ + { $ref: getSchemaPath(EventTypeOutput_2024_06_14) }, + { $ref: getSchemaPath(TeamEventTypeOutput_2024_06_14) }, + ], }) - @ValidateNested() - @Type(() => EventTypeOutput_2024_06_14) - data!: EventTypeOutput_2024_06_14 | null; + @Type(() => Object) + data!: EventTypeOutput_2024_06_14 | TeamEventTypeOutput_2024_06_14 | null; } diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts index a932e5c54a..e7e89d9436 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts @@ -1,10 +1,14 @@ import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_06_14/constants/constants"; import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { DatabaseEventType } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service"; import { InputEventTransformed_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/transformed"; import { SystemField, CustomField } from "@/ee/event-types/event-types_2024_06_14/transformers"; import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository"; import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator"; +import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; +import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; +import { DatabaseTeamEventType } from "@/modules/organizations/event-types/services/output.service"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository"; import { UsersService } from "@/modules/users/services/users.service"; @@ -30,7 +34,8 @@ export class EventTypesService_2024_06_14 { private readonly usersService: UsersService, private readonly selectedCalendarsRepository: SelectedCalendarsRepository, private readonly dbWrite: PrismaWriteService, - private readonly schedulesRepository: SchedulesRepository_2024_06_11 + private readonly schedulesRepository: SchedulesRepository_2024_06_11, + private readonly eventTypeAccessService: EventTypeAccessService ) {} async createUserEventType(user: UserWithProfile, body: InputEventTransformed_2024_06_14) { @@ -39,7 +44,7 @@ export class EventTypesService_2024_06_14 { } await this.checkCanCreateEventType(user.id, body); const eventTypeUser = await this.getUserToCreateEvent(user); - + const { destinationCalendar: _destinationCalendar, ...rest } = body; // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -79,6 +84,31 @@ export class EventTypesService_2024_06_14 { }; } + async getEventTypeByIdIfAuthorized( + authUser: ApiAuthGuardUser, + eventTypeId: number + ): Promise { + const eventType = await this.eventTypesRepository.getEventTypeByIdWithHosts(eventTypeId); + + if (!eventType) { + return null; + } + + const hasAccess = await this.eventTypeAccessService.userIsEventTypeAdminOrOwner( + authUser, + eventType as unknown as EventType + ); + + if (!hasAccess) { + return null; + } + + return { + ownerId: eventType.userId ?? 0, + ...eventType, + }; + } + async checkCanCreateEventType(userId: number, body: InputEventTransformed_2024_06_14) { const existsWithSlug = await this.eventTypesRepository.getUserEventTypeBySlug(userId, body.slug); if (existsWithSlug) { diff --git a/apps/api/v2/src/modules/event-types/services/event-type-access.service.ts b/apps/api/v2/src/modules/event-types/services/event-type-access.service.ts new file mode 100644 index 0000000000..00de2547f1 --- /dev/null +++ b/apps/api/v2/src/modules/event-types/services/event-type-access.service.ts @@ -0,0 +1,78 @@ +import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; +import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; +import { MembershipsService } from "@/modules/memberships/services/memberships.service"; +import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; +import { Injectable } from "@nestjs/common"; + +import type { EventType } from "@calcom/prisma/client"; + +@Injectable() +export class EventTypeAccessService { + constructor( + private readonly eventTypesRepository: EventTypesRepository_2024_06_14, + private readonly membershipsRepository: MembershipsRepository, + private readonly membershipsService: MembershipsService, + private readonly teamsRepository: TeamsRepository + ) {} + + async userIsEventTypeAdminOrOwner(authUser: ApiAuthGuardUser, eventType: EventType): Promise { + const authUserId = authUser.id; + const eventTypeId = eventType.id; + const teamId = eventType.teamId; + const eventTypeOwnerId = eventType.userId || null; + + if (authUser.isSystemAdmin) return true; + + if (eventTypeOwnerId === authUserId) return true; + + if (eventTypeId) { + const isHostOrAssigned = await this.isUserHostOrAssignedToEventType(authUserId, eventTypeId); + if (isHostOrAssigned) return true; + } + + if (teamId) { + const isTeamOrParentOrgAdmin = await this.isUserTeamAdminOrParentOrgAdmin(authUserId, teamId); + if (isTeamOrParentOrgAdmin) return true; + } + + if (eventTypeOwnerId) { + const isOrgAdminOrOwnerOfEventOwner = await this.isUserOrgAdminOrOwnerOfEventOwner( + authUserId, + eventTypeOwnerId + ); + if (isOrgAdminOrOwnerOfEventOwner) return true; + } + + return false; + } + + private async isUserHostOrAssignedToEventType(authUserId: number, eventTypeId: number): Promise { + const [isUserHost, isUserAssigned] = await Promise.all([ + this.eventTypesRepository.isUserHostOfEventType(authUserId, eventTypeId), + this.eventTypesRepository.isUserAssignedToEventType(authUserId, eventTypeId), + ]); + return isUserHost || isUserAssigned; + } + + private async isUserTeamAdminOrParentOrgAdmin(authUserId: number, teamId: number): Promise { + const membership = await this.membershipsRepository.getUserAdminOrOwnerTeamMembership(authUserId, teamId); + if (membership) return true; + + const team = await this.teamsRepository.getById(teamId); + const parentOrgId = team?.parentId ?? null; + if (parentOrgId) { + const isOrgAdmin = await this.membershipsRepository.isUserOrganizationAdmin(authUserId, parentOrgId); + if (isOrgAdmin) return true; + } + + return false; + } + + private async isUserOrgAdminOrOwnerOfEventOwner( + authUserId: number, + eventTypeOwnerId: number + ): Promise { + return this.membershipsService.isUserOrgAdminOrOwnerOfAnotherUser(authUserId, eventTypeOwnerId); + } +} diff --git a/docs/api-reference/v2/openapi.json b/docs/api-reference/v2/openapi.json index e65cb5a7d9..ecef1f6c9d 100644 --- a/docs/api-reference/v2/openapi.json +++ b/docs/api-reference/v2/openapi.json @@ -11579,7 +11579,7 @@ "get": { "operationId": "EventTypesController_2024_06_14_getEventTypeById", "summary": "Get an event type", - "description": "Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.", + "description": "Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.\n \n Access control: This endpoint fetches an event type by ID and returns it only if the authenticated user is authorized. Authorization is granted to:\n - System admins\n - The event type owner\n - Hosts of the event type or users assigned to the event type\n - Team admins/owners of the team that owns the team event type\n - Organization admins/owners of the event type owner's organization\n - Organization admins/owners of the team's parent organization\n\n Note: Update and delete endpoints remain restricted to the event type owner only.", "parameters": [ { "name": "cal-api-version", @@ -19166,6 +19166,380 @@ }, "required": ["status", "data"] }, + "EventTypeTeam": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "slug": { + "type": "string" + }, + "bannerUrl": { + "type": "string" + }, + "name": { + "type": "string" + }, + "logoUrl": { + "type": "string" + }, + "weekStart": { + "type": "string" + }, + "brandColor": { + "type": "string" + }, + "darkBrandColor": { + "type": "string" + }, + "theme": { + "type": "string" + } + }, + "required": [ + "id", + "slug", + "bannerUrl", + "name", + "logoUrl", + "weekStart", + "brandColor", + "darkBrandColor", + "theme" + ] + }, + "TeamEventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "lengthInMinutes": { + "type": "number", + "example": 60 + }, + "lengthInMinutesOptions": { + "example": [15, 30, 60], + "description": "If you want that user can choose between different lengths of the event you can specify them here. Must include the provided `lengthInMinutes`.", + "type": "array", + "items": { + "type": "number" + } + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "slug": { + "type": "string", + "example": "learn-the-secrets-of-masterchief" + }, + "description": { + "type": "string", + "example": "Discover the culinary wonders of Argentina by making the best flan ever!" + }, + "locations": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/OutputAddressLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/OutputLinkLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/OutputIntegrationLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/OutputPhoneLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/OutputOrganizersDefaultAppLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/OutputUnknownLocation_2024_06_14" + } + ] + } + }, + "bookingFields": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/NameDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/EmailDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/LocationDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TitleDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/NotesDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/GuestsDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/PhoneFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/AddressFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/NumberFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextAreaFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/SelectFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiSelectFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiEmailFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/CheckboxGroupFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/RadioGroupFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/BooleanFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/UrlFieldOutput_2024_06_14" + } + ] + } + }, + "disableGuests": { + "type": "boolean" + }, + "slotInterval": { + "type": "object", + "example": 60, + "nullable": true + }, + "minimumBookingNotice": { + "type": "number", + "example": 0 + }, + "beforeEventBuffer": { + "type": "number", + "example": 0 + }, + "afterEventBuffer": { + "type": "number", + "example": 0 + }, + "recurrence": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Recurrence_2024_06_14" + } + ] + }, + "metadata": { + "type": "object" + }, + "price": { + "type": "number" + }, + "currency": { + "type": "string" + }, + "lockTimeZoneToggleOnBookingPage": { + "type": "boolean" + }, + "seatsPerTimeSlot": { + "type": "object", + "nullable": true + }, + "forwardParamsSuccessRedirect": { + "type": "object", + "nullable": true + }, + "successRedirectUrl": { + "type": "object", + "nullable": true + }, + "isInstantEvent": { + "type": "boolean" + }, + "seatsShowAvailabilityCount": { + "type": "boolean", + "nullable": true + }, + "scheduleId": { + "type": "number", + "nullable": true + }, + "bookingLimitsCount": { + "type": "object" + }, + "bookerActiveBookingsLimit": { + "$ref": "#/components/schemas/BookerActiveBookingsLimitOutput_2024_06_14" + }, + "onlyShowFirstAvailableSlot": { + "type": "boolean" + }, + "bookingLimitsDuration": { + "type": "object" + }, + "bookingWindow": { + "type": "array", + "description": "Limit how far in the future this event can be booked", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BusinessDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/CalendarDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/RangeWindow_2024_06_14" + } + ] + } + }, + "bookerLayouts": { + "$ref": "#/components/schemas/BookerLayouts_2024_06_14" + }, + "confirmationPolicy": { + "type": "object" + }, + "requiresBookerEmailVerification": { + "type": "boolean" + }, + "hideCalendarNotes": { + "type": "boolean" + }, + "color": { + "$ref": "#/components/schemas/EventTypeColor_2024_06_14" + }, + "seats": { + "$ref": "#/components/schemas/Seats_2024_06_14" + }, + "offsetStart": { + "type": "number" + }, + "customName": { + "type": "string" + }, + "destinationCalendar": { + "$ref": "#/components/schemas/DestinationCalendar_2024_06_14" + }, + "useDestinationCalendarEmail": { + "type": "boolean" + }, + "hideCalendarEventDetails": { + "type": "boolean" + }, + "hideOrganizerEmail": { + "type": "boolean", + "description": "Boolean to Hide organizer's email address from the booking screen, email notifications, and calendar events" + }, + "calVideoSettings": { + "description": "Cal video settings for the event type", + "allOf": [ + { + "$ref": "#/components/schemas/CalVideoSettings" + } + ] + }, + "hidden": { + "type": "boolean" + }, + "bookingRequiresAuthentication": { + "type": "boolean", + "description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users who are the event-type owner or org/team admin/owner can book this event type." + }, + "teamId": { + "type": "number" + }, + "ownerId": { + "type": "object", + "nullable": true + }, + "parentEventTypeId": { + "type": "object", + "description": "For managed event types, parent event type is the event type that this event type is based on", + "nullable": true + }, + "hosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "assignAllTeamMembers": { + "type": "boolean" + }, + "schedulingType": { + "type": "string", + "enum": ["roundRobin", "collective", "managed"] + }, + "team": { + "$ref": "#/components/schemas/EventTypeTeam" + }, + "emailSettings": { + "description": "Email settings for this event type. Only available for organization team event types.", + "allOf": [ + { + "$ref": "#/components/schemas/EmailSettings_2024_06_14" + } + ] + }, + "rescheduleWithSameRoundRobinHost": { + "type": "boolean", + "description": "Rescheduled events will be assigned to the same host as initially scheduled." + } + }, + "required": [ + "id", + "lengthInMinutes", + "title", + "slug", + "description", + "locations", + "bookingFields", + "disableGuests", + "recurrence", + "metadata", + "price", + "currency", + "lockTimeZoneToggleOnBookingPage", + "forwardParamsSuccessRedirect", + "successRedirectUrl", + "isInstantEvent", + "scheduleId", + "hidden", + "bookingRequiresAuthentication", + "teamId", + "hosts", + "schedulingType", + "team" + ] + }, "GetEventTypeOutput_2024_06_14": { "type": "object", "properties": { @@ -19175,10 +19549,12 @@ "example": "success" }, "data": { - "nullable": true, - "allOf": [ + "oneOf": [ { "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" } ] } @@ -19596,6 +19972,48 @@ }, "required": ["status", "data"] }, + "StripConnectOutputDto": { + "type": "object", + "properties": { + "authUrl": { + "type": "string" + } + }, + "required": ["authUrl"] + }, + "StripConnectOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": ["success", "error"] + }, + "data": { + "$ref": "#/components/schemas/StripConnectOutputDto" + } + }, + "required": ["status", "data"] + }, + "StripCredentialsSaveOutputResponseDto": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + }, + "required": ["url"] + }, + "StripCredentialsCheckOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "object", + "example": "success" + } + }, + "required": ["status"] + }, "OrgTeamOutputDto": { "type": "object", "properties": { @@ -21111,380 +21529,6 @@ }, "required": ["lengthInMinutes", "title", "slug", "schedulingType"] }, - "EventTypeTeam": { - "type": "object", - "properties": { - "id": { - "type": "number" - }, - "slug": { - "type": "string" - }, - "bannerUrl": { - "type": "string" - }, - "name": { - "type": "string" - }, - "logoUrl": { - "type": "string" - }, - "weekStart": { - "type": "string" - }, - "brandColor": { - "type": "string" - }, - "darkBrandColor": { - "type": "string" - }, - "theme": { - "type": "string" - } - }, - "required": [ - "id", - "slug", - "bannerUrl", - "name", - "logoUrl", - "weekStart", - "brandColor", - "darkBrandColor", - "theme" - ] - }, - "TeamEventTypeOutput_2024_06_14": { - "type": "object", - "properties": { - "id": { - "type": "number", - "example": 1 - }, - "lengthInMinutes": { - "type": "number", - "example": 60 - }, - "lengthInMinutesOptions": { - "example": [15, 30, 60], - "description": "If you want that user can choose between different lengths of the event you can specify them here. Must include the provided `lengthInMinutes`.", - "type": "array", - "items": { - "type": "number" - } - }, - "title": { - "type": "string", - "example": "Learn the secrets of masterchief!" - }, - "slug": { - "type": "string", - "example": "learn-the-secrets-of-masterchief" - }, - "description": { - "type": "string", - "example": "Discover the culinary wonders of Argentina by making the best flan ever!" - }, - "locations": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/OutputAddressLocation_2024_06_14" - }, - { - "$ref": "#/components/schemas/OutputLinkLocation_2024_06_14" - }, - { - "$ref": "#/components/schemas/OutputIntegrationLocation_2024_06_14" - }, - { - "$ref": "#/components/schemas/OutputPhoneLocation_2024_06_14" - }, - { - "$ref": "#/components/schemas/OutputOrganizersDefaultAppLocation_2024_06_14" - }, - { - "$ref": "#/components/schemas/OutputUnknownLocation_2024_06_14" - } - ] - } - }, - "bookingFields": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/NameDefaultFieldOutput_2024_06_14" - }, - { - "$ref": "#/components/schemas/EmailDefaultFieldOutput_2024_06_14" - }, - { - "$ref": "#/components/schemas/LocationDefaultFieldOutput_2024_06_14" - }, - { - "$ref": "#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14" - }, - { - "$ref": "#/components/schemas/TitleDefaultFieldOutput_2024_06_14" - }, - { - "$ref": "#/components/schemas/NotesDefaultFieldOutput_2024_06_14" - }, - { - "$ref": "#/components/schemas/GuestsDefaultFieldOutput_2024_06_14" - }, - { - "$ref": "#/components/schemas/PhoneFieldOutput_2024_06_14" - }, - { - "$ref": "#/components/schemas/AddressFieldOutput_2024_06_14" - }, - { - "$ref": "#/components/schemas/TextFieldOutput_2024_06_14" - }, - { - "$ref": "#/components/schemas/NumberFieldOutput_2024_06_14" - }, - { - "$ref": "#/components/schemas/TextAreaFieldOutput_2024_06_14" - }, - { - "$ref": "#/components/schemas/SelectFieldOutput_2024_06_14" - }, - { - "$ref": "#/components/schemas/MultiSelectFieldOutput_2024_06_14" - }, - { - "$ref": "#/components/schemas/MultiEmailFieldOutput_2024_06_14" - }, - { - "$ref": "#/components/schemas/CheckboxGroupFieldOutput_2024_06_14" - }, - { - "$ref": "#/components/schemas/RadioGroupFieldOutput_2024_06_14" - }, - { - "$ref": "#/components/schemas/BooleanFieldOutput_2024_06_14" - }, - { - "$ref": "#/components/schemas/UrlFieldOutput_2024_06_14" - } - ] - } - }, - "disableGuests": { - "type": "boolean" - }, - "slotInterval": { - "type": "object", - "example": 60, - "nullable": true - }, - "minimumBookingNotice": { - "type": "number", - "example": 0 - }, - "beforeEventBuffer": { - "type": "number", - "example": 0 - }, - "afterEventBuffer": { - "type": "number", - "example": 0 - }, - "recurrence": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/Recurrence_2024_06_14" - } - ] - }, - "metadata": { - "type": "object" - }, - "price": { - "type": "number" - }, - "currency": { - "type": "string" - }, - "lockTimeZoneToggleOnBookingPage": { - "type": "boolean" - }, - "seatsPerTimeSlot": { - "type": "object", - "nullable": true - }, - "forwardParamsSuccessRedirect": { - "type": "object", - "nullable": true - }, - "successRedirectUrl": { - "type": "object", - "nullable": true - }, - "isInstantEvent": { - "type": "boolean" - }, - "seatsShowAvailabilityCount": { - "type": "boolean", - "nullable": true - }, - "scheduleId": { - "type": "number", - "nullable": true - }, - "bookingLimitsCount": { - "type": "object" - }, - "bookerActiveBookingsLimit": { - "$ref": "#/components/schemas/BookerActiveBookingsLimitOutput_2024_06_14" - }, - "onlyShowFirstAvailableSlot": { - "type": "boolean" - }, - "bookingLimitsDuration": { - "type": "object" - }, - "bookingWindow": { - "type": "array", - "description": "Limit how far in the future this event can be booked", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/BusinessDaysWindow_2024_06_14" - }, - { - "$ref": "#/components/schemas/CalendarDaysWindow_2024_06_14" - }, - { - "$ref": "#/components/schemas/RangeWindow_2024_06_14" - } - ] - } - }, - "bookerLayouts": { - "$ref": "#/components/schemas/BookerLayouts_2024_06_14" - }, - "confirmationPolicy": { - "type": "object" - }, - "requiresBookerEmailVerification": { - "type": "boolean" - }, - "hideCalendarNotes": { - "type": "boolean" - }, - "color": { - "$ref": "#/components/schemas/EventTypeColor_2024_06_14" - }, - "seats": { - "$ref": "#/components/schemas/Seats_2024_06_14" - }, - "offsetStart": { - "type": "number" - }, - "customName": { - "type": "string" - }, - "destinationCalendar": { - "$ref": "#/components/schemas/DestinationCalendar_2024_06_14" - }, - "useDestinationCalendarEmail": { - "type": "boolean" - }, - "hideCalendarEventDetails": { - "type": "boolean" - }, - "hideOrganizerEmail": { - "type": "boolean", - "description": "Boolean to Hide organizer's email address from the booking screen, email notifications, and calendar events" - }, - "calVideoSettings": { - "description": "Cal video settings for the event type", - "allOf": [ - { - "$ref": "#/components/schemas/CalVideoSettings" - } - ] - }, - "hidden": { - "type": "boolean" - }, - "bookingRequiresAuthentication": { - "type": "boolean", - "description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users who are the event-type owner or org/team admin/owner can book this event type." - }, - "teamId": { - "type": "number" - }, - "ownerId": { - "type": "object", - "nullable": true - }, - "parentEventTypeId": { - "type": "object", - "description": "For managed event types, parent event type is the event type that this event type is based on", - "nullable": true - }, - "hosts": { - "type": "array", - "items": { - "type": "string" - } - }, - "assignAllTeamMembers": { - "type": "boolean" - }, - "schedulingType": { - "type": "string", - "enum": ["roundRobin", "collective", "managed"] - }, - "team": { - "$ref": "#/components/schemas/EventTypeTeam" - }, - "emailSettings": { - "description": "Email settings for this event type. Only available for organization team event types.", - "allOf": [ - { - "$ref": "#/components/schemas/EmailSettings_2024_06_14" - } - ] - }, - "rescheduleWithSameRoundRobinHost": { - "type": "boolean", - "description": "Rescheduled events will be assigned to the same host as initially scheduled." - } - }, - "required": [ - "id", - "lengthInMinutes", - "title", - "slug", - "description", - "locations", - "bookingFields", - "disableGuests", - "recurrence", - "metadata", - "price", - "currency", - "lockTimeZoneToggleOnBookingPage", - "forwardParamsSuccessRedirect", - "successRedirectUrl", - "isInstantEvent", - "scheduleId", - "hidden", - "bookingRequiresAuthentication", - "teamId", - "hosts", - "schedulingType", - "team" - ] - }, "CreateTeamEventTypeOutput": { "type": "object", "properties": { @@ -24945,48 +24989,6 @@ }, "required": ["status", "data"] }, - "StripConnectOutputDto": { - "type": "object", - "properties": { - "authUrl": { - "type": "string" - } - }, - "required": ["authUrl"] - }, - "StripConnectOutputResponseDto": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "success", - "enum": ["success", "error"] - }, - "data": { - "$ref": "#/components/schemas/StripConnectOutputDto" - } - }, - "required": ["status", "data"] - }, - "StripCredentialsSaveOutputResponseDto": { - "type": "object", - "properties": { - "url": { - "type": "string" - } - }, - "required": ["url"] - }, - "StripCredentialsCheckOutputResponseDto": { - "type": "object", - "properties": { - "status": { - "type": "object", - "example": "success" - } - }, - "required": ["status"] - }, "GetDefaultScheduleOutput_2024_06_11": { "type": "object", "properties": {