Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca19aa00c1 | ||
|
|
467c979aa7 | ||
|
|
9f9bf10040 | ||
|
|
8102192987 | ||
|
|
232f9d288c | ||
|
|
7dab100382 | ||
|
|
91d4f1c219 | ||
|
|
a64e7f9c2a | ||
|
|
3c1297aa72 | ||
|
|
96f7a01c5f | ||
|
|
a4a05878ef | ||
|
|
352d68ae1c | ||
|
|
ffd034679c | ||
|
|
d64ae3c901 | ||
|
|
7250e22257 | ||
|
|
f459e5d9c4 | ||
|
|
30bb0f1383 | ||
|
|
24c993a3d2 | ||
|
|
aa4276be5b | ||
|
|
413aba6e52 | ||
|
|
c2547acb3d | ||
|
|
cc1b6bac4a | ||
|
|
d5d90a8b87 | ||
|
|
40333c81c5 | ||
|
|
ff92979311 | ||
|
|
f63edef48d | ||
|
|
37e56ebbf9 | ||
|
|
07c38f1f33 | ||
|
|
192238f4bf | ||
|
|
94a26031a8 | ||
|
|
e9f20b1a0b | ||
|
|
7cb9e2b99b | ||
|
|
dd45d8cdbc | ||
|
|
3a181f1dac | ||
|
|
c3abfa51ba | ||
|
|
6498af167a | ||
|
|
30dce7bacd | ||
|
|
3be9e1bd38 | ||
|
|
db52f44293 | ||
|
|
20d6eb5200 | ||
|
|
424d94f16d | ||
|
|
73e7d6f986 | ||
|
|
145001d26e | ||
|
|
5d2d7ce979 | ||
|
|
4dc21ab2b0 | ||
|
|
df6ee73e05 | ||
|
|
587af33179 | ||
|
|
3b381b037c | ||
|
|
0968fc56eb | ||
|
|
13044cb5b0 | ||
|
|
580b03f2b9 | ||
|
|
543d4bb6b3 | ||
|
|
21d87d5a80 | ||
|
|
194719e7ba | ||
|
|
dc666fc770 | ||
|
|
50a76e6b27 | ||
|
|
be4d3170db | ||
|
|
7b97002230 | ||
|
|
6319d63e1f | ||
|
|
2e18478b5c | ||
|
|
0ef93edd56 | ||
|
|
d6a3661e58 | ||
|
|
be404436d7 | ||
|
|
b1ebefb854 | ||
|
|
f8aa2701b6 | ||
|
|
034dfc8e6b | ||
|
|
a6b6898697 | ||
|
|
0e5a0bfa7b | ||
|
|
6c01f5eace | ||
|
|
ef2c34c55f | ||
|
|
7c1582c06f | ||
|
|
367e2666c6 | ||
|
|
209a401113 | ||
|
|
837349e3d3 | ||
|
|
e66602aa5c | ||
|
|
082f8e1b04 | ||
|
|
07716474d3 | ||
|
|
f335cc4269 | ||
|
|
f19750cfaf | ||
|
|
46810f69ca | ||
|
|
a7247c594b | ||
|
|
8de7876f72 | ||
|
|
49f75f5070 | ||
|
|
4ff543efc7 | ||
|
|
8af5145b56 | ||
|
|
a31242cdc5 | ||
|
|
e20df1e7aa | ||
|
|
960a368175 | ||
|
|
49cb86dfe9 | ||
|
|
b644ec58af | ||
|
|
bd92b89bf0 | ||
|
|
495ac74255 | ||
|
|
c3d35fd8c8 | ||
|
|
87bc8d649a |
+10
-1
@@ -17,6 +17,7 @@
|
||||
#
|
||||
|
||||
# To enable enterprise-only features please add your environment variable to the .env file then make your way to /auth/setup to select your license and follow instructions.
|
||||
|
||||
CALCOM_LICENSE_KEY=
|
||||
# Signature token for the Cal.com License API (used for self-hosted integrations)
|
||||
# We will give you a token when we provide you with a license key this ensure you and only you can communicate with the Cal.com License API for your license key
|
||||
@@ -192,7 +193,10 @@ NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
|
||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
|
||||
STRIPE_TEAM_MONTHLY_PRICE_ID=
|
||||
STRIPE_TEAM_PRODUCT_ID=
|
||||
# It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID
|
||||
STRIPE_ORG_MONTHLY_PRICE_ID=
|
||||
STRIPE_ORG_PRODUCT_ID=
|
||||
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_WEBHOOK_SECRET_APPS=
|
||||
STRIPE_PRIVATE_KEY=
|
||||
@@ -285,7 +289,7 @@ NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL=
|
||||
# use organizations
|
||||
ORGANIZATIONS_ENABLED=
|
||||
NEXT_PUBLIC_ORGANIZATIONS_MIN_SELF_SERVE_SEATS=30
|
||||
NEXT_PUBLIC_ORGANIZATIONS_SELF_SERVE_PRICE=3700 # $37.00 per seat
|
||||
NEXT_PUBLIC_ORGANIZATIONS_SELF_SERVE_PRICE_NEW=37 # $37.00 per seat
|
||||
|
||||
# This variable should only be set to 1 or true if you want to autolink external provider sign-ups with
|
||||
# existing organizations based on email domain address
|
||||
@@ -402,6 +406,11 @@ NEXT_PUBLIC_WEBSITE_TERMS_URL=
|
||||
# [0: silly & upwards, 1: trace & upwards, 2: debug & upwards, 3: info & upwards, 4: warn & upwards, 5: error & fatal, 6: fatal]
|
||||
NEXT_PUBLIC_LOGGER_LEVEL=
|
||||
|
||||
# For storing Daily Video recordings on S3 Bucket
|
||||
CAL_VIDEO_BUCKET_NAME=
|
||||
CAL_VIDEO_BUCKET_REGION=
|
||||
CAL_VIDEO_ASSUME_ROLE_ARN=
|
||||
|
||||
# Used to use Replexica SDK, a tool for real-time AI-powered localization
|
||||
REPLEXICA_API_KEY=
|
||||
|
||||
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
undecided:
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/prisma"
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/prisma"
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
@@ -1,9 +0,0 @@
|
||||
- [x] Refactor individual calendar links functions to create the variables and just use the variables in the string literal instead of functions being called in the string literal
|
||||
- [x] Must ensure during review that logically there is no change in the functionality of booking-single-view.tsx
|
||||
|
||||
|
||||
- Testing
|
||||
- [x] Verify Google Calendar, Ics, Office 365 - Compare with Production
|
||||
- [x] Event Description, Booking title, Booking location, Start time, End time - All correct
|
||||
- [x] Recurring event?
|
||||
- [x] Dynamic Group booking?
|
||||
@@ -38,7 +38,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.147",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.152",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
X_CAL_PLATFORM_EMBED,
|
||||
} from "@calcom/platform-constants";
|
||||
|
||||
import { CalendarServiceExceptionFilter } from "./filters/calendar-service-exception.filter";
|
||||
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
|
||||
|
||||
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
|
||||
@@ -76,6 +77,7 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
||||
app.useGlobalFilters(new ZodExceptionFilter());
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
app.useGlobalFilters(new TRPCExceptionFilter());
|
||||
app.useGlobalFilters(new CalendarServiceExceptionFilter());
|
||||
|
||||
app.use(cookieParser());
|
||||
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { BookingsController_2024_04_15 } from "@/ee/bookings/2024-04-15/controllers/bookings.controller";
|
||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
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";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
@@ -33,6 +38,11 @@ import { Module } from "@nestjs/common";
|
||||
ApiKeysRepository,
|
||||
OAuthClientUsersService,
|
||||
PlatformBookingsService,
|
||||
CalendarsService,
|
||||
CredentialsRepository,
|
||||
AppsRepository,
|
||||
CalendarsRepository,
|
||||
SelectedCalendarsRepository,
|
||||
],
|
||||
controllers: [BookingsController_2024_04_15],
|
||||
})
|
||||
|
||||
@@ -4,19 +4,24 @@ import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bo
|
||||
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";
|
||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
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 { 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";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
@@ -47,6 +52,11 @@ import { Module } from "@nestjs/common";
|
||||
BookingSeatRepository,
|
||||
ApiKeysRepository,
|
||||
PlatformBookingsService,
|
||||
CalendarsService,
|
||||
CredentialsRepository,
|
||||
AppsRepository,
|
||||
CalendarsRepository,
|
||||
SelectedCalendarsRepository,
|
||||
],
|
||||
controllers: [BookingsController_2024_08_13],
|
||||
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
|
||||
|
||||
@@ -39,11 +39,15 @@ import { Request } from "express";
|
||||
import { BOOKING_READ, BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
CancelBookingInput,
|
||||
CancelBookingInput_2024_08_13,
|
||||
CancelBookingInputPipe,
|
||||
CancelSeatedBookingInput_2024_08_13,
|
||||
GetBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
RescheduleBookingInput,
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
RescheduleBookingInputPipe,
|
||||
RescheduleSeatedBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import {
|
||||
CreateBookingInputPipe,
|
||||
@@ -65,9 +69,12 @@ import {
|
||||
@DocsTags("Bookings")
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to \`2024-08-13\``,
|
||||
description: `Must be set to ${VERSION_2024_08_13}`,
|
||||
example: VERSION_2024_08_13,
|
||||
required: true,
|
||||
schema: {
|
||||
default: VERSION_2024_08_13,
|
||||
},
|
||||
})
|
||||
export class BookingsController_2024_08_13 {
|
||||
private readonly logger = new Logger("BookingsController_2024_08_13");
|
||||
@@ -99,7 +106,7 @@ export class BookingsController_2024_08_13 {
|
||||
],
|
||||
},
|
||||
description:
|
||||
"Accepts different types of booking input: CreateBookingInput_2024_08_13, CreateInstantBookingInput_2024_08_13, or CreateRecurringBookingInput_2024_08_13",
|
||||
"Accepts different types of booking input: Create Booking (Option 1), Create Instant Booking (Option 2), or Create Recurring Booking (Option 3)",
|
||||
})
|
||||
@ApiExtraModels(
|
||||
CreateBookingInput_2024_08_13,
|
||||
@@ -172,9 +179,19 @@ export class BookingsController_2024_08_13 {
|
||||
@UseGuards(BookingUidGuard)
|
||||
@ApiOperation({
|
||||
summary: "Reschedule a booking",
|
||||
description:
|
||||
"Reschedule a booking by passing `:bookingUid` of the booking that should be rescheduled and pass request body with a new start time to create a new booking.",
|
||||
description: "Reschedule a booking or seated booking",
|
||||
})
|
||||
@ApiBody({
|
||||
schema: {
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(RescheduleBookingInput_2024_08_13) },
|
||||
{ $ref: getSchemaPath(RescheduleSeatedBookingInput_2024_08_13) },
|
||||
],
|
||||
},
|
||||
description:
|
||||
"Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2)",
|
||||
})
|
||||
@ApiExtraModels(RescheduleBookingInput_2024_08_13, RescheduleSeatedBookingInput_2024_08_13)
|
||||
async rescheduleBooking(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body(new RescheduleBookingInputPipe())
|
||||
@@ -199,6 +216,17 @@ export class BookingsController_2024_08_13 {
|
||||
For seated bookings to cancel one individual booking provide :bookingUid and :seatUid in the request body. For recurring seated bookings it is not possible to cancel all of them with 1 call
|
||||
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.`,
|
||||
})
|
||||
@ApiBody({
|
||||
schema: {
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(CancelBookingInput_2024_08_13) },
|
||||
{ $ref: getSchemaPath(CancelSeatedBookingInput_2024_08_13) },
|
||||
],
|
||||
},
|
||||
description:
|
||||
"Accepts different types of cancel booking input: Cancel Booking (Option 1) or Cancel Seated Booking (Option 2)",
|
||||
})
|
||||
@ApiExtraModels(CancelBookingInput_2024_08_13, CancelSeatedBookingInput_2024_08_13)
|
||||
async cancelBooking(
|
||||
@Req() request: Request,
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
|
||||
@@ -40,6 +40,8 @@ import {
|
||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
|
||||
|
||||
describe("Seated bookings", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
@@ -330,7 +332,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
location: googleMeetUrl,
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
@@ -383,6 +385,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
@@ -418,6 +424,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
@@ -797,7 +807,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
location: googleMeetUrl,
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
@@ -850,6 +860,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
@@ -885,6 +899,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
@@ -910,7 +928,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
location: googleMeetUrl,
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
@@ -966,6 +984,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: nameAttendeeOne,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: createdRecurringSeatedBooking[0].attendees[0].metadata,
|
||||
});
|
||||
@@ -982,6 +1004,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: nameAttendeeTwo,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
@@ -1017,6 +1043,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: nameAttendeeOne,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: createdRecurringSeatedBooking[0].attendees[0].metadata,
|
||||
});
|
||||
@@ -1030,6 +1060,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: nameAttendeeTwo,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||
|
||||
@@ -66,6 +68,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const team2EventTypeSlug = `team-bookings-event-type-${randomString()}`;
|
||||
const phoneOnlyEventTypeSlug = `team-bookings-event-type-${randomString()}`;
|
||||
|
||||
let phoneBasedBooking: BookingOutput_2024_08_13;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
teamUserEmail,
|
||||
@@ -408,12 +412,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
|
||||
it("should create a phone based booking", async () => {
|
||||
const phoneNumber = "+919876543210";
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
|
||||
eventTypeId: phoneOnlyEventTypeId,
|
||||
attendee: {
|
||||
name: "alice",
|
||||
phoneNumber: "+919876543210",
|
||||
phoneNumber,
|
||||
timeZone: "Europe/Madrid",
|
||||
language: "es",
|
||||
},
|
||||
@@ -453,6 +458,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
expect(data.meetingUrl).toEqual(body.meetingUrl);
|
||||
expect(data.absentHost).toEqual(false);
|
||||
expect(data.bookingFieldsResponses.attendeePhoneNumber).toEqual(phoneNumber);
|
||||
phoneBasedBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
@@ -619,6 +626,59 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("reschedule", () => {
|
||||
it("should should reschedule phone based booking", async () => {
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${phoneBasedBooking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts.length).toEqual(1);
|
||||
expect(data.hosts[0].id).toEqual(teamUser.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 8, 14, 15, 0)).toISOString());
|
||||
expect(data.duration).toEqual(15);
|
||||
expect(data.eventTypeId).toEqual(phoneOnlyEventTypeId);
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: phoneBasedBooking.attendees[0].name,
|
||||
email: phoneBasedBooking.attendees[0].email,
|
||||
phoneNumber: phoneBasedBooking.attendees[0].phoneNumber,
|
||||
timeZone: phoneBasedBooking.attendees[0].timeZone,
|
||||
language: phoneBasedBooking.attendees[0].language,
|
||||
absent: false,
|
||||
});
|
||||
expect(data.meetingUrl).toEqual(phoneBasedBooking.meetingUrl);
|
||||
expect(data.absentHost).toEqual(false);
|
||||
expect(data.bookingFieldsResponses.attendeePhoneNumber).toEqual(
|
||||
phoneBasedBooking.bookingFieldsResponses.attendeePhoneNumber
|
||||
);
|
||||
phoneBasedBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
|
||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||
import { CreateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/create-event-type.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
@@ -27,10 +28,12 @@ import { withApiAuth } from "test/utils/withApiAuth";
|
||||
import {
|
||||
CAL_API_VERSION_HEADER,
|
||||
SUCCESS_STATUS,
|
||||
VERSION_2024_06_14,
|
||||
VERSION_2024_08_13,
|
||||
X_CAL_CLIENT_ID,
|
||||
} from "@calcom/platform-constants";
|
||||
import {
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
GetBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
@@ -95,6 +98,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
organization = await teamRepositoryFixture.create({
|
||||
name: `user-bookings-organization-${randomString()}`,
|
||||
isOrganization: true,
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
@@ -146,7 +150,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
startTime: new Date(Date.UTC(2020, 0, 8, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2020, 0, 8, 14, 0, 0)),
|
||||
title: "peer coding lets goo",
|
||||
uid: "booking-in-the-past",
|
||||
uid: `booking-in-the-past-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
@@ -326,6 +330,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
|
||||
it("should create a booking", async () => {
|
||||
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId,
|
||||
@@ -335,7 +340,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
location: googleMeetUrl,
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
@@ -389,6 +394,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
email: body.attendee.email,
|
||||
...body.bookingFieldsResponses,
|
||||
guests: body.guests,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
});
|
||||
expect(data.guests).toEqual(body.guests);
|
||||
|
||||
@@ -972,6 +981,64 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("createdAt filter", () => {
|
||||
it("should should get bookings after specified createdAt time", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?afterCreatedAt=${createdRecurringBooking[1].createdAt}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get bookings before specified createdAt time", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?beforeCreatedAt=${createdRecurringBooking[0].createdAt}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get bookings in createdAt range", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(
|
||||
`/v2/bookings?afterCreatedAt=${createdRecurringBooking[0].createdAt}&beforeCreatedAt=${createdRecurringBooking[1].createdAt}`
|
||||
)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should should sort bookings by start in descending order", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortStart=desc`)
|
||||
@@ -1537,6 +1604,440 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("booking location", () => {
|
||||
const address = "123 Main St";
|
||||
const link = "https://cal.com/join/123456";
|
||||
const phone = "+37121999999";
|
||||
|
||||
let eventTypeWithAllLocationsId: number;
|
||||
it("can create event type with all locations except google meet", async () => {
|
||||
const eventTypeBody: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "book using any location",
|
||||
slug: "book-using-any-location",
|
||||
lengthInMinutes: 15,
|
||||
locations: [
|
||||
{
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
},
|
||||
{
|
||||
type: "address",
|
||||
address,
|
||||
public: true,
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
link,
|
||||
public: true,
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
phone,
|
||||
public: true,
|
||||
},
|
||||
{
|
||||
type: "attendeeAddress",
|
||||
},
|
||||
{
|
||||
type: "attendeePhone",
|
||||
},
|
||||
{
|
||||
type: "attendeeDefined",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const eventTypeResponse = await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(eventTypeBody)
|
||||
.expect(201);
|
||||
const eventTypeResponseBody: CreateEventTypeOutput_2024_06_14 = eventTypeResponse.body;
|
||||
const createdEventType = eventTypeResponseBody.data;
|
||||
expect(createdEventType).toHaveProperty("id");
|
||||
expect(createdEventType.locations).toHaveLength(7);
|
||||
expect(createdEventType.locations).toEqual(createdEventType.locations);
|
||||
eventTypeWithAllLocationsId = createdEventType.id;
|
||||
});
|
||||
|
||||
it("can book with cal video location", async () => {
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
},
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
const createdBooking = bookingResponseBody.data;
|
||||
expect(createdBooking).toHaveProperty("id");
|
||||
|
||||
if (responseDataIsBooking(createdBooking)) {
|
||||
expect(
|
||||
createdBooking.location?.startsWith("http") || createdBooking.location === "integrations:daily"
|
||||
).toEqual(true);
|
||||
await bookingsRepositoryFixture.deleteById(createdBooking.id);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
|
||||
it("can book with address location", async () => {
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "address",
|
||||
},
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
const createdBooking = bookingResponseBody.data;
|
||||
expect(createdBooking).toHaveProperty("id");
|
||||
|
||||
if (responseDataIsBooking(createdBooking)) {
|
||||
expect(createdBooking.location).toEqual(address);
|
||||
await bookingsRepositoryFixture.deleteById(createdBooking.id);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
|
||||
it("can book with link location", async () => {
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "link",
|
||||
},
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
const createdBooking = bookingResponseBody.data;
|
||||
expect(createdBooking).toHaveProperty("id");
|
||||
|
||||
if (responseDataIsBooking(createdBooking)) {
|
||||
expect(createdBooking.location).toEqual(link);
|
||||
await bookingsRepositoryFixture.deleteById(createdBooking.id);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
|
||||
it("can book with link location", async () => {
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "phone",
|
||||
},
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
const createdBooking = bookingResponseBody.data;
|
||||
expect(createdBooking).toHaveProperty("id");
|
||||
|
||||
if (responseDataIsBooking(createdBooking)) {
|
||||
expect(createdBooking.location).toEqual(phone);
|
||||
await bookingsRepositoryFixture.deleteById(createdBooking.id);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
|
||||
it("can book with attendeeAddress location", async () => {
|
||||
const attendeeAddress = "123 Example St, City, Valhalla";
|
||||
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "attendeeAddress",
|
||||
address: attendeeAddress,
|
||||
},
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
const createdBooking = bookingResponseBody.data;
|
||||
expect(createdBooking).toHaveProperty("id");
|
||||
|
||||
if (responseDataIsBooking(createdBooking)) {
|
||||
expect(createdBooking.location).toEqual(attendeeAddress);
|
||||
await bookingsRepositoryFixture.deleteById(createdBooking.id);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
|
||||
xit("can book with attendeeAddress location", async () => {
|
||||
const attendeePhone = "+37120993151";
|
||||
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "attendeePhone",
|
||||
phone: attendeePhone,
|
||||
},
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
const createdBooking = bookingResponseBody.data;
|
||||
expect(createdBooking).toHaveProperty("id");
|
||||
|
||||
if (responseDataIsBooking(createdBooking)) {
|
||||
expect(createdBooking.location).toEqual(attendeePhone);
|
||||
await bookingsRepositoryFixture.deleteById(createdBooking.id);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
|
||||
it("can book with attendeeDefined location", async () => {
|
||||
const attendeeDefinedLocation = "namek 100";
|
||||
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "attendeeDefined",
|
||||
location: attendeeDefinedLocation,
|
||||
},
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
const createdBooking = bookingResponseBody.data;
|
||||
expect(createdBooking).toHaveProperty("id");
|
||||
|
||||
if (responseDataIsBooking(createdBooking)) {
|
||||
expect(createdBooking.location).toEqual(attendeeDefinedLocation);
|
||||
await bookingsRepositoryFixture.deleteById(createdBooking.id);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
|
||||
it("can't book with not location that is not in event type", async () => {
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "integration",
|
||||
integration: "google-meet",
|
||||
},
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("can't book with invalid location type", async () => {
|
||||
const bookingBody = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "blablabala",
|
||||
},
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rescheduling booking with invalid booking responses", () => {
|
||||
it("should reschedule", async () => {
|
||||
const email = `user-bookings-attendee-${randomString(10)}@gmail.com`;
|
||||
const username = `user-bookings-attendee-${randomString(10)}`;
|
||||
const locale = "it";
|
||||
const timeZone = "Europe/Rome";
|
||||
const attendee = await userRepositoryFixture.create({
|
||||
email,
|
||||
username,
|
||||
locale,
|
||||
timeZone,
|
||||
});
|
||||
|
||||
const booking = await bookingsRepositoryFixture.create({
|
||||
uid: `booking-uid-${randomString(10)}`,
|
||||
title: "booking title",
|
||||
startTime: "2060-09-05T11:00:00.000Z",
|
||||
endTime: "2060-09-05T12:00:00.000Z",
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
status: "ACCEPTED",
|
||||
metadata: {},
|
||||
responses: "null",
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: email,
|
||||
name: username,
|
||||
timeZone,
|
||||
locale,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2060, 0, 9, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${booking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.reschedulingReason).toEqual(body.reschedulingReason);
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2060, 0, 9, 15, 0, 0)).toISOString());
|
||||
expect(data.rescheduledFromUid).toEqual(booking.uid);
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.hosts[0].username).toEqual(user.username);
|
||||
expect(data.hosts[0].email).toEqual(user.email);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.eventTypeId).toEqual(booking.eventTypeId);
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: attendee.username,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale,
|
||||
absent: false,
|
||||
});
|
||||
expect(data.bookingFieldsResponses).toEqual({
|
||||
name: attendee.username,
|
||||
email: attendee.email,
|
||||
rescheduledReason: body.reschedulingReason,
|
||||
guests: [],
|
||||
});
|
||||
await bookingsRepositoryFixture.deleteById(data.id);
|
||||
await userRepositoryFixture.delete(attendee.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 {
|
||||
return (
|
||||
!Array.isArray(data) &&
|
||||
|
||||
@@ -229,8 +229,7 @@ export class BookingsService_2024_08_13 {
|
||||
const fetchedBookings: { bookings: { id: number }[] } = await getAllUserBookings({
|
||||
bookingListingByStatus: queryParams.status || [],
|
||||
skip: queryParams.skip ?? 0,
|
||||
// note(Lauris): we substract -1 because getAllUSerBookings child function adds +1 for some reason
|
||||
take: queryParams.take ? queryParams.take - 1 : 100,
|
||||
take: queryParams.take ?? 100,
|
||||
filters: this.inputService.transformGetBookingsFilters(queryParams),
|
||||
ctx: {
|
||||
user,
|
||||
@@ -564,7 +563,9 @@ export class BookingsService_2024_08_13 {
|
||||
throw new BadRequestException(`Booking with uid ${bookingUid} has no event type`);
|
||||
}
|
||||
|
||||
const eventType = await this.eventTypesRepository.getEventTypeByIdIncludeUsersAndTeam(booking.eventTypeId);
|
||||
const eventType = await this.eventTypesRepository.getEventTypeByIdIncludeUsersAndTeam(
|
||||
booking.eventTypeId
|
||||
);
|
||||
if (!eventType) {
|
||||
throw new BadRequestException(`Booking with uid ${bookingUid} has no event type`);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
} from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
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 { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
|
||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
import { defaultBookingResponses } from "@/lib/safe-parse/default-responses-booking";
|
||||
import { safeParse } from "@/lib/safe-parse/safe-parse";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
@@ -15,13 +18,17 @@ import { BadRequestException, Injectable, NotFoundException } from "@nestjs/comm
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { CreationSource } from "@prisma/client";
|
||||
import { isURL, isPhoneNumber } from "class-validator";
|
||||
import { Request } from "express";
|
||||
import { DateTime } from "luxon";
|
||||
import { NextApiRequest } from "next/types";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventTypeMetaDataSchema } from "@calcom/platform-libraries/event-types";
|
||||
import {
|
||||
EventTypeMetaDataSchema,
|
||||
apiToInternalintegrationsMapping,
|
||||
} from "@calcom/platform-libraries/event-types";
|
||||
import {
|
||||
CancelBookingInput,
|
||||
CancelBookingInput_2024_08_13,
|
||||
@@ -35,6 +42,7 @@ import {
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
RescheduleSeatedBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { BookingInputLocation_2024_08_13 } from "@calcom/platform-types/bookings/2024-08-13/inputs/location.input";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
|
||||
type BookingRequest = NextApiRequest & { userId: number | undefined } & OAuthRequestParams;
|
||||
@@ -77,6 +85,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
private readonly config: ConfigService,
|
||||
private readonly apiKeyRepository: ApiKeysRepository,
|
||||
private readonly bookingSeatRepository: BookingSeatRepository,
|
||||
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
|
||||
private readonly platformBookingsService: PlatformBookingsService,
|
||||
private readonly usersRepository: UsersRepository
|
||||
) {}
|
||||
@@ -91,23 +100,21 @@ export class InputBookingsService_2024_08_13 {
|
||||
const newRequest = { ...request };
|
||||
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
|
||||
|
||||
const location = request.body.location || request.body.meetingUrl;
|
||||
this.logger.log(`createBookingRequest_2024_04_15`, {
|
||||
this.logger.log(`createBookingRequest_2024_08_13`, {
|
||||
requestId: request.get("X-Request-Id"),
|
||||
ownerId: userId,
|
||||
location,
|
||||
oAuthClientParams,
|
||||
});
|
||||
|
||||
if (oAuthClientParams) {
|
||||
Object.assign(newRequest, { userId, ...oAuthClientParams, platformBookingLocation: location });
|
||||
Object.assign(newRequest, { userId, ...oAuthClientParams });
|
||||
newRequest.body = {
|
||||
...bodyTransformed,
|
||||
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
|
||||
creationSource: CreationSource.API_V2,
|
||||
};
|
||||
} else {
|
||||
Object.assign(newRequest, { userId, platformBookingLocation: location });
|
||||
Object.assign(newRequest, { userId });
|
||||
newRequest.body = { ...bodyTransformed, noEmail: false, creationSource: CreationSource.API_V2 };
|
||||
}
|
||||
|
||||
@@ -143,6 +150,10 @@ export class InputBookingsService_2024_08_13 {
|
||||
)
|
||||
: inputBooking.attendee.email;
|
||||
|
||||
const inputLocation = inputBooking.location || inputBooking.meetingUrl;
|
||||
this.isBookingLocationWithEventTypeLocations(inputLocation, eventType);
|
||||
const location = inputLocation ? this.transformLocation(inputLocation) : undefined;
|
||||
|
||||
return {
|
||||
start: startTime.toISO(),
|
||||
end: endTime.toISO(),
|
||||
@@ -159,6 +170,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
email: attendeeEmail ?? "",
|
||||
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
|
||||
guests,
|
||||
location,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -196,17 +208,14 @@ export class InputBookingsService_2024_08_13 {
|
||||
const newRequest = { ...request };
|
||||
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
|
||||
|
||||
const location = request.body.location || request.body.meetingUrl;
|
||||
|
||||
if (oAuthClientParams) {
|
||||
Object.assign(newRequest, {
|
||||
userId,
|
||||
...oAuthClientParams,
|
||||
platformBookingLocation: location,
|
||||
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
|
||||
});
|
||||
} else {
|
||||
Object.assign(newRequest, { userId, platformBookingLocation: location });
|
||||
Object.assign(newRequest, { userId });
|
||||
}
|
||||
|
||||
newRequest.body = bodyTransformed.map((event) => ({
|
||||
@@ -217,6 +226,144 @@ export class InputBookingsService_2024_08_13 {
|
||||
return newRequest as unknown as BookingRequest;
|
||||
}
|
||||
|
||||
transformLocation(location: string | BookingInputLocation_2024_08_13): {
|
||||
value: string;
|
||||
optionValue: string;
|
||||
} {
|
||||
if (typeof location === "string") {
|
||||
// note(Lauris): this is for backwards compatibility because before switching to booking location objects
|
||||
// we only received a string. If someone is complaining that their location is not displaying as a URL
|
||||
// or whatever check that they are not providing a string for bookign location but one of the input objects.
|
||||
if (isURL(location, { require_protocol: false }) || location.startsWith("www.")) {
|
||||
return {
|
||||
value: "link",
|
||||
optionValue: location,
|
||||
};
|
||||
}
|
||||
|
||||
if (isPhoneNumber(location)) {
|
||||
return {
|
||||
value: "phone",
|
||||
optionValue: location,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: "somewhereElse",
|
||||
optionValue: location,
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "integration") {
|
||||
const integration = apiToInternalintegrationsMapping[location.integration];
|
||||
if (!integration) {
|
||||
throw new BadRequestException(`Invalid integration: ${location.integration}`);
|
||||
}
|
||||
return {
|
||||
value: integration,
|
||||
optionValue: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "address") {
|
||||
return {
|
||||
value: "inPerson",
|
||||
optionValue: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "attendeeAddress") {
|
||||
return {
|
||||
value: "attendeeInPerson",
|
||||
optionValue: location.address,
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "link") {
|
||||
return {
|
||||
value: "link",
|
||||
optionValue: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "phone") {
|
||||
return {
|
||||
value: "userPhone",
|
||||
optionValue: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "organizersDefaultApp") {
|
||||
return {
|
||||
value: "conferencing",
|
||||
optionValue: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "attendeePhone") {
|
||||
return {
|
||||
value: "phone",
|
||||
optionValue: location.phone,
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "attendeeDefined") {
|
||||
return {
|
||||
value: "somewhereElse",
|
||||
optionValue: location.location,
|
||||
};
|
||||
}
|
||||
|
||||
throw new BadRequestException(
|
||||
`Booking location with type ${(location as BookingInputLocation_2024_08_13).type} not valid.`
|
||||
);
|
||||
}
|
||||
|
||||
private isBookingLocationWithEventTypeLocations(
|
||||
inputBookingLocation: undefined | string | BookingInputLocation_2024_08_13,
|
||||
dbEventType: EventType
|
||||
) {
|
||||
if (!inputBookingLocation || typeof inputBookingLocation === "string") {
|
||||
// note(Lauris): for backwards compatibility because we had string locations before so let them pass.
|
||||
return true;
|
||||
}
|
||||
|
||||
const eventTypeLocations = this.outputEventTypesService.transformLocations(dbEventType.locations);
|
||||
const allowedLocationTypes = eventTypeLocations.map((location) => location.type);
|
||||
|
||||
const isAllowed = allowedLocationTypes.includes(inputBookingLocation.type);
|
||||
if (!isAllowed) {
|
||||
throw new BadRequestException(
|
||||
`Booking location with type ${inputBookingLocation.type} not valid for event type with id=${
|
||||
dbEventType.id
|
||||
}. The event type has following location types: ${allowedLocationTypes.join(
|
||||
", "
|
||||
)}, and only these types are allowed for booking location.`
|
||||
);
|
||||
}
|
||||
|
||||
if (inputBookingLocation.type === "integration" && "integration" in inputBookingLocation) {
|
||||
const allowedIntegrations = eventTypeLocations
|
||||
.filter((location) => location.type === "integration")
|
||||
.map((location) => location.integration);
|
||||
|
||||
const isAllowedIntegration = allowedIntegrations.includes(inputBookingLocation.integration);
|
||||
if (!isAllowedIntegration) {
|
||||
throw new BadRequestException(
|
||||
`Booking location with integration ${
|
||||
inputBookingLocation.integration
|
||||
} not valid for event type with id=${
|
||||
dbEventType.id
|
||||
}. The event type has following integrations: ${allowedIntegrations.join(
|
||||
", "
|
||||
)}, and only these integrations are allowed for booking location.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async transformInputCreateRecurringBooking(
|
||||
inputBooking: CreateRecurringBookingInput_2024_08_13,
|
||||
platformClientId?: string
|
||||
@@ -262,6 +409,10 @@ export class InputBookingsService_2024_08_13 {
|
||||
)
|
||||
: inputBooking.attendee.email;
|
||||
|
||||
const inputLocation = inputBooking.location || inputBooking.meetingUrl;
|
||||
this.isBookingLocationWithEventTypeLocations(inputLocation, eventType);
|
||||
const location = inputLocation ? this.transformLocation(inputLocation) : undefined;
|
||||
|
||||
for (let i = 0; i < repeatsTimes; i++) {
|
||||
const endTime = startTime.plus({ minutes: eventType.length });
|
||||
|
||||
@@ -280,7 +431,9 @@ export class InputBookingsService_2024_08_13 {
|
||||
...(inputBooking.bookingFieldsResponses || {}),
|
||||
name: inputBooking.attendee.name,
|
||||
email: attendeeEmail,
|
||||
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
|
||||
guests,
|
||||
location,
|
||||
},
|
||||
schedulingType: eventType.schedulingType,
|
||||
});
|
||||
@@ -417,8 +570,19 @@ export class InputBookingsService_2024_08_13 {
|
||||
throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`);
|
||||
}
|
||||
|
||||
const bookingResponses = bookingResponsesSchema.parse(booking.responses);
|
||||
const attendee = booking.attendees.find((attendee) => attendee.email === bookingResponses.email);
|
||||
const bookingResponses = safeParse(
|
||||
bookingResponsesSchema,
|
||||
booking.responses,
|
||||
defaultBookingResponses,
|
||||
false
|
||||
);
|
||||
const bookingResponsesMissing =
|
||||
bookingResponses.name === defaultBookingResponses.name &&
|
||||
bookingResponses.email === defaultBookingResponses.email;
|
||||
|
||||
const attendee = bookingResponsesMissing
|
||||
? booking.attendees[0]
|
||||
: booking.attendees.find((attendee) => attendee.email === bookingResponses.email);
|
||||
|
||||
if (!attendee) {
|
||||
throw new NotFoundException(
|
||||
@@ -426,6 +590,12 @@ export class InputBookingsService_2024_08_13 {
|
||||
);
|
||||
}
|
||||
|
||||
if (bookingResponsesMissing) {
|
||||
bookingResponses.name = attendee.name;
|
||||
bookingResponses.email = attendee.email;
|
||||
bookingResponses.attendeePhoneNumber = attendee.phoneNumber || undefined;
|
||||
}
|
||||
|
||||
const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone(attendee.timeZone);
|
||||
const endTime = startTime.plus({ minutes: eventType.length });
|
||||
|
||||
@@ -482,6 +652,8 @@ export class InputBookingsService_2024_08_13 {
|
||||
queryParams.eventTypeIds || (queryParams.eventTypeId ? [queryParams.eventTypeId] : undefined),
|
||||
afterUpdatedDate: queryParams.afterUpdatedAt,
|
||||
beforeUpdatedDate: queryParams.beforeUpdatedAt,
|
||||
afterCreatedDate: queryParams.afterCreatedAt,
|
||||
beforeCreatedDate: queryParams.beforeCreatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import { Booking, BookingSeat } from "@calcom/prisma/client";
|
||||
export const bookingResponsesSchema = z
|
||||
.object({
|
||||
email: z.string(),
|
||||
attendeePhoneNumber: z.string().optional(),
|
||||
name: z.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
@@ -45,6 +46,7 @@ export const seatedBookingDataSchema = z
|
||||
responses: z
|
||||
.object({
|
||||
email: z.string(),
|
||||
attendeePhoneNumber: z.string().optional(),
|
||||
name: z.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
|
||||
+50
-1
@@ -116,6 +116,7 @@ describe("Event types Endpoints", () => {
|
||||
slug: "email",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
};
|
||||
|
||||
const defaultResponseBookingFieldLocation = {
|
||||
@@ -336,6 +337,46 @@ describe("Event types Endpoints", () => {
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it("should not be able to create phone-only event type", async () => {
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Phone coding consultation",
|
||||
slug: "phone-coding-consultation",
|
||||
description: "Our team will review your codebase.",
|
||||
lengthInMinutes: 60,
|
||||
locations: [
|
||||
{
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
},
|
||||
],
|
||||
bookingFields: [
|
||||
{
|
||||
type: "email",
|
||||
required: false,
|
||||
label: "Email",
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
label: "Phone number",
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error.message).toBe(
|
||||
"checkIsEmailUserAccessible - Email booking field must be required and visible"
|
||||
);
|
||||
});
|
||||
|
||||
it("should create an event type", async () => {
|
||||
const nameBookingField: NameDefaultFieldInput_2024_06_14 = {
|
||||
type: "name",
|
||||
@@ -1191,7 +1232,14 @@ describe("Event types Endpoints", () => {
|
||||
|
||||
const expectedReturnSystemFields = [
|
||||
{ isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
|
||||
{
|
||||
isDefault: true,
|
||||
required: true,
|
||||
slug: "email",
|
||||
type: "email",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "radioInput",
|
||||
@@ -1644,6 +1692,7 @@ describe("Event types Endpoints", () => {
|
||||
slug: "email",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
|
||||
+11
-2
@@ -29,7 +29,12 @@ import {
|
||||
} from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
EVENT_TYPE_READ,
|
||||
EVENT_TYPE_WRITE,
|
||||
SUCCESS_STATUS,
|
||||
VERSION_2024_06_14,
|
||||
} from "@calcom/platform-constants";
|
||||
import {
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
GetEventTypesQuery_2024_06_14,
|
||||
@@ -45,8 +50,12 @@ import {
|
||||
@DocsTags("Event Types")
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to \`2024-06-14\``,
|
||||
description: `Must be set to ${VERSION_2024_06_14}`,
|
||||
example: VERSION_2024_06_14,
|
||||
required: true,
|
||||
schema: {
|
||||
default: VERSION_2024_06_14,
|
||||
},
|
||||
})
|
||||
export class EventTypesController_2024_06_14 {
|
||||
constructor(
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
updateEventType,
|
||||
getEventTypesPublic,
|
||||
EventTypesPublic,
|
||||
SystemField,
|
||||
CustomField,
|
||||
} from "@calcom/platform-libraries/event-types";
|
||||
import { GetEventTypesQuery_2024_06_14, InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
@@ -31,6 +33,9 @@ export class EventTypesService_2024_06_14 {
|
||||
) {}
|
||||
|
||||
async createUserEventType(user: UserWithProfile, body: InputEventTransformed_2024_06_14) {
|
||||
if (body.bookingFields) {
|
||||
this.checkHasUserAccessibleEmailBookingField(body.bookingFields);
|
||||
}
|
||||
await this.checkCanCreateEventType(user.id, body);
|
||||
const eventTypeUser = await this.getUserToCreateEvent(user);
|
||||
const { destinationCalendar: _destinationCalendar, ...rest } = body;
|
||||
@@ -78,6 +83,16 @@ export class EventTypesService_2024_06_14 {
|
||||
await this.checkUserOwnsSchedule(userId, body.scheduleId);
|
||||
}
|
||||
|
||||
checkHasUserAccessibleEmailBookingField(bookingFields: (SystemField | CustomField)[]) {
|
||||
const emailField = bookingFields.find((field) => field.type === "email" && field.name === "email");
|
||||
const isEmailFieldRequiredAndVisible = emailField?.required && !emailField?.hidden;
|
||||
if (!isEmailFieldRequiredAndVisible) {
|
||||
throw new BadRequestException(
|
||||
"checkIsEmailUserAccessible - Email booking field must be required and visible"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getEventTypeByUsernameAndSlug(
|
||||
username: string,
|
||||
eventTypeSlug: string,
|
||||
@@ -219,6 +234,9 @@ export class EventTypesService_2024_06_14 {
|
||||
body: Partial<InputEventTransformed_2024_06_14>,
|
||||
user: UserWithProfile
|
||||
) {
|
||||
if (body.bookingFields) {
|
||||
this.checkHasUserAccessibleEmailBookingField(body.bookingFields);
|
||||
}
|
||||
await this.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId);
|
||||
const eventTypeUser = await this.getUserToUpdateEvent(user);
|
||||
|
||||
|
||||
+25
-14
@@ -37,8 +37,6 @@ import {
|
||||
} from "@calcom/platform-types";
|
||||
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { OutputEventTypesService_2024_06_14 } from "./output-event-types.service";
|
||||
|
||||
interface ValidationContext {
|
||||
eventTypeId?: number;
|
||||
seatsPerTimeSlot?: number | null;
|
||||
@@ -51,7 +49,6 @@ interface ValidationContext {
|
||||
export class InputEventTypesService_2024_06_14 {
|
||||
constructor(
|
||||
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
||||
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
|
||||
private readonly calendarsService: CalendarsService
|
||||
) {}
|
||||
|
||||
@@ -128,12 +125,11 @@ export class InputEventTypesService_2024_06_14 {
|
||||
} = inputEventType;
|
||||
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
|
||||
|
||||
const hasMultipleLocations = (locations || defaultLocations).length > 1;
|
||||
const eventType = {
|
||||
...rest,
|
||||
length: lengthInMinutes,
|
||||
locations: this.transformInputLocations(locations || defaultLocations),
|
||||
bookingFields: this.transformInputBookingFields(bookingFields, hasMultipleLocations),
|
||||
bookingFields: this.transformInputBookingFields(bookingFields),
|
||||
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
|
||||
durationLimits: bookingLimitsDuration
|
||||
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
||||
@@ -182,15 +178,12 @@ export class InputEventTypesService_2024_06_14 {
|
||||
: {};
|
||||
|
||||
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
|
||||
const hasMultipleLocations = !!(locations && locations?.length > 1);
|
||||
|
||||
const eventType = {
|
||||
...rest,
|
||||
length: lengthInMinutes,
|
||||
locations: locations ? this.transformInputLocations(locations) : undefined,
|
||||
bookingFields: bookingFields
|
||||
? this.transformInputBookingFields(bookingFields, hasMultipleLocations)
|
||||
: undefined,
|
||||
bookingFields: bookingFields ? this.transformInputBookingFields(bookingFields) : undefined,
|
||||
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
|
||||
durationLimits: bookingLimitsDuration
|
||||
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
||||
@@ -220,10 +213,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
return transformLocationsApiToInternal(inputLocations);
|
||||
}
|
||||
|
||||
transformInputBookingFields(
|
||||
inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"],
|
||||
hasMultipleLocations: boolean
|
||||
) {
|
||||
transformInputBookingFields(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]) {
|
||||
const internalFields: (SystemField | CustomField)[] = inputBookingFields
|
||||
? transformBookingFieldsApiToInternal(inputBookingFields)
|
||||
: [];
|
||||
@@ -252,7 +242,28 @@ export class InputEventTypesService_2024_06_14 {
|
||||
systemCustomRescheduleReasonField || systemAfterFieldRescheduleReason,
|
||||
];
|
||||
|
||||
return [...defaultFieldsBefore, ...userCustomFields, ...defaultFieldsAfter];
|
||||
const bookingFields = [...defaultFieldsBefore, ...userCustomFields, ...defaultFieldsAfter];
|
||||
|
||||
if (!this.hasEmailOrPhoneOnlySetup(bookingFields)) {
|
||||
throw new BadRequestException(
|
||||
"Booking fields validation failed: visible and required email or visible and required attendee phone field is needed."
|
||||
);
|
||||
}
|
||||
|
||||
return bookingFields;
|
||||
}
|
||||
|
||||
hasEmailOrPhoneOnlySetup(bookingFields: (SystemField | CustomField)[]) {
|
||||
const emailField = bookingFields.find((field) => field.type === "email" && field.name === "email");
|
||||
const attendeePhoneNumberField = bookingFields.find(
|
||||
(field) => field.type === "phone" && field.name === "attendeePhoneNumber"
|
||||
);
|
||||
|
||||
const isEmailFieldRequiredAndVisible = emailField?.required && !emailField?.hidden;
|
||||
const isAttendeePhoneNumberFieldRequiredAndVisible =
|
||||
attendeePhoneNumberField?.required && !attendeePhoneNumberField?.hidden;
|
||||
|
||||
return isEmailFieldRequiredAndVisible || isAttendeePhoneNumberFieldRequiredAndVisible;
|
||||
}
|
||||
|
||||
isUserCustomField(field: SystemField | CustomField): field is CustomField {
|
||||
|
||||
+5
-1
@@ -39,8 +39,12 @@ import {
|
||||
@DocsTags("Schedules")
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to \`2024-06-11\``,
|
||||
description: `Must be set to ${VERSION_2024_06_11}`,
|
||||
example: VERSION_2024_06_11,
|
||||
required: true,
|
||||
schema: {
|
||||
default: VERSION_2024_06_11,
|
||||
},
|
||||
})
|
||||
@ApiHeader({
|
||||
name: "Authorization",
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
CalendarAppError,
|
||||
CalendarAppDelegationCredentialInvalidGrantError,
|
||||
CalendarAppDelegationCredentialError,
|
||||
CalendarAppDelegationCredentialConfigurationError,
|
||||
CalendarAppDelegationCredentialClientIdNotAuthorizedError,
|
||||
CalendarAppDelegationCredentialNotSetupError,
|
||||
} from "@calcom/platform-libraries/app-store";
|
||||
import { Response } from "@calcom/platform-types";
|
||||
|
||||
type CalendarError =
|
||||
| CalendarAppError
|
||||
| CalendarAppDelegationCredentialInvalidGrantError
|
||||
| CalendarAppDelegationCredentialError
|
||||
| CalendarAppDelegationCredentialConfigurationError
|
||||
| CalendarAppDelegationCredentialNotSetupError
|
||||
| CalendarAppDelegationCredentialClientIdNotAuthorizedError;
|
||||
@Catch(
|
||||
CalendarAppError,
|
||||
CalendarAppDelegationCredentialInvalidGrantError,
|
||||
CalendarAppDelegationCredentialError,
|
||||
CalendarAppDelegationCredentialConfigurationError,
|
||||
CalendarAppDelegationCredentialClientIdNotAuthorizedError,
|
||||
CalendarAppDelegationCredentialNotSetupError
|
||||
)
|
||||
export class CalendarServiceExceptionFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger("CalendarServiceExceptionFilter");
|
||||
|
||||
catch(exception: CalendarError, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
const requestId = request.headers["X-Request-Id"];
|
||||
|
||||
this.logger.error(`Calendar Service Exception Filter: ${exception?.message}`, {
|
||||
exception,
|
||||
body: request.body,
|
||||
headers: filterReqHeaders(request.headers),
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
requestId,
|
||||
});
|
||||
|
||||
response.status(400).json({
|
||||
status: ERROR_STATUS,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
error: { code: exception.name, message: exception.message },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,20 @@ import { IncomingHttpHeaders } from "http";
|
||||
|
||||
export function filterReqHeaders(headers: IncomingHttpHeaders): Partial<IncomingHttpHeaders> {
|
||||
return {
|
||||
"Content-Type": headers["content-type"],
|
||||
"User-Agent": headers["user-agent"],
|
||||
"Content-Type": headers["Content-Type"] ?? headers["content-type"],
|
||||
Authorization: headers["Authorization"] ?? headers["authorization"],
|
||||
"X-Cal-Client-Id": headers["X-Cal-Client-Id"] ?? headers["x-cal-client-id"],
|
||||
"Cal-Api-Version": headers["Cal-Api-Version"] ?? headers["cal-api-version"],
|
||||
"X-Request-Id": headers["X-Request-Id"] ?? headers["x-request-id"],
|
||||
// IP-related headers
|
||||
"User-Agent": headers["User-agent"] ?? headers["user-agent"],
|
||||
"X-Forwarded-For": headers["x-forwarded-for"],
|
||||
"X-Forwarded-Host": headers["x-forwarded-host"],
|
||||
"CF-Connecting-IP": headers["cf-connecting-ip"],
|
||||
"CloudFront-Viewer-Address": headers["cloudfront-viewer-address"],
|
||||
// Vercel headers
|
||||
"x-vercel-id": headers["x-vercel-id"],
|
||||
"x-vercel-deployment-url": headers["x-vercel-deployment-url"],
|
||||
"x-vercel-country": headers["x-vercel-country"],
|
||||
"x-vercel-region": headers["x-vercel-region"],
|
||||
Host: headers["host"],
|
||||
"X-Vercel-Id": headers["x-vercel-id"],
|
||||
"X-Vercel-Deployment-Url": headers["x-vercel-deployment-url"],
|
||||
"X-Vercel-Country": headers["x-vercel-country"],
|
||||
"X-Vercel-Region": headers["x-vercel-region"],
|
||||
Host: headers["Host"] ?? headers["host"],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ export const defaultBookingResponses = {
|
||||
name: "unknown",
|
||||
email: "unknown",
|
||||
guests: [],
|
||||
rescheduleReason: "unknown",
|
||||
rescheduledReason: "unknown",
|
||||
};
|
||||
|
||||
export const defaultBookingMetadata = { videoCallUrl: "unknown" };
|
||||
|
||||
@@ -21,13 +21,23 @@ export class ResponseInterceptor implements NestInterceptor {
|
||||
const { statusCode } = response;
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
let jsonBodyString = "{}";
|
||||
|
||||
try {
|
||||
if (data && typeof data === "object") {
|
||||
jsonBodyString = JSON.stringify(data);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error("Could not parse request body");
|
||||
}
|
||||
|
||||
this.logger.log("Outgoing Response", {
|
||||
requestId,
|
||||
method,
|
||||
url,
|
||||
statusCode,
|
||||
responseTime,
|
||||
responseBody: data,
|
||||
responseBody: jsonBodyString,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
})
|
||||
|
||||
@@ -11,13 +11,22 @@ export class RequestIdMiddleware implements NestMiddleware {
|
||||
const requestId = uuid();
|
||||
req.headers["X-Request-Id"] = requestId;
|
||||
const { method, headers, body: requestBody, baseUrl } = req;
|
||||
let jsonBodyString = "{}";
|
||||
|
||||
try {
|
||||
if (requestBody && typeof requestBody === "object") {
|
||||
jsonBodyString = JSON.stringify(requestBody);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error("Could not parse request body");
|
||||
}
|
||||
|
||||
this.logger.log("Incoming Request", {
|
||||
requestId,
|
||||
method,
|
||||
url: baseUrl,
|
||||
headers: filterReqHeaders(headers),
|
||||
requestBody,
|
||||
requestBody: jsonBodyString,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
|
||||
@@ -35,12 +35,13 @@ import {
|
||||
Redirect,
|
||||
UnauthorizedException,
|
||||
Req,
|
||||
HttpException,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import { Request } from "express";
|
||||
|
||||
import { GOOGLE_MEET, ZOOM, SUCCESS_STATUS, OFFICE_365_VIDEO } from "@calcom/platform-constants";
|
||||
import { GOOGLE_MEET, ZOOM, SUCCESS_STATUS, OFFICE_365_VIDEO, CAL_VIDEO } from "@calcom/platform-constants";
|
||||
|
||||
export type OAuthCallbackState = {
|
||||
accessToken: string;
|
||||
@@ -56,7 +57,7 @@ export type OAuthCallbackState = {
|
||||
})
|
||||
@DocsTags("Conferencing")
|
||||
export class ConferencingController {
|
||||
private readonly logger = new Logger("Platform Gcal Provider");
|
||||
private readonly logger = new Logger("ConferencingController");
|
||||
|
||||
constructor(
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
@@ -70,6 +71,12 @@ export class ConferencingController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiOperation({ summary: "Connect your conferencing application" })
|
||||
@ApiParam({
|
||||
name: "app",
|
||||
description: "Conferencing application type",
|
||||
enum: [GOOGLE_MEET],
|
||||
required: true,
|
||||
})
|
||||
async connect(
|
||||
@GetUser("id") userId: number,
|
||||
@Param("app") app: string
|
||||
@@ -92,6 +99,12 @@ export class ConferencingController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiOperation({ summary: "Get OAuth conferencing app auth url" })
|
||||
@ApiParam({
|
||||
name: "app",
|
||||
description: "Conferencing application type",
|
||||
enum: [ZOOM, OFFICE_365_VIDEO],
|
||||
required: true,
|
||||
})
|
||||
async redirect(
|
||||
@Req() req: Request,
|
||||
@Headers("Authorization") authorization: string,
|
||||
@@ -137,6 +150,12 @@ export class ConferencingController {
|
||||
@UseGuards()
|
||||
@Redirect(undefined, 301)
|
||||
@ApiOperation({ summary: "conferencing apps oauths callback" })
|
||||
@ApiParam({
|
||||
name: "app",
|
||||
description: "Conferencing application type",
|
||||
enum: [ZOOM, OFFICE_365_VIDEO],
|
||||
required: true,
|
||||
})
|
||||
async save(
|
||||
@Query("state") state: string,
|
||||
@Param("app") app: string,
|
||||
@@ -144,6 +163,10 @@ export class ConferencingController {
|
||||
@Query("error") error: string | undefined,
|
||||
@Query("error_description") error_description: string | undefined
|
||||
): Promise<{ url: string }> {
|
||||
if (!state) {
|
||||
throw new BadRequestException("Missing `state` query param");
|
||||
}
|
||||
|
||||
const decodedCallbackState: OAuthCallbackState = JSON.parse(state);
|
||||
try {
|
||||
const userId = await this.tokensRepository.getAccessTokenOwnerId(decodedCallbackState.accessToken);
|
||||
@@ -169,6 +192,9 @@ export class ConferencingController {
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException || error instanceof Error) {
|
||||
this.logger.error(error.message);
|
||||
}
|
||||
return {
|
||||
url: decodedCallbackState.onErrorReturnTo ?? "",
|
||||
};
|
||||
@@ -195,6 +221,12 @@ export class ConferencingController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiOperation({ summary: "Set your default conferencing application" })
|
||||
@ApiParam({
|
||||
name: "app",
|
||||
description: "Conferencing application type",
|
||||
enum: [GOOGLE_MEET, ZOOM, OFFICE_365_VIDEO, CAL_VIDEO],
|
||||
required: true,
|
||||
})
|
||||
async default(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Param("app") app: string
|
||||
@@ -216,6 +248,12 @@ export class ConferencingController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiOperation({ summary: "Disconnect your conferencing application" })
|
||||
@ApiParam({
|
||||
name: "app",
|
||||
description: "Conferencing application type",
|
||||
enum: [GOOGLE_MEET, ZOOM, OFFICE_365_VIDEO],
|
||||
required: true,
|
||||
})
|
||||
async disconnect(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Param("app") app: string
|
||||
|
||||
+7
@@ -26,6 +26,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
|
||||
import { randomString } from "test/utils/randomString";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { slugify } from "@calcom/platform-libraries";
|
||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
const CLIENT_REDIRECT_URI = "http://localhost:4321";
|
||||
@@ -228,6 +229,9 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
expect(responseBody.data.user.timeFormat).toEqual(requestBody.timeFormat);
|
||||
expect(responseBody.data.user.locale).toEqual(requestBody.locale);
|
||||
expect(responseBody.data.user.avatarUrl).toEqual(requestBody.avatarUrl);
|
||||
const [emailUser, emailDomain] = responseBody.data.user.email.split("@");
|
||||
const [domainName, TLD] = emailDomain.split(".");
|
||||
expect(responseBody.data.user.username).toEqual(slugify(`${emailUser}-${domainName}-${TLD}`));
|
||||
expect(responseBody.data.accessToken).toBeDefined();
|
||||
expect(responseBody.data.refreshToken).toBeDefined();
|
||||
|
||||
@@ -546,6 +550,9 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
expect(responseBody.data.email).toEqual(
|
||||
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, userUpdatedEmail)
|
||||
);
|
||||
const [emailUser, emailDomain] = responseBody.data.email.split("@");
|
||||
const [domainName, TLD] = emailDomain.split(".");
|
||||
expect(responseBody.data.username).toEqual(slugify(`${emailUser}-${domainName}-${TLD}`));
|
||||
expect(responseBody.data.locale).toEqual(Locales.PT_BR);
|
||||
});
|
||||
|
||||
|
||||
+1
-14
@@ -29,19 +29,12 @@ import {
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
ApiOperation as DocsOperation,
|
||||
ApiCreatedResponse as DocsCreatedResponse,
|
||||
ApiTags,
|
||||
} from "@nestjs/swagger";
|
||||
import { ApiCreatedResponse as DocsCreatedResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { User, MembershipRole } from "@prisma/client";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { CreateOAuthClientInput, UpdateOAuthClientInput, Pagination } from "@calcom/platform-types";
|
||||
|
||||
const AUTH_DOCUMENTATION = `⚠️ First, this endpoint requires \`Cookie: next-auth.session-token=eyJhbGciOiJ\` header. Log into Cal web app using owner of organization that was created after visiting \`/settings/organizations/new\`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.
|
||||
Second, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.`;
|
||||
|
||||
@Controller({
|
||||
path: "/v2/oauth-clients",
|
||||
version: API_VERSIONS_VALUES,
|
||||
@@ -61,7 +54,6 @@ export class OAuthClientsController {
|
||||
@Post("/")
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
@DocsCreatedResponse({
|
||||
description: "Create an OAuth client",
|
||||
type: CreateOAuthClientResponseDto,
|
||||
@@ -90,7 +82,6 @@ export class OAuthClientsController {
|
||||
@Get("/")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
async getOAuthClients(@GetOrgId() organizationId: number): Promise<GetOAuthClientsResponseDto> {
|
||||
const clients = await this.oAuthClientsService.getOAuthClients(organizationId);
|
||||
return { status: SUCCESS_STATUS, data: clients };
|
||||
@@ -99,7 +90,6 @@ export class OAuthClientsController {
|
||||
@Get("/:clientId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
@UseGuards(OAuthClientGuard)
|
||||
async getOAuthClientById(@Param("clientId") clientId: string): Promise<GetOAuthClientResponseDto> {
|
||||
const client = await this.oAuthClientsService.getOAuthClientById(clientId);
|
||||
@@ -109,7 +99,6 @@ export class OAuthClientsController {
|
||||
@Get("/:clientId/managed-users")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
@UseGuards(OAuthClientGuard)
|
||||
async getOAuthClientManagedUsersById(
|
||||
@Param("clientId") clientId: string,
|
||||
@@ -128,7 +117,6 @@ export class OAuthClientsController {
|
||||
@Patch("/:clientId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
@UseGuards(OAuthClientGuard)
|
||||
async updateOAuthClient(
|
||||
@Param("clientId") clientId: string,
|
||||
@@ -142,7 +130,6 @@ export class OAuthClientsController {
|
||||
@Delete("/:clientId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
@UseGuards(OAuthClientGuard)
|
||||
async deleteOAuthClient(@Param("clientId") clientId: string): Promise<GetOAuthClientResponseDto> {
|
||||
this.logger.log(`Deleting OAuth Client with ID: ${clientId}`);
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { AuthModule } from "@/modules/auth/auth.module";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { OAuthClientUsersController } from "@/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller";
|
||||
import { OAuthClientsController } from "@/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller";
|
||||
@@ -16,6 +20,7 @@ import { OrganizationsModule } from "@/modules/organizations/organizations.modul
|
||||
import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
@@ -42,6 +47,11 @@ import { JwtService } from "@nestjs/jwt";
|
||||
OAuthClientRepository,
|
||||
TokensRepository,
|
||||
OAuthFlowService,
|
||||
CalendarsService,
|
||||
CredentialsRepository,
|
||||
AppsRepository,
|
||||
CalendarsRepository,
|
||||
SelectedCalendarsRepository,
|
||||
OAuthClientUsersService,
|
||||
OrganizationsTeamsService,
|
||||
OAuthClientsService,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { GetManagedUsersInput } from "@/modules/oauth-clients/controllers/oauth-client-users/inputs/get-managed-users.input";
|
||||
@@ -5,18 +6,21 @@ import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, ConflictException, Injectable } from "@nestjs/common";
|
||||
import { BadRequestException, ConflictException, Injectable, Logger } from "@nestjs/common";
|
||||
import { User, CreationSource, PlatformOAuthClient } from "@prisma/client";
|
||||
|
||||
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthClientUsersService {
|
||||
private readonly logger = new Logger("OAuthClientUsersService");
|
||||
|
||||
constructor(
|
||||
private readonly userRepository: UsersRepository,
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
private readonly eventTypesService: EventTypesService_2024_04_15,
|
||||
private readonly schedulesService: SchedulesService_2024_04_15
|
||||
private readonly schedulesService: SchedulesService_2024_04_15,
|
||||
private readonly calendarsService: CalendarsService
|
||||
) {}
|
||||
|
||||
async createOAuthClientUser(oAuthClient: PlatformOAuthClient, body: CreateManagedUserInput) {
|
||||
@@ -87,6 +91,13 @@ export class OAuthClientUsersService {
|
||||
user.defaultScheduleId = defaultSchedule.id;
|
||||
}
|
||||
|
||||
try {
|
||||
this.logger.log(`Setting default calendars in db for user with id ${user.id}`);
|
||||
await this.calendarsService.getCalendars(user.id);
|
||||
} catch (err) {
|
||||
this.logger.error(`Could not get calendars of new managed user with id ${user.id}`);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
tokens: {
|
||||
@@ -123,7 +134,9 @@ export class OAuthClientUsersService {
|
||||
if (body.email) {
|
||||
const emailWithOAuthId = OAuthClientUsersService.getOAuthUserEmail(oAuthClientId, body.email);
|
||||
body.email = emailWithOAuthId;
|
||||
const newUsername = slugify(emailWithOAuthId);
|
||||
const [emailUser, emailDomain] = emailWithOAuthId.split("@");
|
||||
const [domainName, TLD] = emailDomain.split(".");
|
||||
const newUsername = slugify(`${emailUser}-${domainName}-${TLD}`);
|
||||
await this.userRepository.updateUsername(userId, newUsername);
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -30,5 +30,7 @@ export class CreateDelegationCredentialInput {
|
||||
})
|
||||
@Expose()
|
||||
@Type(() => Object)
|
||||
serviceAccountKey!: GoogleServiceAccountKeyInput | MicrosoftServiceAccountKeyInput;
|
||||
serviceAccountKey!: (GoogleServiceAccountKeyInput | MicrosoftServiceAccountKeyInput) & {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
+10
-8
@@ -38,6 +38,15 @@ export class OrganizationsDelegationCredentialService {
|
||||
delegatedServiceAccountUser: User,
|
||||
body: UpdateDelegationCredentialInput
|
||||
) {
|
||||
const delegationCredential =
|
||||
await this.organizationsDelegationCredentialRepository.findByIdWithWorkspacePlatform(
|
||||
delegationCredentialId
|
||||
);
|
||||
|
||||
if (!delegationCredential) {
|
||||
throw new NotFoundException(`DelegationCredential with id ${delegationCredentialId} not found`);
|
||||
}
|
||||
|
||||
if (body.enabled !== undefined) {
|
||||
await this.updateDelegationCredentialEnabled(
|
||||
orgId,
|
||||
@@ -49,15 +58,8 @@ export class OrganizationsDelegationCredentialService {
|
||||
if (body.serviceAccountKey !== undefined) {
|
||||
await this.updateDelegationCredentialServiceAccountKey(delegationCredentialId, body.serviceAccountKey);
|
||||
}
|
||||
const delegationCredential =
|
||||
await this.organizationsDelegationCredentialRepository.findByIdWithWorkspacePlatform(
|
||||
delegationCredentialId
|
||||
);
|
||||
if (!delegationCredential) {
|
||||
throw new NotFoundException(`DelegationCredential with id ${delegationCredentialId} not found`);
|
||||
}
|
||||
|
||||
return delegationCredential;
|
||||
return { ...delegationCredential, enabled: body?.enabled ?? delegationCredential?.enabled };
|
||||
}
|
||||
|
||||
async updateDelegationCredentialEnabled(
|
||||
|
||||
+2
-2
@@ -81,7 +81,7 @@ export class OrganizationsEventTypesController {
|
||||
bodyEventType
|
||||
);
|
||||
|
||||
const eventType = await this.organizationsEventTypesService.createTeamEventType(
|
||||
const eventType = await this.organizationsEventTypesService.createOrganizationTeamEventType(
|
||||
user,
|
||||
teamId,
|
||||
orgId,
|
||||
@@ -212,7 +212,7 @@ export class OrganizationsEventTypesController {
|
||||
bodyEventType
|
||||
);
|
||||
|
||||
const eventType = await this.organizationsEventTypesService.updateTeamEventType(
|
||||
const eventType = await this.organizationsEventTypesService.updateOrganizationTeamEventType(
|
||||
eventTypeId,
|
||||
teamId,
|
||||
transformedBody,
|
||||
|
||||
+129
-3
@@ -581,6 +581,123 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should be able to create phone-only event type", async () => {
|
||||
const body: CreateTeamEventTypeInput_2024_06_14 = {
|
||||
title: "Phone coding consultation",
|
||||
slug: "phone-coding-consultation",
|
||||
description: "Our team will review your codebase.",
|
||||
lengthInMinutes: 60,
|
||||
locations: [
|
||||
{
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
},
|
||||
{
|
||||
type: "organizersDefaultApp",
|
||||
},
|
||||
],
|
||||
schedulingType: "COLLECTIVE",
|
||||
hosts: [
|
||||
{
|
||||
userId: teammate1.id,
|
||||
mandatory: true,
|
||||
priority: "high",
|
||||
},
|
||||
],
|
||||
bookingFields: [
|
||||
{
|
||||
type: "email",
|
||||
required: false,
|
||||
label: "Email",
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
label: "Phone number",
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const data = responseBody.data;
|
||||
expect(data.bookingFields).toEqual([
|
||||
{
|
||||
isDefault: true,
|
||||
type: "name",
|
||||
slug: "name",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "email",
|
||||
slug: "email",
|
||||
required: false,
|
||||
label: "Email",
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "radioInput",
|
||||
slug: "location",
|
||||
required: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
hidden: false,
|
||||
label: "Phone number",
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "text",
|
||||
slug: "title",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "notes",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "multiemail",
|
||||
slug: "guests",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "rescheduleReason",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should be able to configure phone-only event type", async () => {
|
||||
const body: UpdateTeamEventTypeInput_2024_06_14 = {
|
||||
bookingFields: [
|
||||
@@ -588,13 +705,14 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
type: "email",
|
||||
required: false,
|
||||
label: "Email",
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
label: "Phone number",
|
||||
hidden: true,
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -622,6 +740,7 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
required: false,
|
||||
label: "Email",
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
@@ -635,7 +754,7 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
hidden: true,
|
||||
hidden: false,
|
||||
label: "Phone number",
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
@@ -776,7 +895,14 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
|
||||
expect(fetchedEventType.bookingFields).toEqual([
|
||||
{ isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
|
||||
{
|
||||
isDefault: true,
|
||||
required: true,
|
||||
slug: "email",
|
||||
type: "email",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
disableOnPrefill: false,
|
||||
isDefault: true,
|
||||
|
||||
+4
-4
@@ -26,7 +26,7 @@ export class OrganizationsEventTypesService {
|
||||
private readonly usersService: UsersService
|
||||
) {}
|
||||
|
||||
async createTeamEventType(
|
||||
async createOrganizationTeamEventType(
|
||||
user: UserWithProfile,
|
||||
teamId: number,
|
||||
orgId: number,
|
||||
@@ -48,7 +48,7 @@ export class OrganizationsEventTypesService {
|
||||
},
|
||||
});
|
||||
|
||||
return this.teamsEventTypesService.updateTeamEventType(eventTypeCreated.id, teamId, body, user);
|
||||
return this.teamsEventTypesService.updateTeamEventType(eventTypeCreated.id, teamId, body, user, true);
|
||||
}
|
||||
|
||||
async getUserToCreateTeamEvent(user: UserWithProfile, organizationId: number) {
|
||||
@@ -91,13 +91,13 @@ export class OrganizationsEventTypesService {
|
||||
return await this.organizationEventTypesRepository.getOrganizationTeamsEventTypes(orgId, skip, take);
|
||||
}
|
||||
|
||||
async updateTeamEventType(
|
||||
async updateOrganizationTeamEventType(
|
||||
eventTypeId: number,
|
||||
teamId: number,
|
||||
body: TransformedUpdateTeamEventTypeInput,
|
||||
user: UserWithProfile
|
||||
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
|
||||
return this.teamsEventTypesService.updateTeamEventType(eventTypeId, teamId, body, user);
|
||||
return this.teamsEventTypesService.updateTeamEventType(eventTypeId, teamId, body, user, true);
|
||||
}
|
||||
|
||||
async deleteTeamEventType(teamId: number, eventTypeId: number) {
|
||||
|
||||
+4
-3
@@ -37,11 +37,12 @@ export class OrganizationsMembershipRepository {
|
||||
}
|
||||
|
||||
async createOrgMembership(organizationId: number, data: CreateOrgMembershipDto) {
|
||||
return this.dbWrite.prisma.membership.create({
|
||||
data: { ...data, teamId: organizationId },
|
||||
return this.dbWrite.prisma.membership.upsert({
|
||||
create: { ...data, teamId: organizationId },
|
||||
update: { role: data.role, accepted: data.accepted, disableImpersonation: data.disableImpersonation },
|
||||
where: { userId_teamId: { userId: data.userId, teamId: organizationId } },
|
||||
});
|
||||
}
|
||||
|
||||
async updateOrgMembership(organizationId: number, membershipId: number, data: UpdateOrgMembershipDto) {
|
||||
return this.dbWrite.prisma.membership.update({
|
||||
data: { ...data },
|
||||
|
||||
+98
@@ -75,6 +75,9 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
const seatedEventTypeSlug = `slots-2024-09-04-seated-event-type-${randomString()}`;
|
||||
let seatedEventType: EventType;
|
||||
|
||||
const variableLengthEventTypeSlug = `slots-2024-09-04-variable-length-event-type-${randomString()}`;
|
||||
let variableLengthEventType: EventType;
|
||||
|
||||
let reservedSlot: ReserveSlotOutputData_2024_09_04;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -168,6 +171,17 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
);
|
||||
seatedEventType = seatedEvent;
|
||||
|
||||
const variableLengthEvent = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: "frisbee match",
|
||||
slug: `slots-2024-09-04-variable-length-event-type-${randomString()}`,
|
||||
length: 15,
|
||||
metadata: { multipleDuration: [15, 30, 45, 60] },
|
||||
},
|
||||
user.id
|
||||
);
|
||||
variableLengthEventType = variableLengthEvent;
|
||||
|
||||
team = await teamRepositoryFixture.create({
|
||||
name: `slots-2024-09-04-team-${randomString()}`,
|
||||
isOrganization: false,
|
||||
@@ -1246,6 +1260,90 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
await bookingsRepositoryFixture.deleteById(booking.id);
|
||||
});
|
||||
|
||||
describe("variable length", () => {
|
||||
it("should not be able to reserve a slot for variable length event type with invalid duration", async () => {
|
||||
const slotStartTime = "2050-09-05T10:00:00.000Z";
|
||||
const reserveResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/slots/reservations`)
|
||||
.send({
|
||||
eventTypeId: variableLengthEventType.id,
|
||||
slotStart: slotStartTime,
|
||||
slotDuration: 1000,
|
||||
})
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||
.expect(400);
|
||||
|
||||
expect(reserveResponse.body.error.message).toEqual(
|
||||
"Provided 'slotDuration' is not one of the possible lengths for the event type. The possible lengths for this variable length event type are: 15, 30, 45, 60"
|
||||
);
|
||||
});
|
||||
|
||||
it("should reserve a slot with slot duration for variable event type length", async () => {
|
||||
// note(Lauris): mock current date to test slots release time
|
||||
const now = "2049-09-05T12:00:00.000Z";
|
||||
const newDate = DateTime.fromISO(now, { zone: "UTC" }).toJSDate();
|
||||
advanceTo(newDate);
|
||||
|
||||
const slotDuration = 60;
|
||||
const slotStartTime = "2050-09-05T10:00:00.000Z";
|
||||
const reserveResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/slots/reservations`)
|
||||
.send({
|
||||
eventTypeId: variableLengthEventType.id,
|
||||
slotStart: slotStartTime,
|
||||
slotDuration,
|
||||
})
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||
.expect(201);
|
||||
|
||||
const reserveResponseBody: ReserveSlotOutputResponse_2024_09_04 = reserveResponse.body;
|
||||
expect(reserveResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const responseReservedSlot: ReserveSlotOutputData_2024_09_04 = reserveResponseBody.data;
|
||||
expect(responseReservedSlot.reservationUid).toBeDefined();
|
||||
expect(responseReservedSlot.eventTypeId).toEqual(variableLengthEventType.id);
|
||||
expect(responseReservedSlot.slotStart).toEqual(slotStartTime);
|
||||
expect(responseReservedSlot.slotDuration).toEqual(slotDuration);
|
||||
expect(responseReservedSlot.slotEnd).toEqual(
|
||||
DateTime.fromISO(slotStartTime, { zone: "UTC" }).plus({ minutes: slotDuration }).toISO()
|
||||
);
|
||||
expect(responseReservedSlot.reservationDuration).toEqual(5);
|
||||
|
||||
if (!responseReservedSlot.reservationUid) {
|
||||
throw new Error("Reserved slot uid is undefined");
|
||||
}
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(
|
||||
`/v2/slots?eventTypeId=${variableLengthEventType.id}&start=2050-09-05&end=2050-09-09&duration=60`
|
||||
)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: GetSlotsOutput_2024_09_04 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const slots = responseBody.data;
|
||||
|
||||
expect(slots).toBeDefined();
|
||||
const days = Object.keys(slots);
|
||||
expect(days.length).toEqual(5);
|
||||
|
||||
const expectedSlotsUTC2050_09_05 = expectedSlotsUTC["2050-09-05"].filter(
|
||||
(slot) => slot.start !== slotStartTime
|
||||
);
|
||||
expect(slots).toEqual({ ...expectedSlotsUTC, "2050-09-05": expectedSlotsUTC2050_09_05 });
|
||||
|
||||
const dbSlot = await selectedSlotsRepositoryFixture.getByUid(reservedSlot.reservationUid);
|
||||
expect(dbSlot).toBeDefined();
|
||||
if (dbSlot) {
|
||||
const dbReleaseAt = DateTime.fromJSDate(dbSlot.releaseAt, { zone: "UTC" }).toISO();
|
||||
const expectedReleaseAt = DateTime.fromISO(now, { zone: "UTC" }).plus({ minutes: 5 }).toISO();
|
||||
expect(dbReleaseAt).toEqual(expectedReleaseAt);
|
||||
expect(responseReservedSlot.reservationUntil).toEqual(expectedReleaseAt);
|
||||
}
|
||||
clear();
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await userRepositoryFixture.deleteByEmail(unrelatedUser.email);
|
||||
|
||||
@@ -47,8 +47,12 @@ import { ApiResponse } from "@calcom/platform-types";
|
||||
@DocsTags("Slots")
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to \`2024-09-04\``,
|
||||
description: `Must be set to ${VERSION_2024_09_04}`,
|
||||
example: VERSION_2024_09_04,
|
||||
required: true,
|
||||
schema: {
|
||||
default: VERSION_2024_09_04,
|
||||
},
|
||||
})
|
||||
export class SlotsController_2024_09_04 {
|
||||
constructor(private readonly slotsService: SlotsService_2024_09_04) {}
|
||||
|
||||
@@ -21,9 +21,11 @@ import { getAvailableSlots } from "@calcom/platform-libraries/slots";
|
||||
import { GetSlotsInput_2024_09_04, ReserveSlotInput_2024_09_04 } from "@calcom/platform-types";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
|
||||
const eventTypeMetadataSchema = z.object({
|
||||
multipleDuration: z.number().array().optional(),
|
||||
});
|
||||
const eventTypeMetadataSchema = z
|
||||
.object({
|
||||
multipleDuration: z.number().array().optional(),
|
||||
})
|
||||
.nullable();
|
||||
|
||||
const DEFAULT_RESERVATION_DURATION = 5;
|
||||
|
||||
@@ -89,17 +91,8 @@ export class SlotsService_2024_09_04 {
|
||||
throw new BadRequestException("Invalid start date");
|
||||
}
|
||||
|
||||
const metadata = eventTypeMetadataSchema.parse(eventType);
|
||||
if (
|
||||
input.slotDuration &&
|
||||
metadata.multipleDuration &&
|
||||
!metadata.multipleDuration.includes(input.slotDuration)
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
`Provided 'slotDuration' is not one of the possible lengths for the event type. The possible lengths for this variable length event type are: ${metadata.multipleDuration.join(
|
||||
", "
|
||||
)}`
|
||||
);
|
||||
if (input.slotDuration) {
|
||||
this.validateSlotDuration(eventType, input.slotDuration);
|
||||
}
|
||||
|
||||
const endDate = startDate.plus({ minutes: input.slotDuration ?? eventType.length });
|
||||
@@ -160,6 +153,23 @@ export class SlotsService_2024_09_04 {
|
||||
return this.slotsOutputService.getReservationSlotCreated(slot, reservationDuration);
|
||||
}
|
||||
|
||||
validateSlotDuration(eventType: EventType, inputSlotDuration: number) {
|
||||
const eventTypeMetadata = eventTypeMetadataSchema.parse(eventType.metadata);
|
||||
if (!eventTypeMetadata?.multipleDuration) {
|
||||
throw new BadRequestException(
|
||||
"You passed 'slotDuration' but this event type is not a variable length event type."
|
||||
);
|
||||
}
|
||||
|
||||
if (!eventTypeMetadata.multipleDuration.includes(inputSlotDuration)) {
|
||||
throw new BadRequestException(
|
||||
`Provided 'slotDuration' is not one of the possible lengths for the event type. The possible lengths for this variable length event type are: ${eventTypeMetadata.multipleDuration.join(
|
||||
", "
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async canSpecifyCustomReservationDuration(authUserId: number, eventType: EventType) {
|
||||
if (eventType.userId) {
|
||||
return await this.canSpecifyCustomReservationDurationIndividualEvent(authUserId, eventType.userId);
|
||||
@@ -214,17 +224,8 @@ export class SlotsService_2024_09_04 {
|
||||
throw new BadRequestException("Invalid start date");
|
||||
}
|
||||
|
||||
const metadata = eventTypeMetadataSchema.parse(eventType);
|
||||
if (
|
||||
input.slotDuration &&
|
||||
metadata.multipleDuration &&
|
||||
!metadata.multipleDuration.includes(input.slotDuration)
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
`Provided 'slotDuration' is not one of the possible lengths for the event type. The possible lengths for this variable length event type are: ${metadata.multipleDuration.join(
|
||||
", "
|
||||
)}`
|
||||
);
|
||||
if (input.slotDuration) {
|
||||
this.validateSlotDuration(eventType, input.slotDuration);
|
||||
}
|
||||
|
||||
const endDate = startDate.plus({ minutes: input.slotDuration ?? eventType.length });
|
||||
|
||||
+50
@@ -167,6 +167,56 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
return request(app.getHttpServer()).post(`/v2/teams/${team.id}/event-types`).send(body).expect(404);
|
||||
});
|
||||
|
||||
it("should not be able to create phone-only event type", async () => {
|
||||
const body: CreateTeamEventTypeInput_2024_06_14 = {
|
||||
title: "Phone coding consultation",
|
||||
slug: "phone-coding-consultation",
|
||||
description: "Our team will review your codebase.",
|
||||
lengthInMinutes: 60,
|
||||
locations: [
|
||||
{
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
},
|
||||
{
|
||||
type: "organizersDefaultApp",
|
||||
},
|
||||
],
|
||||
schedulingType: "COLLECTIVE",
|
||||
hosts: [
|
||||
{
|
||||
userId: teamMember1.id,
|
||||
},
|
||||
{
|
||||
userId: teamMember2.id,
|
||||
},
|
||||
],
|
||||
bookingFields: [
|
||||
{
|
||||
type: "email",
|
||||
required: false,
|
||||
label: "Email",
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
label: "Phone number",
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/teams/${team.id}/event-types`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
expect(response.body.error.message).toBe(
|
||||
"checkIsEmailUserAccessible - Email booking field must be required and visible"
|
||||
);
|
||||
});
|
||||
|
||||
it("should create a collective team event-type", async () => {
|
||||
const body: CreateTeamEventTypeInput_2024_06_14 = {
|
||||
title: `teams-event-types-collective-${randomString()}`,
|
||||
|
||||
+2
-1
@@ -181,7 +181,8 @@ export class TeamsEventTypesController {
|
||||
eventTypeId,
|
||||
teamId,
|
||||
transformedBody,
|
||||
user
|
||||
user,
|
||||
false
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -30,6 +30,10 @@ export class TeamsEventTypesService {
|
||||
teamId: number,
|
||||
body: TransformedCreateTeamEventTypeInput
|
||||
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
|
||||
// note(Lauris): once phone only event types / bookings are enabled for simple users remove checkHasUserAccessibleEmailBookingField check
|
||||
if (body.bookingFields) {
|
||||
this.eventTypesService.checkHasUserAccessibleEmailBookingField(body.bookingFields);
|
||||
}
|
||||
const eventTypeUser = await this.getUserToCreateTeamEvent(user);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { hosts, children, destinationCalendar, ...rest } = body;
|
||||
@@ -44,7 +48,7 @@ export class TeamsEventTypesService {
|
||||
},
|
||||
});
|
||||
|
||||
return this.updateTeamEventType(eventTypeCreated.id, teamId, body, user);
|
||||
return this.updateTeamEventType(eventTypeCreated.id, teamId, body, user, false);
|
||||
}
|
||||
|
||||
async validateEventTypeExists(teamId: number, eventTypeId: number) {
|
||||
@@ -105,8 +109,14 @@ export class TeamsEventTypesService {
|
||||
eventTypeId: number,
|
||||
teamId: number,
|
||||
body: TransformedUpdateTeamEventTypeInput,
|
||||
user: UserWithProfile
|
||||
user: UserWithProfile,
|
||||
// note(Lauris): once phone only event types / bookings are enabled for simple users remove isOrg parameter (right now only organization team event types support hidden / non-required email field)
|
||||
isOrg: boolean
|
||||
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
|
||||
if (!isOrg && body.bookingFields) {
|
||||
// note(Lauris): once phone only event types / bookings are enabled for simple users remove checkHasUserAccessibleEmailBookingField check
|
||||
this.eventTypesService.checkHasUserAccessibleEmailBookingField(body.bookingFields);
|
||||
}
|
||||
await this.validateEventTypeExists(teamId, eventTypeId);
|
||||
const eventTypeUser = await this.eventTypesService.getUserToUpdateEvent(user);
|
||||
await updateEventType({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,8 @@
|
||||
"@calcom/platform-libraries/event-types": ["../../../packages/platform/libraries/event-types.ts"],
|
||||
"@calcom/platform-libraries/slots": ["../../../packages/platform/libraries/slots.ts"],
|
||||
"@calcom/platform-libraries/emails": ["../../../packages/platform/libraries/emails.ts"],
|
||||
"@calcom/platform-libraries/schedules": ["../../../packages/platform/libraries/schedules.ts"]
|
||||
"@calcom/platform-libraries/schedules": ["../../../packages/platform/libraries/schedules.ts"],
|
||||
"@calcom/platform-libraries/app-store": ["../../../packages/platform/libraries/app-store.ts"]
|
||||
},
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
@@ -8,4 +8,8 @@ export const generateMetadata = async () =>
|
||||
(t) => t("create_your_org_description")
|
||||
);
|
||||
|
||||
export default EnterprisePage;
|
||||
const ServerPageWrapper = async () => {
|
||||
return <EnterprisePage />;
|
||||
};
|
||||
|
||||
export default ServerPageWrapper;
|
||||
|
||||
+1
-1
@@ -174,7 +174,7 @@ const organizationAdminKeys = [
|
||||
|
||||
const useTabs = ({ isDelegationCredentialEnabled }: { isDelegationCredentialEnabled: boolean }) => {
|
||||
const session = useSession();
|
||||
const { data: user } = trpc.viewer.me.useQuery({ includePasswordAdded: true });
|
||||
const { data: user } = trpc.viewer.me.get.useQuery({ includePasswordAdded: true });
|
||||
const orgBranding = useOrgBranding();
|
||||
const isAdmin = session.data?.user.role === UserPermissionRole.ADMIN;
|
||||
const isOrgAdminOrOwner =
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { getTranslate } from "app/_utils";
|
||||
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import CreateNewOutOfOfficeEntryButton from "@calcom/features/settings/outOfOffice/CreateNewOutOfOfficeEntryButton";
|
||||
import { OutOfOfficeEntriesList } from "@calcom/features/settings/outOfOffice/OutOfOfficeEntriesList";
|
||||
import OutOfOfficeEntriesList from "@calcom/features/settings/outOfOffice/OutOfOfficeEntriesList";
|
||||
import { OutOfOfficeToggleGroup } from "@calcom/features/settings/outOfOffice/OutOfOfficeToggleGroup";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
|
||||
+2
-3
@@ -3,11 +3,10 @@ import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { getServerSideProps } from "@calcom/features/ee/organizations/pages/organization";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/[id]/about-view";
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/about-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
+2
-3
@@ -3,11 +3,10 @@ import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { getServerSideProps } from "@calcom/features/ee/organizations/pages/organization";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/[id]/add-teams-view";
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/add-teams-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
@@ -0,0 +1,29 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies } from "next/headers";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboarding-handover";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("handover_onboarding_page_title"),
|
||||
(t) => t("handover_onboarding_page_description")
|
||||
);
|
||||
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<LegacyPage />
|
||||
</LayoutWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerPage;
|
||||
+2
-3
@@ -3,11 +3,10 @@ import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { getServerSideProps } from "@calcom/features/ee/organizations/pages/organization";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/[id]/onboard-members-view";
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboard-members-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
@@ -7,7 +7,6 @@ import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequir
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
import { type inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/create-new-view";
|
||||
|
||||
@@ -17,7 +16,11 @@ export const generateMetadata = async () =>
|
||||
(t) => t("organizations_description")
|
||||
);
|
||||
|
||||
const getData = withAppDirSsr<inferSSRProps<typeof getServerSideProps>>(getServerSideProps);
|
||||
type Props = {
|
||||
isOrg: boolean;
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<Props>(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/payment-status-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("organization_onboarding_status"),
|
||||
(t) => t("organization_onboarding_status_description")
|
||||
);
|
||||
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<LegacyPage />
|
||||
</LayoutWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerPage;
|
||||
@@ -7,17 +7,20 @@ import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequir
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
import { type inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/platform/new/create-new-view";
|
||||
|
||||
type Props = {
|
||||
isOrg: boolean;
|
||||
};
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("set_up_your_platform_organization"),
|
||||
(t) => t("platform_organization_description")
|
||||
);
|
||||
|
||||
const getData = withAppDirSsr<inferSSRProps<typeof getServerSideProps>>(getServerSideProps);
|
||||
const getData = withAppDirSsr<Props>(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
|
||||
@@ -8,4 +8,8 @@ export const generateMetadata = async () =>
|
||||
(t) => t("no_meeting_found")
|
||||
);
|
||||
|
||||
export default NoMeetingFound;
|
||||
const ServerPage = async () => {
|
||||
return <NoMeetingFound />;
|
||||
};
|
||||
|
||||
export default ServerPage;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import type { AppRouter } from "@calcom/trpc/types/server/routers/_app";
|
||||
|
||||
import { createTRPCReact } from "@trpc/react-query";
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useState } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
import { ENDPOINTS } from "@calcom/trpc/react/shared";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import type { AppRouter } from "@calcom/trpc/types/server/routers/_app";
|
||||
|
||||
import { httpBatchLink, httpLink, loggerLink, splitLink, TRPCClientError } from "@trpc/client";
|
||||
|
||||
|
||||
@@ -46,6 +46,4 @@ async function handler(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
const postHandler = defaultResponderForAppDir(handler);
|
||||
|
||||
export { postHandler as POST };
|
||||
export const POST = defaultResponderForAppDir(handler);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
@@ -14,5 +15,5 @@ async function handler(req: NextRequest) {
|
||||
|
||||
return NextResponse.json({ username: account.name }, { status: 201 });
|
||||
}
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
export const GET = defaultResponderForAppDir(handler);
|
||||
export const POST = defaultResponderForAppDir(handler);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import { parseUrlFormData } from "app/api/parseRequestData";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { NextResponse } from "next/server";
|
||||
@@ -7,7 +8,7 @@ import prisma from "@calcom/prisma";
|
||||
import { generateSecret } from "@calcom/trpc/server/routers/viewer/oAuth/addClient.handler";
|
||||
import type { OAuthTokenPayload } from "@calcom/types/oauth";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
async function handler(req: NextRequest) {
|
||||
const { client_id, client_secret, grant_type } = await parseUrlFormData(req);
|
||||
|
||||
if (!client_id || !client_secret) {
|
||||
@@ -63,3 +64,5 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
return NextResponse.json({ access_token }, { status: 200 });
|
||||
}
|
||||
|
||||
export const POST = defaultResponderForAppDir(handler);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import { parseUrlFormData } from "app/api/parseRequestData";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { NextResponse } from "next/server";
|
||||
@@ -7,7 +8,7 @@ import prisma from "@calcom/prisma";
|
||||
import { generateSecret } from "@calcom/trpc/server/routers/viewer/oAuth/addClient.handler";
|
||||
import type { OAuthTokenPayload } from "@calcom/types/oauth";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
async function handler(req: NextRequest) {
|
||||
const { code, client_id, client_secret, grant_type, redirect_uri } = await parseUrlFormData(req);
|
||||
if (grant_type !== "authorization_code") {
|
||||
return NextResponse.json({ message: "grant_type invalid" }, { status: 400 });
|
||||
@@ -88,3 +89,5 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
return NextResponse.json({ access_token, refresh_token }, { status: 200 });
|
||||
}
|
||||
|
||||
export const POST = defaultResponderForAppDir(handler);
|
||||
|
||||
@@ -36,6 +36,4 @@ async function handler(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
const getHandler = defaultResponderForAppDir(handler);
|
||||
|
||||
export { getHandler as GET };
|
||||
export const GET = defaultResponderForAppDir(handler);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import { parseRequestData } from "app/api/parseRequestData";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
@@ -75,4 +76,4 @@ async function expireResetPasswordRequest(rawRequestId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export { handler as POST };
|
||||
export const POST = defaultResponderForAppDir(handler);
|
||||
|
||||
@@ -22,8 +22,6 @@ async function handler(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
const getHandler = defaultResponderForAppDir(handler);
|
||||
|
||||
export { getHandler as GET };
|
||||
export const GET = defaultResponderForAppDir(handler);
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -20,6 +20,4 @@ async function handler(req: NextRequest) {
|
||||
return NextResponse.json({ message: "No redirect URL provided" }, { status: 400 });
|
||||
}
|
||||
|
||||
const postHandler = defaultResponderForAppDir(handler);
|
||||
|
||||
export { postHandler as POST };
|
||||
export const POST = defaultResponderForAppDir(handler);
|
||||
|
||||
@@ -12,6 +12,4 @@ async function handler(req: NextRequest) {
|
||||
return NextResponse.json(tokenResponse);
|
||||
}
|
||||
|
||||
const postHandler = defaultResponderForAppDir(handler);
|
||||
|
||||
export { postHandler as POST };
|
||||
export const POST = defaultResponderForAppDir(handler);
|
||||
|
||||
@@ -31,8 +31,6 @@ async function handler(req: NextRequest) {
|
||||
return NextResponse.json(userInfo);
|
||||
}
|
||||
|
||||
const getHandler = defaultResponderForAppDir(handler);
|
||||
|
||||
export { getHandler as GET };
|
||||
export const GET = defaultResponderForAppDir(handler);
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -59,6 +59,4 @@ async function handler(req: NextRequest) {
|
||||
return NextResponse.json({ message: "First admin user created successfully." });
|
||||
}
|
||||
|
||||
const postHandler = defaultResponderForAppDir(handler);
|
||||
|
||||
export { postHandler as POST };
|
||||
export const POST = defaultResponderForAppDir(handler);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import { parseRequestData } from "app/api/parseRequestData";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
@@ -13,7 +14,7 @@ import { IdentityProvider } from "@calcom/prisma/client";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
async function handler(req: NextRequest) {
|
||||
const body = await parseRequestData(req);
|
||||
const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) });
|
||||
|
||||
@@ -114,3 +115,5 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
return NextResponse.json({ message: "Two factor disabled" });
|
||||
}
|
||||
|
||||
export const POST = defaultResponderForAppDir(handler);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import { parseRequestData } from "app/api/parseRequestData";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
@@ -11,7 +12,7 @@ import prisma from "@calcom/prisma";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
async function postHandler(req: NextRequest) {
|
||||
const body = await parseRequestData(req);
|
||||
const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) });
|
||||
|
||||
@@ -67,3 +68,5 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
return NextResponse.json({ message: "Two-factor enabled" });
|
||||
}
|
||||
|
||||
export const POST = defaultResponderForAppDir(postHandler);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import { parseRequestData } from "app/api/parseRequestData";
|
||||
import crypto from "crypto";
|
||||
import { cookies, headers } from "next/headers";
|
||||
@@ -15,7 +16,7 @@ import { IdentityProvider } from "@calcom/prisma/enums";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
async function postHandler(req: NextRequest) {
|
||||
const body = await parseRequestData(req);
|
||||
const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) });
|
||||
|
||||
@@ -81,3 +82,5 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
return NextResponse.json({ secret, keyUri, dataUri, backupCodes });
|
||||
}
|
||||
|
||||
export const POST = defaultResponderForAppDir(postHandler);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
@@ -105,4 +106,6 @@ async function deleteHandler(req: NextRequest) {
|
||||
return NextResponse.json({ message: "Calendar Selection Saved" });
|
||||
}
|
||||
|
||||
export { deleteHandler as DELETE, postHandler as POST, getHandler as GET };
|
||||
export const POST = defaultResponderForAppDir(postHandler);
|
||||
export const DELETE = defaultResponderForAppDir(deleteHandler);
|
||||
export const GET = defaultResponderForAppDir(getHandler);
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AVATAR_FALLBACK } from "@calcom/lib/constants";
|
||||
import { AVATAR_FALLBACK, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const querySchema = z.object({
|
||||
@@ -47,7 +47,8 @@ async function handler(req: NextRequest, { params }: { params: Params }) {
|
||||
img = data;
|
||||
} catch (e) {
|
||||
// If anything goes wrong or avatar is not found, use default avatar
|
||||
return NextResponse.redirect(AVATAR_FALLBACK, 302);
|
||||
const url = new URL(AVATAR_FALLBACK, WEBAPP_URL).toString();
|
||||
return NextResponse.redirect(url, 302);
|
||||
}
|
||||
|
||||
const decoded = img.toString().replace("data:image/png;base64,", "").replace("data:image/jpeg;base64,", "");
|
||||
@@ -63,6 +64,4 @@ async function handler(req: NextRequest, { params }: { params: Params }) {
|
||||
});
|
||||
}
|
||||
|
||||
const getHandler = defaultResponderForAppDir(handler);
|
||||
|
||||
export { getHandler as GET };
|
||||
export const GET = defaultResponderForAppDir(handler);
|
||||
|
||||
@@ -26,7 +26,5 @@ async function handler(req: NextRequest) {
|
||||
return NextResponse.json(result, { status: statusCode });
|
||||
}
|
||||
|
||||
const deleteHandler = defaultResponderForAppDir(handler);
|
||||
const postHandler = defaultResponderForAppDir(handler);
|
||||
|
||||
export { deleteHandler as DELETE, postHandler as POST };
|
||||
export const DELETE = defaultResponderForAppDir(handler);
|
||||
export const POST = defaultResponderForAppDir(handler);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
@@ -11,7 +12,7 @@ import { BookingStatus, ReminderType } from "@calcom/prisma/enums";
|
||||
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
async function postHandler(request: NextRequest) {
|
||||
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
|
||||
|
||||
if (process.env.CRON_API_KEY !== apiKey) {
|
||||
@@ -54,6 +55,8 @@ export async function POST(request: NextRequest) {
|
||||
locale: true,
|
||||
timeZone: true,
|
||||
destinationCalendar: true,
|
||||
isPlatformManaged: true,
|
||||
platformOAuthClients: { select: { id: true, areEmailsEnabled: true } },
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
@@ -69,11 +72,18 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
const bookingsToRemind = bookings.filter(
|
||||
(booking) =>
|
||||
!booking.user ||
|
||||
!booking.user.isPlatformManaged ||
|
||||
(booking.user.isPlatformManaged && Boolean(booking.user.platformOAuthClients?.[0]?.areEmailsEnabled))
|
||||
);
|
||||
|
||||
const reminders = await prisma.reminderMail.findMany({
|
||||
where: {
|
||||
reminderType: ReminderType.PENDING_BOOKING_CONFIRMATION,
|
||||
referenceId: {
|
||||
in: bookings.map((b) => b.id),
|
||||
in: bookingsToRemind.map((b) => b.id),
|
||||
},
|
||||
elapsedMinutes: {
|
||||
gte: interval,
|
||||
@@ -81,7 +91,7 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
for (const booking of bookings.filter((b) => !reminders.some((r) => r.referenceId == b.id))) {
|
||||
for (const booking of bookingsToRemind.filter((b) => !reminders.some((r) => r.referenceId == b.id))) {
|
||||
const { user } = booking;
|
||||
const name = user?.name || user?.username;
|
||||
if (!user || !name || !user.timeZone) {
|
||||
@@ -145,3 +155,5 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
return NextResponse.json({ notificationsSent });
|
||||
}
|
||||
|
||||
export const POST = defaultResponderForAppDir(postHandler);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
async function postHandler(request: NextRequest) {
|
||||
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
|
||||
|
||||
if (![process.env.CRON_API_KEY, `Bearer ${process.env.CRON_SECRET}`].includes(`${apiKey}`)) {
|
||||
@@ -21,3 +22,5 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
return NextResponse.json({ ok: true, count: deleted.count });
|
||||
}
|
||||
|
||||
export const POST = defaultResponderForAppDir(postHandler);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
@@ -20,7 +21,7 @@ const travelScheduleSelect = {
|
||||
},
|
||||
};
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
async function postHandler(request: NextRequest) {
|
||||
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
|
||||
|
||||
if (process.env.CRON_API_KEY !== apiKey) {
|
||||
@@ -167,3 +168,5 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
return NextResponse.json({ timeZonesChanged });
|
||||
}
|
||||
|
||||
export const POST = defaultResponderForAppDir(postHandler);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
@@ -9,7 +10,7 @@ const querySchema = z.object({
|
||||
page: z.coerce.number().min(0).optional().default(0),
|
||||
});
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
async function postHandler(request: NextRequest) {
|
||||
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
|
||||
|
||||
if (process.env.CRON_API_KEY !== apiKey) {
|
||||
@@ -51,3 +52,5 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
export const POST = defaultResponderForAppDir(postHandler);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
@@ -13,7 +14,7 @@ const querySchema = z.object({
|
||||
page: z.coerce.number().min(0).optional().default(0),
|
||||
});
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
async function postHandler(request: NextRequest) {
|
||||
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
|
||||
|
||||
if (process.env.CRON_API_KEY !== apiKey) {
|
||||
@@ -313,3 +314,5 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
export const POST = defaultResponderForAppDir(postHandler);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
@@ -15,7 +16,7 @@ const log = logger.getSubLogger({
|
||||
* syncAppMeta makes sure any app metadata that has been replicated into the database
|
||||
* remains synchronized with any changes made to the app config files.
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
async function postHandler(request: NextRequest) {
|
||||
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
|
||||
|
||||
if (process.env.CRON_API_KEY !== apiKey) {
|
||||
@@ -62,3 +63,5 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
export const POST = defaultResponderForAppDir(postHandler);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { handleWebhookScheduledTriggers } from "@calcom/features/webhooks/lib/handleWebhookScheduledTriggers";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
async function postHandler(req: NextRequest) {
|
||||
const apiKey = req.headers.get("authorization") || req.nextUrl.searchParams.get("apiKey");
|
||||
|
||||
if (process.env.CRON_API_KEY !== apiKey) {
|
||||
@@ -15,3 +16,5 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
export const POST = defaultResponderForAppDir(postHandler);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
|
||||
import { handler } from "@calcom/features/ee/workflows/api/scheduleEmailReminders";
|
||||
|
||||
export { handler as POST };
|
||||
export const POST = defaultResponderForAppDir(handler);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
|
||||
import { handler } from "@calcom/features/ee/workflows/api/scheduleSMSReminders";
|
||||
|
||||
export { handler as POST };
|
||||
export const POST = defaultResponderForAppDir(handler);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
|
||||
import { handler } from "@calcom/features/ee/workflows/api/scheduleWhatsappReminders";
|
||||
|
||||
export { handler as POST };
|
||||
export const POST = defaultResponderForAppDir(handler);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { renderEmail } from "@calcom/emails";
|
||||
@@ -7,7 +8,7 @@ import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
/**
|
||||
* This API endpoint is used for development purposes to preview email templates
|
||||
*/
|
||||
export async function GET() {
|
||||
async function getHandler() {
|
||||
// Only allow in development mode
|
||||
if (IS_PRODUCTION) {
|
||||
return new NextResponse("Only for development purposes", {
|
||||
@@ -73,3 +74,5 @@ export async function GET() {
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export const GET = defaultResponderForAppDir(getHandler);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
@@ -6,7 +7,7 @@ import { FUTURE_ROUTES_OVERRIDE_COOKIE_NAME as COOKIE_NAME } from "@calcom/lib/c
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
export async function GET() {
|
||||
async function getHandler() {
|
||||
const headersList = headers();
|
||||
const cookiesList = cookies();
|
||||
const legacyReq = buildLegacyRequest(headersList, cookiesList);
|
||||
@@ -35,3 +36,5 @@ export async function GET() {
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export const GET = defaultResponderForAppDir(getHandler);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
@@ -7,7 +8,7 @@ import prisma from "@calcom/prisma";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
export async function POST() {
|
||||
async function postHandler() {
|
||||
const headersList = headers();
|
||||
const cookiesList = cookies();
|
||||
const legacyReq = buildLegacyRequest(headersList, cookiesList);
|
||||
@@ -51,3 +52,5 @@ export async function POST() {
|
||||
|
||||
return NextResponse.json({ shortLink });
|
||||
}
|
||||
|
||||
export const POST = defaultResponderForAppDir(postHandler);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import { headers } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
async function getHandler() {
|
||||
const headersList = headers();
|
||||
const country = headersList.get("x-vercel-ip-country") || "Unknown";
|
||||
|
||||
@@ -10,3 +11,5 @@ export async function GET() {
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export const GET = defaultResponderForAppDir(getHandler);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
import crypto from "node:crypto";
|
||||
@@ -6,7 +7,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
export async function GET() {
|
||||
async function getHandler() {
|
||||
const headersList = headers();
|
||||
const cookiesList = cookies();
|
||||
const legacyReq = buildLegacyRequest(headersList, cookiesList);
|
||||
@@ -28,3 +29,5 @@ export async function GET() {
|
||||
|
||||
return NextResponse.json({ hash });
|
||||
}
|
||||
|
||||
export const GET = defaultResponderForAppDir(getHandler);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
@@ -162,7 +163,7 @@ async function getTeamLogos(subdomain: string, isValidOrgDomain: boolean) {
|
||||
/**
|
||||
* This API endpoint is used to serve the logo associated with a team if no logo is found we serve our default logo
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
async function getHandler(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const parsedQuery = logoApiSchema.parse(Object.fromEntries(searchParams.entries()));
|
||||
|
||||
@@ -217,3 +218,5 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: "Failed fetching logo" }, { status: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = defaultResponderForAppDir(getHandler);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
@@ -8,7 +9,7 @@ import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
let isCold = true;
|
||||
|
||||
export async function GET() {
|
||||
async function getHandler() {
|
||||
const prePrismaDate = performance.now();
|
||||
const prisma = (await import("@calcom/prisma")).default;
|
||||
const preSessionDate = performance.now();
|
||||
@@ -48,3 +49,5 @@ export async function GET() {
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export const GET = defaultResponderForAppDir(getHandler);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
async function getHandler() {
|
||||
return NextResponse.json({ message: "Please don't" }, { status: 400 });
|
||||
}
|
||||
|
||||
export async function POST() {
|
||||
async function postHandler() {
|
||||
return NextResponse.json({ message: "Please don't" }, { status: 400 });
|
||||
}
|
||||
|
||||
export const GET = defaultResponderForAppDir(getHandler);
|
||||
export const POST = defaultResponderForAppDir(postHandler);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { IS_PLAIN_CHAT_ENABLED } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
@@ -20,6 +21,11 @@ const responseSchema = z.object({
|
||||
});
|
||||
|
||||
async function handler() {
|
||||
// Early return if Plain Chat is not enabled
|
||||
if (!IS_PLAIN_CHAT_ENABLED) {
|
||||
return NextResponse.json({ error: "Plain Chat is not enabled" }, { status: 404 });
|
||||
}
|
||||
|
||||
const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) });
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized - No session email found" }, { status: 401 });
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import { createHmac } from "crypto";
|
||||
import { headers } from "next/headers";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { getRoomNameFromRecordingId, getBatchProcessorJobAccessLink } from "@calcom/app-store/dailyvideo/lib";
|
||||
import { sendDailyVideoRecordingEmails } from "@calcom/emails";
|
||||
import { sendDailyVideoTranscriptEmails } from "@calcom/emails";
|
||||
import {
|
||||
sendDailyVideoRecordingEmails,
|
||||
sendDailyVideoTranscriptEmails,
|
||||
} from "@calcom/emails/daily-video-emails";
|
||||
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
@@ -48,7 +51,7 @@ const getDownloadLinkOfCalVideo = async (recordingId: string) => {
|
||||
return downloadLink;
|
||||
};
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
export async function postHandler(request: NextRequest) {
|
||||
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
|
||||
return NextResponse.json({ message: "No SendGrid API key or email" }, { status: 405 });
|
||||
}
|
||||
@@ -225,3 +228,5 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const POST = defaultResponderForAppDir(postHandler);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { DirectorySyncEvent, DirectorySyncRequest } from "@boxyhq/saml-jackson";
|
||||
import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
@@ -51,23 +52,23 @@ const handleEvents = async (event: DirectorySyncEvent) => {
|
||||
};
|
||||
|
||||
// This is the handler for the SCIM API requests
|
||||
export async function GET(request: NextRequest, { params }: { params: { directory?: string[] } }) {
|
||||
async function getHandler(request: NextRequest, { params }: { params: { directory?: string[] } }) {
|
||||
return handleScimRequest(request, "GET", params.directory);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest, { params }: { params: { directory?: string[] } }) {
|
||||
async function postHandler(request: NextRequest, { params }: { params: { directory?: string[] } }) {
|
||||
return handleScimRequest(request, "POST", params.directory);
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest, { params }: { params: { directory?: string[] } }) {
|
||||
async function putHandler(request: NextRequest, { params }: { params: { directory?: string[] } }) {
|
||||
return handleScimRequest(request, "PUT", params.directory);
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest, { params }: { params: { directory?: string[] } }) {
|
||||
async function patchHandler(request: NextRequest, { params }: { params: { directory?: string[] } }) {
|
||||
return handleScimRequest(request, "PATCH", params.directory);
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest, { params }: { params: { directory?: string[] } }) {
|
||||
async function deleteHandler(request: NextRequest, { params }: { params: { directory?: string[] } }) {
|
||||
return handleScimRequest(request, "DELETE", params.directory);
|
||||
}
|
||||
|
||||
@@ -139,3 +140,8 @@ async function handleScimRequest(request: NextRequest, method: string, directory
|
||||
|
||||
return NextResponse.json(data, { status });
|
||||
}
|
||||
export const GET = defaultResponderForAppDir(getHandler);
|
||||
export const POST = defaultResponderForAppDir(postHandler);
|
||||
export const PATCH = defaultResponderForAppDir(patchHandler);
|
||||
export const DELETE = defaultResponderForAppDir(deleteHandler);
|
||||
export const PUT = defaultResponderForAppDir(putHandler);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user