feat: authentication secured event types (#23217)

* feat: EventType bookingRequiresAuthentication column

* feat: toggle EventType bookingRequiresAuthentication via api

* feat: check bookingRequiresAuthentication when booking

* docs: v2 swagger

* fix: ts error

* refactor: use Forbidden exception instead of Unauthorized

* refactor: use findFirst instead of unique

* fix: only count accepted memberships

* fix: orgs schedules docs

* regenerate docs

* chore: update platform libraries

* fix: unit test

* fix: e2e test
This commit is contained in:
Lauris Skraucis
2025-08-22 15:22:50 +01:00
committed by GitHub
parent aea6e105e6
commit e0dc2ba03a
25 changed files with 810 additions and 17 deletions
+1 -1
View File
@@ -38,7 +38,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.320",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.321",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
"@calcom/prisma": "*",
@@ -22,6 +22,7 @@ import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { KyselyModule } from "@/modules/kysely/kysely.module";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
@@ -53,6 +54,7 @@ import { Module } from "@nestjs/common";
StripeModule,
TeamsModule,
TeamsEventTypesModule,
MembershipsModule,
],
providers: [
TokensRepository,
@@ -13,10 +13,14 @@ import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.ser
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import {
AuthOptionalUser,
GetOptionalUser,
} from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
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 { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
@@ -96,6 +100,7 @@ export class BookingsController_2024_08_13 {
) {}
@Post("/")
@UseGuards(OptionalApiAuthGuard)
@ApiOperation({
summary: "Create a booking",
description: `
@@ -138,9 +143,10 @@ export class BookingsController_2024_08_13 {
async createBooking(
@Body(new CreateBookingInputPipe())
body: CreateBookingInput,
@Req() request: Request
@Req() request: Request,
@GetOptionalUser() user: AuthOptionalUser
): Promise<CreateBookingOutput_2024_08_13> {
const booking = await this.bookingsService.createBooking(request, body);
const booking = await this.bookingsService.createBooking(request, body, user);
if (Array.isArray(booking)) {
await this.bookingsService.billBookings(booking);
@@ -3,6 +3,7 @@ import { AppModule } from "@/app.module";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { Locales } from "@/lib/enums/locales";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import {
CreateManagedUserData,
CreateManagedUserOutput,
@@ -80,7 +81,7 @@ describe("Managed user bookings 2024-08-13", () => {
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UsersModule],
imports: [AppModule, UsersModule, MembershipsModule],
}).compile();
app = moduleRef.createNestApplication();
@@ -772,6 +773,77 @@ describe("Managed user bookings 2024-08-13", () => {
});
});
describe("event type booking requires authentication", () => {
let eventTypeRequiringAuthenticationId: number;
let body: CreateBookingInput_2024_08_13;
beforeAll(async () => {
const eventTypeRequiringAuthentication = await eventTypesRepositoryFixture.create(
{
title: `event-type-requiring-authentication-${randomString()}`,
slug: `event-type-requiring-authentication-${randomString()}`,
length: 60,
requiresConfirmation: true,
bookingRequiresAuthentication: true,
},
secondManagedUser.user.id
);
eventTypeRequiringAuthenticationId = eventTypeRequiringAuthentication.id;
body = {
start: new Date(Date.UTC(2030, 0, 9, 15, 0, 0)).toISOString(),
eventTypeId: eventTypeRequiringAuthenticationId,
attendee: {
email: "external@example.com",
name: "External Attendee",
timeZone: "Europe/Rome",
},
};
});
it("can't be booked without credentials", async () => {
await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(401);
});
it("can't be booked with managed user credentials who is not admin and not event type owner", async () => {
await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(403);
});
it("can be booked with managed user credentials who is event type owner", async () => {
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(201);
const bookingId = response.body.data.id;
await bookingsRepositoryFixture.deleteById(bookingId);
});
it("can be booked with managed user credentials who is admin", async () => {
const response = await request(app.getHttpServer())
.post(`/v2/bookings`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
.expect(201);
const bookingId = response.body.data.id;
await bookingsRepositoryFixture.deleteById(bookingId);
});
});
afterAll(async () => {
await userRepositoryFixture.delete(firstManagedUser.user.id);
await userRepositoryFixture.delete(secondManagedUser.user.id);
@@ -6,9 +6,12 @@ import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/servi
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { getPagination } from "@/lib/pagination/pagination";
import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
import { BillingService } from "@/modules/billing/services/billing.service";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
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";
@@ -18,7 +21,14 @@ import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-eve
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { UsersService } from "@/modules/users/services/users.service";
import { UsersRepository, UserWithProfile } from "@/modules/users/users.repository";
import { ConflictException, Injectable, Logger, NotFoundException } from "@nestjs/common";
import {
ConflictException,
ForbiddenException,
Injectable,
Logger,
NotFoundException,
UnauthorizedException,
} from "@nestjs/common";
import { BadRequestException } from "@nestjs/common";
import { Request } from "express";
import { DateTime } from "luxon";
@@ -95,10 +105,12 @@ 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
) {}
async createBooking(request: Request, body: CreateBookingInput) {
async createBooking(request: Request, body: CreateBookingInput, authUser: AuthOptionalUser) {
let bookingTeamEventType = false;
try {
const eventType = await this.getBookedEventType(body);
@@ -108,6 +120,7 @@ export class BookingsService_2024_08_13 {
if (!eventType) {
this.errorsBookingsService.handleEventTypeToBeBookedNotFound(body);
}
await this.checkBookingRequiresAuthenticationSetting(eventType, authUser);
if (eventType.schedulingType === "MANAGED") {
throw new BadRequestException(
@@ -142,6 +155,56 @@ export class BookingsService_2024_08_13 {
}
}
async checkBookingRequiresAuthenticationSetting(
eventType: EventTypeWithOwnerAndTeam,
authUser: AuthOptionalUser
) {
if (!eventType.bookingRequiresAuthentication) return true;
if (!authUser) {
throw new UnauthorizedException(
"checkBookingRequiresAuthentication - request must be authenticated by passing credentials belonging to event type owner, host or team or org admin or owner."
);
}
const authUserId = authUser.id;
const authUserRole = authUser.role;
const eventTypeId = eventType.id;
const teamId = eventType.teamId;
const bookingUserId = eventType.owner?.id || null;
if (authUserRole === "ADMIN") return;
if (bookingUserId === authUserId) return;
if (eventTypeId) {
const [isUserHost, isUserAssigned] = await Promise.all([
this.eventTypesRepository.isUserHostOfEventType(authUserId, eventTypeId),
this.eventTypesRepository.isUserAssignedToEventType(authUserId, eventTypeId),
]);
if (isUserHost || isUserAssigned) return;
}
if (teamId) {
const membership = await this.membershipsRepository.getUserAdminOrOwnerTeamMembership(
authUserId,
teamId
);
if (membership) return;
}
if (
bookingUserId &&
(await this.membershipsService.isUserOrgAdminOrOwnerOfAnotherUser(authUserId, bookingUserId))
) {
return;
}
throw new ForbiddenException(
"checkBookingRequiresAuthentication - user is not authorized to access this event type. User has to be either event type owner, host, team admin or owner or org admin or owner."
);
}
async getBookedEventType(body: CreateBookingInput) {
if (body.eventTypeId) {
return await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(body.eventTypeId);
@@ -498,6 +498,7 @@ describe("Event types Endpoints", () => {
lightThemeHex: "#fafafa",
},
customName: `{Event type title} between {Organiser} and {Scheduler}`,
bookingRequiresAuthentication: true,
};
return request(app.getHttpServer())
@@ -570,6 +571,7 @@ describe("Event types Endpoints", () => {
];
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
expect(createdEventType.bookingRequiresAuthentication).toEqual(true);
eventType = responseBody.data;
});
});
@@ -1026,6 +1028,7 @@ describe("Event types Endpoints", () => {
lightThemeHex: "#fafafa",
},
customName: `{Event type title} betweennnnnnnnnnn {Organiser} and {Scheduler}`,
bookingRequiresAuthentication: false,
};
return request(app.getHttpServer())
@@ -1120,6 +1123,8 @@ describe("Event types Endpoints", () => {
eventType.color = updatedEventType.color;
eventType.bookingFields = updatedEventType.bookingFields;
eventType.calVideoSettings = updatedEventType.calVideoSettings;
expect(updatedEventType.bookingRequiresAuthentication).toEqual(false);
});
});
@@ -2028,7 +2033,6 @@ describe("Event types Endpoints", () => {
const username = name;
let user: User;
let legacyEventTypeId1: number;
let legacyEventTypeId2: number;
beforeAll(async () => {
const moduleRef = await withApiAuth(
@@ -132,4 +132,26 @@ export class EventTypesRepository_2024_06_14 {
async deleteEventType(eventTypeId: number) {
return this.dbWrite.prisma.eventType.delete({ where: { id: eventTypeId } });
}
async isUserHostOfEventType(userId: number, eventTypeId: number) {
const eventType = await this.dbRead.prisma.eventType.findFirst({
where: {
id: eventTypeId,
hosts: { some: { userId: userId } },
},
select: { id: true },
});
return !!eventType;
}
async isUserAssignedToEventType(userId: number, eventTypeId: number) {
const eventType = await this.dbRead.prisma.eventType.findFirst({
where: {
id: eventTypeId,
users: { some: { id: userId } },
},
select: { id: true },
});
return !!eventType;
}
}
@@ -90,6 +90,7 @@ type Input = Pick<
| "hideOrganizerEmail"
| "calVideoSettings"
| "hidden"
| "bookingRequiresAuthentication"
>;
@Injectable()
@@ -129,6 +130,7 @@ export class OutputEventTypesService_2024_06_14 {
hideOrganizerEmail,
calVideoSettings,
hidden,
bookingRequiresAuthentication,
} = databaseEventType;
const locations = this.transformLocations(databaseEventType.locations);
@@ -206,6 +208,7 @@ export class OutputEventTypesService_2024_06_14 {
hideOrganizerEmail,
calVideoSettings,
hidden,
bookingRequiresAuthentication,
};
}
@@ -2,6 +2,8 @@ import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.st
import { ExecutionContext } from "@nestjs/common";
import { createParamDecorator } from "@nestjs/common";
export type AuthOptionalUser = ApiAuthGuardUser | null;
export const GetOptionalUser = createParamDecorator<
keyof ApiAuthGuardUser | (keyof ApiAuthGuardUser)[],
ExecutionContext
@@ -1,7 +1,7 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { Injectable } from "@nestjs/common";
import { MembershipRole } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
@Injectable()
export class MembershipsRepository {
@@ -90,6 +90,26 @@ export class MembershipsRepository {
return !!adminMembership;
}
async getOrgIdsWhereUserIsAdminOrOwner(userId: number) {
const userOrgAdminMemberships = await this.dbRead.prisma.membership.findMany({
where: {
userId: userId,
accepted: true,
role: {
in: ["OWNER", "ADMIN"],
},
team: {
parentId: null,
},
},
select: {
teamId: true,
},
});
return userOrgAdminMemberships.map((m) => m.teamId);
}
async createMembership(teamId: number, userId: number, role: MembershipRole, accepted: boolean) {
const membership = await this.dbRead.prisma.membership.create({
data: {
@@ -103,4 +123,46 @@ export class MembershipsRepository {
return membership;
}
async getUserAdminOrOwnerTeamMembership(userId: number, teamId: number) {
return this.dbRead.prisma.membership.findFirst({
where: {
userId: userId,
teamId: teamId,
accepted: true,
role: {
in: ["OWNER", "ADMIN"],
},
},
});
}
async getUserMembershipInOneOfOrgs(userId: number, orgIds: number[]) {
return this.dbRead.prisma.membership.findFirst({
where: {
userId: userId,
accepted: true,
teamId: {
in: orgIds,
},
team: {
parentId: null,
},
},
});
}
async getUserMembershipInOneOfOrgsTeams(userId: number, orgIds: number[]) {
return this.dbRead.prisma.membership.findFirst({
where: {
userId: userId,
accepted: true,
team: {
parentId: {
in: orgIds,
},
},
},
});
}
}
@@ -21,4 +21,28 @@ export class MembershipsService {
"teamId"
);
}
async isUserOrgAdminOrOwnerOfAnotherUser(userId: number, anotherUserId: number) {
const orgIdsWhereUserIsAdminOrOwner = await this.membershipsRepository.getOrgIdsWhereUserIsAdminOrOwner(
userId
);
if (orgIdsWhereUserIsAdminOrOwner.length === 0) {
return false;
}
const anotherUserOrgMembership = await this.membershipsRepository.getUserMembershipInOneOfOrgs(
anotherUserId,
orgIdsWhereUserIsAdminOrOwner
);
if (anotherUserOrgMembership) return true;
const anotherUserOrgTeamMembership = await this.membershipsRepository.getUserMembershipInOneOfOrgsTeams(
anotherUserId,
orgIdsWhereUserIsAdminOrOwner
);
return !!anotherUserOrgTeamMembership;
}
}
@@ -75,6 +75,7 @@ type Input = Pick<
| "team"
| "calVideoSettings"
| "hidden"
| "bookingRequiresAuthentication"
>;
@Injectable()
@@ -43,6 +43,8 @@ export class OrganizationsTeamsSchedulesController {
@DocsTags("Orgs / Teams / Schedules")
@ApiOperation({ summary: "Get all team member schedules" })
async getTeamSchedules(
// note(Lauirs): putting orgId so swagger is generacted correctly
@Param("orgId", ParseIntPipe) _orgId: number,
@Param("teamId", ParseIntPipe) teamId: number,
@Query() queryParams: SkipTakePagination
): Promise<GetSchedulesOutput_2024_06_11> {
@@ -65,6 +67,9 @@ export class OrganizationsTeamsSchedulesController {
summary: "Get schedules of a team member",
})
async getUserSchedules(
// note(Lauirs): putting orgId and teamId so swagger is generacted correctly
@Param("orgId", ParseIntPipe) _orgId: number,
@Param("teamId", ParseIntPipe) _teamId: number,
@Param("userId", ParseIntPipe) userId: number
): Promise<GetSchedulesOutput_2024_06_11> {
const schedules = await this.schedulesService.getUserSchedules(userId);
@@ -1,6 +1,9 @@
import { VERSION_2024_09_04 } from "@/lib/api-versions";
import { OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER, OPTIONAL_X_CAL_CLIENT_ID_HEADER } from "@/lib/docs/headers";
import { GetOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
import {
AuthOptionalUser,
GetOptionalUser,
} from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
import { GetReservedSlotOutput_2024_09_04 } from "@/modules/slots/slots-2024-09-04/outputs/get-reserved-slot.output";
import { GetSlotsOutput_2024_09_04 } from "@/modules/slots/slots-2024-09-04/outputs/get-slots.output";
@@ -26,7 +29,6 @@ import {
ApiResponse as DocsResponse,
ApiQuery,
} from "@nestjs/swagger";
import { User } from "@prisma/client";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
@@ -261,7 +263,7 @@ export class SlotsController_2024_09_04 {
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
async reserveSlot(
@Body() body: ReserveSlotInput_2024_09_04,
@GetOptionalUser() user: User
@GetOptionalUser() user: AuthOptionalUser
): Promise<ReserveSlotOutputResponse_2024_09_04> {
const reservedSlot = await this.slotsService.reserveSlot(body, user?.id);
+257
View File
@@ -5547,6 +5547,97 @@
]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/schedules": {
"get": {
"operationId": "OrganizationsTeamsSchedulesController_getTeamSchedules",
"summary": "Get all team member schedules",
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-secret-key",
"in": "header",
"description": "For platform customers - OAuth client secret key",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-client-id",
"in": "header",
"description": "For platform customers - OAuth client ID",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "orgId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "take",
"required": false,
"in": "query",
"description": "Maximum number of items to return",
"example": 25,
"schema": {
"minimum": 1,
"maximum": 250,
"default": 250,
"type": "number"
}
},
{
"name": "skip",
"required": false,
"in": "query",
"description": "Number of items to skip",
"example": 0,
"schema": {
"minimum": 0,
"default": 0,
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11"
}
}
}
}
},
"tags": [
"Orgs / Teams / Schedules"
]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/stripe/connect": {
"get": {
"operationId": "OrganizationsStripeController_getTeamStripeConnectUrl",
@@ -5720,6 +5811,22 @@
"type": "string"
}
},
{
"name": "orgId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "userId",
"required": true,
@@ -13215,6 +13322,71 @@
]
}
},
"/v2/teams/{teamId}/schedules": {
"get": {
"operationId": "TeamsSchedulesController_getTeamSchedules",
"summary": "Get all team member schedules",
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_ or managed user access token",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "take",
"required": false,
"in": "query",
"description": "Maximum number of items to return",
"example": 25,
"schema": {
"minimum": 1,
"maximum": 250,
"default": 250,
"type": "number"
}
},
{
"name": "skip",
"required": false,
"in": "query",
"description": "Number of items to skip",
"example": 0,
"schema": {
"minimum": 0,
"default": 0,
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11"
}
}
}
}
},
"tags": [
"Teams / Schedules"
]
}
},
"/v2/teams/{teamId}/verified-resources/emails/verification-code/request": {
"post": {
"operationId": "TeamsVerifiedResourcesController_requestEmailVerificationCode",
@@ -16504,6 +16676,14 @@
}
]
},
"hidden": {
"type": "boolean"
},
"bookingRequiresAuthentication": {
"type": "boolean",
"default": false,
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
},
"locations": {
"type": "array",
"description": "Locations where the event will take place. If not provided, cal video link will be used as the location.",
@@ -18166,6 +18346,13 @@
}
]
},
"hidden": {
"type": "boolean"
},
"bookingRequiresAuthentication": {
"type": "boolean",
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
},
"ownerId": {
"type": "number",
"example": 10
@@ -18195,6 +18382,8 @@
"successRedirectUrl",
"isInstantEvent",
"scheduleId",
"hidden",
"bookingRequiresAuthentication",
"ownerId",
"users"
]
@@ -18515,6 +18704,14 @@
}
]
},
"hidden": {
"type": "boolean"
},
"bookingRequiresAuthentication": {
"type": "boolean",
"default": false,
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
},
"locations": {
"type": "array",
"description": "Locations where the event will take place. If not provided, cal video link will be used as the location.",
@@ -20343,6 +20540,14 @@
}
]
},
"hidden": {
"type": "boolean"
},
"bookingRequiresAuthentication": {
"type": "boolean",
"default": false,
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
},
"schedulingType": {
"type": "string",
"enum": [
@@ -20777,6 +20982,13 @@
}
]
},
"hidden": {
"type": "boolean"
},
"bookingRequiresAuthentication": {
"type": "boolean",
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
},
"teamId": {
"type": "number"
},
@@ -21223,6 +21435,14 @@
}
]
},
"hidden": {
"type": "boolean"
},
"bookingRequiresAuthentication": {
"type": "boolean",
"default": false,
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
},
"hosts": {
"type": "array",
"items": {
@@ -22589,6 +22809,11 @@
},
"unit": {
"type": "object",
"enum": [
"hour",
"minute",
"day"
],
"description": "Unit for the offset time",
"example": "hour"
}
@@ -22612,6 +22837,9 @@
"type": {
"type": "string",
"default": "beforeEvent",
"enum": [
"beforeEvent"
],
"description": "Trigger type for the workflow",
"example": "beforeEvent"
}
@@ -22635,6 +22863,9 @@
"type": {
"type": "string",
"default": "afterEvent",
"enum": [
"afterEvent"
],
"description": "Trigger type for the workflow",
"example": "afterEvent"
}
@@ -22650,6 +22881,9 @@
"type": {
"type": "string",
"default": "eventCancelled",
"enum": [
"eventCancelled"
],
"description": "Trigger type for the workflow"
}
},
@@ -22663,6 +22897,9 @@
"type": {
"type": "string",
"default": "newEvent",
"enum": [
"newEvent"
],
"description": "Trigger type for the workflow"
}
},
@@ -22676,6 +22913,9 @@
"type": {
"type": "string",
"default": "rescheduleEvent",
"enum": [
"rescheduleEvent"
],
"description": "Trigger type for the workflow"
}
},
@@ -22697,6 +22937,9 @@
"type": {
"type": "string",
"default": "afterGuestsCalVideoNoShow",
"enum": [
"afterGuestsCalVideoNoShow"
],
"description": "Trigger type for the workflow",
"example": "afterGuestsCalVideoNoShow"
}
@@ -22720,6 +22963,9 @@
"type": {
"type": "string",
"default": "afterHostsCalVideoNoShow",
"enum": [
"afterHostsCalVideoNoShow"
],
"description": "Trigger type for the workflow",
"example": "afterHostsCalVideoNoShow"
}
@@ -23314,6 +23560,17 @@
"properties": {
"type": {
"type": "object",
"enum": [
[
"beforeEvent",
"eventCancelled",
"newEvent",
"afterEvent",
"rescheduleEvent",
"afterHostsCalVideoNoShow",
"afterGuestsCalVideoNoShow"
]
],
"description": "Trigger type for the workflow"
}
},
+235
View File
@@ -5315,6 +5315,95 @@
"tags": ["Orgs / Teams / Routing forms / Responses"]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/schedules": {
"get": {
"operationId": "OrganizationsTeamsSchedulesController_getTeamSchedules",
"summary": "Get all team member schedules",
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "For non-platform customers - value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-secret-key",
"in": "header",
"description": "For platform customers - OAuth client secret key",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-client-id",
"in": "header",
"description": "For platform customers - OAuth client ID",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "orgId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "take",
"required": false,
"in": "query",
"description": "Maximum number of items to return",
"example": 25,
"schema": {
"minimum": 1,
"maximum": 250,
"default": 250,
"type": "number"
}
},
{
"name": "skip",
"required": false,
"in": "query",
"description": "Number of items to skip",
"example": 0,
"schema": {
"minimum": 0,
"default": 0,
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11"
}
}
}
}
},
"tags": ["Orgs / Teams / Schedules"]
}
},
"/v2/organizations/{orgId}/teams/{teamId}/stripe/connect": {
"get": {
"operationId": "OrganizationsStripeController_getTeamStripeConnectUrl",
@@ -5482,6 +5571,22 @@
"type": "string"
}
},
{
"name": "orgId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "userId",
"required": true,
@@ -12629,6 +12734,69 @@
"tags": ["Teams / Memberships"]
}
},
"/v2/teams/{teamId}/schedules": {
"get": {
"operationId": "TeamsSchedulesController_getTeamSchedules",
"summary": "Get all team member schedules",
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_ or managed user access token",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "take",
"required": false,
"in": "query",
"description": "Maximum number of items to return",
"example": 25,
"schema": {
"minimum": 1,
"maximum": 250,
"default": 250,
"type": "number"
}
},
{
"name": "skip",
"required": false,
"in": "query",
"description": "Number of items to skip",
"example": 0,
"schema": {
"minimum": 0,
"default": 0,
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11"
}
}
}
}
},
"tags": ["Teams / Schedules"]
}
},
"/v2/teams/{teamId}/verified-resources/emails/verification-code/request": {
"post": {
"operationId": "TeamsVerifiedResourcesController_requestEmailVerificationCode",
@@ -15566,6 +15734,14 @@
}
]
},
"hidden": {
"type": "boolean"
},
"bookingRequiresAuthentication": {
"type": "boolean",
"default": false,
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
},
"locations": {
"type": "array",
"description": "Locations where the event will take place. If not provided, cal video link will be used as the location.",
@@ -17045,6 +17221,13 @@
}
]
},
"hidden": {
"type": "boolean"
},
"bookingRequiresAuthentication": {
"type": "boolean",
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
},
"ownerId": {
"type": "number",
"example": 10
@@ -17074,6 +17257,8 @@
"successRedirectUrl",
"isInstantEvent",
"scheduleId",
"hidden",
"bookingRequiresAuthentication",
"ownerId",
"users"
]
@@ -17372,6 +17557,14 @@
}
]
},
"hidden": {
"type": "boolean"
},
"bookingRequiresAuthentication": {
"type": "boolean",
"default": false,
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
},
"locations": {
"type": "array",
"description": "Locations where the event will take place. If not provided, cal video link will be used as the location.",
@@ -18935,6 +19128,14 @@
}
]
},
"hidden": {
"type": "boolean"
},
"bookingRequiresAuthentication": {
"type": "boolean",
"default": false,
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
},
"schedulingType": {
"type": "string",
"enum": ["collective", "roundRobin", "managed"],
@@ -19338,6 +19539,13 @@
}
]
},
"hidden": {
"type": "boolean"
},
"bookingRequiresAuthentication": {
"type": "boolean",
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
},
"teamId": {
"type": "number"
},
@@ -19747,6 +19955,14 @@
}
]
},
"hidden": {
"type": "boolean"
},
"bookingRequiresAuthentication": {
"type": "boolean",
"default": false,
"description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type."
},
"hosts": {
"type": "array",
"items": {
@@ -20844,6 +21060,7 @@
},
"unit": {
"type": "object",
"enum": ["hour", "minute", "day"],
"description": "Unit for the offset time",
"example": "hour"
}
@@ -20864,6 +21081,7 @@
"type": {
"type": "string",
"default": "beforeEvent",
"enum": ["beforeEvent"],
"description": "Trigger type for the workflow",
"example": "beforeEvent"
}
@@ -20884,6 +21102,7 @@
"type": {
"type": "string",
"default": "afterEvent",
"enum": ["afterEvent"],
"description": "Trigger type for the workflow",
"example": "afterEvent"
}
@@ -20896,6 +21115,7 @@
"type": {
"type": "string",
"default": "eventCancelled",
"enum": ["eventCancelled"],
"description": "Trigger type for the workflow"
}
},
@@ -20907,6 +21127,7 @@
"type": {
"type": "string",
"default": "newEvent",
"enum": ["newEvent"],
"description": "Trigger type for the workflow"
}
},
@@ -20918,6 +21139,7 @@
"type": {
"type": "string",
"default": "rescheduleEvent",
"enum": ["rescheduleEvent"],
"description": "Trigger type for the workflow"
}
},
@@ -20937,6 +21159,7 @@
"type": {
"type": "string",
"default": "afterGuestsCalVideoNoShow",
"enum": ["afterGuestsCalVideoNoShow"],
"description": "Trigger type for the workflow",
"example": "afterGuestsCalVideoNoShow"
}
@@ -20957,6 +21180,7 @@
"type": {
"type": "string",
"default": "afterHostsCalVideoNoShow",
"enum": ["afterHostsCalVideoNoShow"],
"description": "Trigger type for the workflow",
"example": "afterHostsCalVideoNoShow"
}
@@ -21428,6 +21652,17 @@
"properties": {
"type": {
"type": "object",
"enum": [
[
"beforeEvent",
"eventCancelled",
"newEvent",
"afterEvent",
"rescheduleEvent",
"afterHostsCalVideoNoShow",
"afterGuestsCalVideoNoShow"
]
],
"description": "Trigger type for the workflow"
}
},
+1
View File
@@ -147,6 +147,7 @@ const commons = {
instantMeetingParameters: [],
eventTypeColor: null,
hostGroups: [],
bookingRequiresAuthentication: false,
};
export const dynamicEvent = {
+1
View File
@@ -160,6 +160,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
customReplyToEmail: null,
restrictionScheduleId: null,
useBookerTimezone: false,
bookingRequiresAuthentication: false,
...eventType,
};
};
@@ -451,6 +451,15 @@ class BaseCreateEventTypeInput {
@IsBoolean()
@DocsPropertyOptional()
hidden?: boolean;
@IsOptional()
@IsBoolean()
@DocsPropertyOptional({
default: false,
description:
"Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type.",
})
bookingRequiresAuthentication?: boolean;
}
export class CreateEventTypeInput_2024_06_14 extends BaseCreateEventTypeInput {
@IsOptional()
@@ -419,6 +419,15 @@ class BaseUpdateEventTypeInput {
@IsBoolean()
@DocsPropertyOptional()
hidden?: boolean;
@IsOptional()
@IsBoolean()
@DocsPropertyOptional({
default: false,
description:
"Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type.",
})
bookingRequiresAuthentication?: boolean;
}
export class UpdateEventTypeInput_2024_06_14 extends BaseUpdateEventTypeInput {
@IsOptional()
@@ -444,6 +444,14 @@ class BaseEventTypeOutput_2024_06_14 {
@IsBoolean()
@DocsProperty()
hidden?: boolean;
@IsOptional()
@IsBoolean()
@DocsProperty({
description:
"Boolean to require authentication for booking this event type via api. If true, only authenticated users can book this event type.",
})
bookingRequiresAuthentication?: boolean;
}
export class TeamEventTypeResponseHost extends TeamEventTypeHostInput {
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "EventType" ADD COLUMN "bookingRequiresAuthentication" BOOLEAN NOT NULL DEFAULT false;
+2
View File
@@ -228,6 +228,8 @@ model EventType {
restrictionSchedule Schedule? @relation("restrictionSchedule", fields: [restrictionScheduleId], references: [id])
hostGroups HostGroup[]
bookingRequiresAuthentication Boolean @default(false)
@@unique([userId, slug])
@@unique([teamId, slug])
@@unique([userId, parentId])
+1
View File
@@ -681,6 +681,7 @@ export const allManagedEventTypeProps: { [k in keyof Omit<Prisma.EventTypeSelect
rescheduleWithSameRoundRobinHost: true,
maxLeadThreshold: true,
customReplyToEmail: true,
bookingRequiresAuthentication: true,
};
// All properties that are defined as unlocked based on all managed props
+5 -5
View File
@@ -2493,7 +2493,7 @@ __metadata:
"@axiomhq/winston": ^1.2.0
"@calcom/platform-constants": "*"
"@calcom/platform-enums": "*"
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.320"
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.321"
"@calcom/platform-types": "*"
"@calcom/platform-utils": "*"
"@calcom/prisma": "*"
@@ -3553,13 +3553,13 @@ __metadata:
languageName: unknown
linkType: soft
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.320":
version: 0.0.320
resolution: "@calcom/platform-libraries@npm:0.0.320"
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.321":
version: 0.0.321
resolution: "@calcom/platform-libraries@npm:0.0.321"
dependencies:
"@calcom/features": "*"
"@calcom/lib": "*"
checksum: 822539306fd09e4accdc071dc109740a32574b6d299cc69a793554a765b0a67408af3b19f6420da9499d1e4ab5855e1940807005645a7b1d567340e251747687
checksum: 634b092311d9952d6ce1eec3a55fc18ab314d282d087ca7d9e9baf3f53f1e718e81d2b2cb6613f377d129c864290eb14f89770cfdd0c14e8968a34db093163f5
languageName: node
linkType: hard