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>
This commit is contained in:
co-authored by
Udit Takkar
parent
81a99ea0b2
commit
829edec0ea
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
|
||||
+128
-8
@@ -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<EventTypeOutput_2024_06_14> = 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<TeamEventTypeOutput_2024_06_14> = 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<TeamEventTypeOutput_2024_06_14> = 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<EventTypeOutput_2024_06_14> = 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<EventTypeOutput_2024_06_14> = 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+23
-5
@@ -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: `<Note>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.</Note>`,
|
||||
description: `<Note>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.</Note>
|
||||
|
||||
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<GetEventTypeOutput_2024_06_14> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
+10
-7
@@ -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;
|
||||
}
|
||||
|
||||
+32
-2
@@ -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<DatabaseTeamEventType | ({ ownerId: number } & DatabaseEventType) | null> {
|
||||
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) {
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
return this.membershipsService.isUserOrgAdminOrOwnerOfAnotherUser(authUserId, eventTypeOwnerId);
|
||||
}
|
||||
}
|
||||
+421
-419
@@ -11579,7 +11579,7 @@
|
||||
"get": {
|
||||
"operationId": "EventTypesController_2024_06_14_getEventTypeById",
|
||||
"summary": "Get an event type",
|
||||
"description": "<Note>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.</Note>",
|
||||
"description": "<Note>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.</Note>\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": {
|
||||
|
||||
Reference in New Issue
Block a user