From 4cc1bc6746135c8d6e0d1d522746c367d3ede01a Mon Sep 17 00:00:00 2001 From: Lauris Skraucis Date: Thu, 24 Apr 2025 15:06:37 +0200 Subject: [PATCH] fix: v2 sentry errors (#20902) * refactor: no_available_users_found_error for team event * fix: hosts_unavailable_for_booking * fix: Cannot read properties of undefined (reading 'replace') * fix: Cannot read properties of undefined (reading 'phoneNumber') * fix: No SelectedCalendar found. * fix: Cannot read properties of undefined (reading 'length') * refactor: add bookings errors service --- apps/api/v2/README.md | 2 + .../ee/bookings/2024-08-13/bookings.module.ts | 2 + .../2024-08-13/services/bookings.service.ts | 43 +++------------ .../2024-08-13/services/errors.service.ts | 55 +++++++++++++++++++ .../controllers/calendars.controller.ts | 10 ++-- .../services/apple-calendar.service.ts | 3 +- apps/api/v2/src/ee/gcal/gcal.controller.ts | 4 -- .../api-auth-guard-only-allow.decorator.ts | 5 ++ .../auth/guards/api-auth/api-auth.guard.ts | 15 ++++- .../strategies/api-auth/api-auth.strategy.ts | 25 +++++++-- .../selected-calendars.repository.ts | 7 ++- .../services/selected-calendars.service.ts | 32 ++++++++--- apps/api/v2/swagger/documentation.json | 29 +++++++++- docs/api-reference/v2/openapi.json | 26 ++++++++- .../atoms/hooks/connect/useConnect.ts | 6 +- .../2024-08-13/inputs/create-booking.input.ts | 2 + packages/platform/types/calendars/index.ts | 1 + .../inputs/busy-times.input.ts} | 0 .../create-calendar-credentials.input.ts | 14 +++++ .../platform/types/calendars/inputs/index.ts | 2 + 20 files changed, 216 insertions(+), 67 deletions(-) create mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/services/errors.service.ts create mode 100644 apps/api/v2/src/modules/auth/decorators/api-auth-guard-only-allow.decorator.ts create mode 100644 packages/platform/types/calendars/index.ts rename packages/platform/types/{calendars.ts => calendars/inputs/busy-times.input.ts} (100%) create mode 100644 packages/platform/types/calendars/inputs/create-calendar-credentials.input.ts create mode 100644 packages/platform/types/calendars/inputs/index.ts diff --git a/apps/api/v2/README.md b/apps/api/v2/README.md index dbb5cc135e..8903497ac2 100644 --- a/apps/api/v2/README.md +++ b/apps/api/v2/README.md @@ -112,6 +112,8 @@ $ yarn run test:cov ### Guards 1. In case a guard would return "false" for "canActivate" instead throw ForbiddenException with an error message containing guard name and the error. 2. In case a guard would return "false" for "canActivate" DO NOT cache the result in redis, because we don't want that someone is forbidden, updates whatever was the problem, and then has to wait for cache to expire. We only cache in redis guard results where "canAccess" is "true". +3. If you use ApiAuthGuard but want that only specific auth method is allowed, for example, api key, then you also need to add `@ApiAuthGuardOnlyAllow(["API_KEY"])` under the `@UseGuards(ApiAuthGuard)`. Shortly, use `ApiAuthGuardOnlyAllow` to specify which auth methods are allowed by `ApiAuthGuard`. If `ApiAuthGuardOnlyAllow` is not used or nothing is passed to it or empty array it means that +all auth methods are allowed. ## Support diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts index 57e3df75c2..87f9c4ab20 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts @@ -1,6 +1,7 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository"; import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller"; import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; +import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service"; import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service"; import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service"; @@ -67,6 +68,7 @@ import { Module } from "@nestjs/common"; SelectedCalendarsRepository, OrganizationsTeamsRepository, OrganizationsRepository, + ErrorsBookingsService_2024_08_13, ], controllers: [BookingsController_2024_08_13], exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13], diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index 8e4244c2fb..ea32105e58 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -1,5 +1,6 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository"; import { CalendarLink } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output"; +import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service"; import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service"; import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service"; @@ -85,34 +86,19 @@ export class BookingsService_2024_08_13 { private readonly organizationsTeamsRepository: OrganizationsTeamsRepository, private readonly organizationsRepository: OrganizationsRepository, private readonly teamsRepository: TeamsRepository, - private readonly teamsEventTypesRepository: TeamsEventTypesRepository + private readonly teamsEventTypesRepository: TeamsEventTypesRepository, + private readonly errorsBookingsService: ErrorsBookingsService_2024_08_13 ) {} async createBooking(request: Request, body: CreateBookingInput) { + let bookingTeamEventType = false; try { const eventType = await this.getBookedEventType(body); + if (eventType?.team) { + bookingTeamEventType = true; + } if (!eventType) { - if (body.username && body.eventTypeSlug && !body.organizationSlug) { - throw new NotFoundException( - `Event type with slug ${body.eventTypeSlug} belonging to user ${body.username} not found.` - ); - } - if (body.username && body.eventTypeSlug && body.organizationSlug) { - throw new NotFoundException( - `Event type with slug ${body.eventTypeSlug} belonging to user ${body.username} within organization ${body.organizationSlug} not found.` - ); - } - if (body.teamSlug && body.eventTypeSlug && !body.organizationSlug) { - throw new NotFoundException( - `Event type with slug ${body.eventTypeSlug} belonging to team ${body.teamSlug} not found.` - ); - } - if (body.teamSlug && body.eventTypeSlug && body.organizationSlug) { - throw new NotFoundException( - `Event type with slug ${body.eventTypeSlug} belonging to team ${body.teamSlug} within organization ${body.organizationSlug} not found.` - ); - } - throw new NotFoundException(`Event type with id ${body.eventTypeId} not found.`); + this.errorsBookingsService.handleEventTypeToBeBookedNotFound(body); } body.eventTypeId = eventType.id; @@ -138,18 +124,7 @@ export class BookingsService_2024_08_13 { return await this.createRegularBooking(request, body, eventType); } catch (error) { - if (error instanceof Error) { - if (error.message === "no_available_users_found_error") { - throw new BadRequestException("User either already has booking at this time or is not available"); - } else if (error.message === "booking_time_out_of_bounds_error") { - throw new BadRequestException( - `The event type can't be booked at selected time ${body.start}. This could be because it's too soon (violating the minimum booking notice) or too far in the future (outside the event's scheduling window). Try fetching available slots first using the GET /v2/slots endpoint and then make a booking with "start" time equal to one of the available slots: https://cal.com/docs/api-reference/v2/slots` - ); - } else if (error.message === "Attempting to book a meeting in the past.") { - throw new BadRequestException("Attempting to book a meeting in the past."); - } - } - throw error; + this.errorsBookingsService.handleBookingError(error, bookingTeamEventType); } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/errors.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/errors.service.ts new file mode 100644 index 0000000000..0563358baf --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/errors.service.ts @@ -0,0 +1,55 @@ +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +import { Logger } from "@nestjs/common"; + +import { CreateBookingInput } from "@calcom/platform-types"; + +@Injectable() +export class ErrorsBookingsService_2024_08_13 { + private readonly logger = new Logger("ErrorsBookingsService_2024_08_13"); + + handleEventTypeToBeBookedNotFound(body: CreateBookingInput): never { + if (body.username && body.eventTypeSlug && !body.organizationSlug) { + throw new NotFoundException( + `Event type with slug ${body.eventTypeSlug} belonging to user ${body.username} not found.` + ); + } + if (body.username && body.eventTypeSlug && body.organizationSlug) { + throw new NotFoundException( + `Event type with slug ${body.eventTypeSlug} belonging to user ${body.username} within organization ${body.organizationSlug} not found.` + ); + } + if (body.teamSlug && body.eventTypeSlug && !body.organizationSlug) { + throw new NotFoundException( + `Event type with slug ${body.eventTypeSlug} belonging to team ${body.teamSlug} not found.` + ); + } + if (body.teamSlug && body.eventTypeSlug && body.organizationSlug) { + throw new NotFoundException( + `Event type with slug ${body.eventTypeSlug} belonging to team ${body.teamSlug} within organization ${body.organizationSlug} not found.` + ); + } + throw new NotFoundException(`Event type with id ${body.eventTypeId} not found.`); + } + + handleBookingError(error: unknown, bookingTeamEventType: boolean): never { + const hostsUnavaile = "One of the hosts either already has booking at this time or is not available"; + + if (error instanceof Error) { + if (error.message === "no_available_users_found_error") { + if (bookingTeamEventType) { + throw new BadRequestException(hostsUnavaile); + } + throw new BadRequestException("User either already has booking at this time or is not available"); + } else if (error.message === "booking_time_out_of_bounds_error") { + throw new BadRequestException( + `The event type can't be booked at the "start" time provided. This could be because it's too soon (violating the minimum booking notice) or too far in the future (outside the event's scheduling window). Try fetching available slots first using the GET /v2/slots endpoint and then make a booking with "start" time equal to one of the available slots: https://cal.com/docs/api-reference/v2/slots` + ); + } else if (error.message === "Attempting to book a meeting in the past.") { + throw new BadRequestException("Attempting to book a meeting in the past."); + } else if (error.message === "hosts_unavailable_for_booking") { + throw new BadRequestException(hostsUnavaile); + } + } + throw error; + } +} diff --git a/apps/api/v2/src/ee/calendars/controllers/calendars.controller.ts b/apps/api/v2/src/ee/calendars/controllers/calendars.controller.ts index b9f9a72109..b2cdc5d14f 100644 --- a/apps/api/v2/src/ee/calendars/controllers/calendars.controller.ts +++ b/apps/api/v2/src/ee/calendars/controllers/calendars.controller.ts @@ -15,6 +15,7 @@ import { IcsFeedService } from "@/ee/calendars/services/ics-feed.service"; import { OutlookService } from "@/ee/calendars/services/outlook.service"; import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers"; +import { ApiAuthGuardOnlyAllow } from "@/modules/auth/decorators/api-auth-guard-only-allow.decorator"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator"; import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; @@ -51,7 +52,7 @@ import { APPLE_CALENDAR, CREDENTIAL_CALENDARS, } from "@calcom/platform-constants"; -import { ApiResponse, CalendarBusyTimesInput } from "@calcom/platform-types"; +import { ApiResponse, CalendarBusyTimesInput, CreateCalendarCredentialsInput } from "@calcom/platform-types"; @Controller({ path: "/v2/calendars", @@ -135,6 +136,7 @@ export class CalendarsController { name: "calendar", }) @UseGuards(ApiAuthGuard) + @ApiAuthGuardOnlyAllow(["API_KEY", "ACCESS_TOKEN"]) @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) @Get("/:calendar/connect") @HttpCode(HttpStatus.OK) @@ -173,7 +175,7 @@ export class CalendarsController { @Get("/:calendar/save") @HttpCode(HttpStatus.OK) @Redirect(undefined, 301) - @ApiOperation({ summary: "Save an oAuth calendar credentials" }) + @ApiOperation({ summary: "Save Google or Outlook calendar credentials" }) async save( @Query("state") state: string, @Query("code") code: string, @@ -221,11 +223,11 @@ export class CalendarsController { @UseGuards(ApiAuthGuard) @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) @Post("/:calendar/credentials") - @ApiOperation({ summary: "Sync credentials" }) + @ApiOperation({ summary: "Save Apple calendar credentials" }) async syncCredentials( @GetUser() user: User, @Param("calendar") calendar: string, - @Body() body: { username: string; password: string } + @Body() body: CreateCalendarCredentialsInput ): Promise<{ status: string }> { const { username, password } = body; diff --git a/apps/api/v2/src/ee/calendars/services/apple-calendar.service.ts b/apps/api/v2/src/ee/calendars/services/apple-calendar.service.ts index ebf5ce4315..9a79276dea 100644 --- a/apps/api/v2/src/ee/calendars/services/apple-calendar.service.ts +++ b/apps/api/v2/src/ee/calendars/services/apple-calendar.service.ts @@ -60,8 +60,9 @@ export class AppleCalendarService implements CredentialSyncCalendarApp { } async saveCalendarCredentials(userId: number, userEmail: string, username: string, password: string) { - if (username.length <= 1 || password.length <= 1) + if (!username || !password || username.length <= 1 || password.length <= 1) { throw new BadRequestException(`Username or password cannot be empty`); + } const existingAppleCalendarCredentials = await this.credentialRepository.getAllUserCredentialsByTypeAndId( APPLE_CALENDAR_TYPE, diff --git a/apps/api/v2/src/ee/gcal/gcal.controller.ts b/apps/api/v2/src/ee/gcal/gcal.controller.ts index 817abec19f..7ef128ae06 100644 --- a/apps/api/v2/src/ee/gcal/gcal.controller.ts +++ b/apps/api/v2/src/ee/gcal/gcal.controller.ts @@ -9,8 +9,6 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; -import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository"; -import { TokensRepository } from "@/modules/tokens/tokens.repository"; import { BadRequestException, Controller, @@ -48,8 +46,6 @@ export class GcalController { constructor( private readonly credentialRepository: CredentialsRepository, - private readonly tokensRepository: TokensRepository, - private readonly selectedCalendarsRepository: SelectedCalendarsRepository, private readonly config: ConfigService, private readonly gcalService: GCalService, private readonly calendarsService: CalendarsService diff --git a/apps/api/v2/src/modules/auth/decorators/api-auth-guard-only-allow.decorator.ts b/apps/api/v2/src/modules/auth/decorators/api-auth-guard-only-allow.decorator.ts new file mode 100644 index 0000000000..cd294395c6 --- /dev/null +++ b/apps/api/v2/src/modules/auth/decorators/api-auth-guard-only-allow.decorator.ts @@ -0,0 +1,5 @@ +import { Reflector } from "@nestjs/core"; + +export type AllowedAuthMethod = "OAUTH_CLIENT_CREDENTIALS" | "API_KEY" | "ACCESS_TOKEN" | "NEXT_AUTH"; + +export const ApiAuthGuardOnlyAllow = Reflector.createDecorator(); diff --git a/apps/api/v2/src/modules/auth/guards/api-auth/api-auth.guard.ts b/apps/api/v2/src/modules/auth/guards/api-auth/api-auth.guard.ts index bfc6e240df..468e5a7aa9 100644 --- a/apps/api/v2/src/modules/auth/guards/api-auth/api-auth.guard.ts +++ b/apps/api/v2/src/modules/auth/guards/api-auth/api-auth.guard.ts @@ -1,7 +1,20 @@ +import { ExecutionContext, Inject } from "@nestjs/common"; +import { Reflector } from "@nestjs/core"; import { AuthGuard } from "@nestjs/passport"; +import { ApiAuthGuardOnlyAllow } from "../../decorators/api-auth-guard-only-allow.decorator"; + export class ApiAuthGuard extends AuthGuard("api-auth") { - constructor() { + constructor(@Inject(Reflector) private readonly reflector: Reflector) { super(); } + + getRequest(context: ExecutionContext) { + const request = context.switchToHttp().getRequest(); + + const allowedMethods = this.reflector.get(ApiAuthGuardOnlyAllow, context.getHandler()); + request.allowedAuthMethods = allowedMethods; + + return request; + } } diff --git a/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts b/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts index a46d0b8a84..00692b7772 100644 --- a/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts +++ b/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts @@ -18,11 +18,14 @@ import { getToken } from "next-auth/jwt"; import { INVALID_ACCESS_TOKEN, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants"; +import type { AllowedAuthMethod } from "../../decorators/api-auth-guard-only-allow.decorator"; + export type ApiAuthGuardUser = UserWithProfile & { isSystemAdmin: boolean }; export type ApiAuthGuardRequest = Request & { authMethod: AuthMethods; organizationId: number | null; user: ApiAuthGuardUser; + allowedAuthMethods?: AllowedAuthMethod[]; }; export const NO_AUTH_PROVIDED_MESSAGE = "No authentication method provided. Either pass an API key as 'Bearer' header or OAuth client credentials as 'x-cal-secret-key' and 'x-cal-client-id' headers"; @@ -49,12 +52,19 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") const oAuthClientId = params.clientId || request.get(X_CAL_CLIENT_ID); const bearerToken = request.get("Authorization")?.replace("Bearer ", ""); - if (oAuthClientId && oAuthClientSecret) { + const allowedMethods = request.allowedAuthMethods; + const noSpecificAuthExpected = !allowedMethods || !allowedMethods.length; + const oAuthAllowed = noSpecificAuthExpected || allowedMethods.includes("OAUTH_CLIENT_CREDENTIALS"); + const apiKeyAllowed = noSpecificAuthExpected || allowedMethods.includes("API_KEY"); + const accessTokenAllowed = noSpecificAuthExpected || allowedMethods.includes("ACCESS_TOKEN"); + const nextAuthAllowed = noSpecificAuthExpected || allowedMethods.includes("NEXT_AUTH"); + + if (oAuthClientId && oAuthClientSecret && oAuthAllowed) { request.authMethod = AuthMethods["OAUTH_CLIENT"]; return await this.authenticateOAuthClient(oAuthClientId, oAuthClientSecret, request); } - if (bearerToken) { + if (bearerToken && (apiKeyAllowed || accessTokenAllowed)) { const requestOrigin = request.get("Origin"); request.authMethod = isApiKey(bearerToken, this.config.get("api.apiKeyPrefix") ?? "cal_") ? AuthMethods["API_KEY"] @@ -64,13 +74,18 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") const nextAuthSecret = this.config.get("next.authSecret", { infer: true }); const nextAuthToken = await getToken({ req: request, secret: nextAuthSecret }); - - if (nextAuthToken) { + if (nextAuthToken && nextAuthAllowed) { request.authMethod = AuthMethods["NEXT_AUTH"]; return await this.authenticateNextAuth(nextAuthToken, request); } - throw new UnauthorizedException(`ApiAuthStrategy - ${NO_AUTH_PROVIDED_MESSAGE}`); + const noAuthProvided = !oAuthClientId && !oAuthClientSecret && !bearerToken && !nextAuthToken; + if (noAuthProvided) { + throw new UnauthorizedException(`ApiAuthStrategy - ${NO_AUTH_PROVIDED_MESSAGE}`); + } + throw new UnauthorizedException( + `ApiAuthStrategy - Invalid authentication method. Please provide one of the allowed methods: ${allowedMethods}` + ); } catch (err) { if (err instanceof Error) { return this.error(err); diff --git a/apps/api/v2/src/modules/selected-calendars/selected-calendars.repository.ts b/apps/api/v2/src/modules/selected-calendars/selected-calendars.repository.ts index 2082aeaad9..640026144f 100644 --- a/apps/api/v2/src/modules/selected-calendars/selected-calendars.repository.ts +++ b/apps/api/v2/src/modules/selected-calendars/selected-calendars.repository.ts @@ -7,6 +7,9 @@ const ensureUserLevelWhere = { eventTypeId: null, }; +export const NO_SELECTED_CALENDAR_FOUND = "No SelectedCalendar found."; +export const MULTIPLE_SELECTED_CALENDARS_FOUND = "Multiple SelecteCalendars found. Skipping deletion"; + @Injectable() export class SelectedCalendarsRepository { constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} @@ -113,11 +116,11 @@ export class SelectedCalendarsRepository { // Make the behaviour same as .delete which throws error if no record is found if (records.length === 0) { - throw new Error("No SelectedCalendar found."); + throw new Error(NO_SELECTED_CALENDAR_FOUND); } if (records.length > 1) { - throw new Error("Multiple SelecteCalendars found. Skipping deletion"); + throw new Error(MULTIPLE_SELECTED_CALENDARS_FOUND); } return await this.dbWrite.prisma.selectedCalendar.delete({ diff --git a/apps/api/v2/src/modules/selected-calendars/services/selected-calendars.service.ts b/apps/api/v2/src/modules/selected-calendars/services/selected-calendars.service.ts index bc2f153f43..6af04f9c56 100644 --- a/apps/api/v2/src/modules/selected-calendars/services/selected-calendars.service.ts +++ b/apps/api/v2/src/modules/selected-calendars/services/selected-calendars.service.ts @@ -5,9 +5,13 @@ import { SelectedCalendarsInputDto, SelectedCalendarsQueryParamsInputDto, } from "@/modules/selected-calendars/inputs/selected-calendars.input"; -import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository"; +import { + MULTIPLE_SELECTED_CALENDARS_FOUND, + NO_SELECTED_CALENDAR_FOUND, + SelectedCalendarsRepository, +} from "@/modules/selected-calendars/selected-calendars.repository"; import { UserWithProfile } from "@/modules/users/users.repository"; -import { Injectable, NotFoundException } from "@nestjs/common"; +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { SelectedCalendarRepository } from "@calcom/platform-libraries"; @@ -111,13 +115,23 @@ export class SelectedCalendarsService { } } - const removedCalendarEntry = await this.selectedCalendarsRepository.removeUserSelectedCalendar( - user.id, - integration, - externalId, - delegationCredentialId - ); + try { + const removedCalendarEntry = await this.selectedCalendarsRepository.removeUserSelectedCalendar( + user.id, + integration, + externalId, + delegationCredentialId + ); - return removedCalendarEntry; + return removedCalendarEntry; + } catch (error) { + if (error instanceof Error) { + if (error.message === NO_SELECTED_CALENDAR_FOUND) { + throw new NotFoundException(NO_SELECTED_CALENDAR_FOUND); + } else if (error.message === MULTIPLE_SELECTED_CALENDARS_FOUND) { + throw new BadRequestException(MULTIPLE_SELECTED_CALENDARS_FOUND); + } + } + } } } diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 39b519a70f..f4051cd617 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -7697,7 +7697,7 @@ "/v2/calendars/{calendar}/save": { "get": { "operationId": "CalendarsController_save", - "summary": "Save an oAuth calendar credentials", + "summary": "Save Google or Outlook calendar credentials", "parameters": [ { "name": "state", @@ -7741,7 +7741,7 @@ "/v2/calendars/{calendar}/credentials": { "post": { "operationId": "CalendarsController_syncCredentials", - "summary": "Sync credentials", + "summary": "Save Apple calendar credentials", "parameters": [ { "name": "calendar", @@ -7764,6 +7764,16 @@ } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCalendarCredentialsInput" + } + } + } + }, "responses": { "201": { "description": "" @@ -20178,6 +20188,21 @@ "data" ] }, + "CreateCalendarCredentialsInput": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "required": [ + "username", + "password" + ] + }, "DeleteCalendarCredentialsInputBodyDto": { "type": "object", "properties": { diff --git a/docs/api-reference/v2/openapi.json b/docs/api-reference/v2/openapi.json index 1bc2fbb830..d94af4488c 100644 --- a/docs/api-reference/v2/openapi.json +++ b/docs/api-reference/v2/openapi.json @@ -7354,7 +7354,7 @@ "/v2/calendars/{calendar}/save": { "get": { "operationId": "CalendarsController_save", - "summary": "Save an oAuth calendar credentials", + "summary": "Save Google or Outlook calendar credentials", "parameters": [ { "name": "state", @@ -7393,7 +7393,7 @@ "/v2/calendars/{calendar}/credentials": { "post": { "operationId": "CalendarsController_syncCredentials", - "summary": "Sync credentials", + "summary": "Save Apple calendar credentials", "parameters": [ { "name": "calendar", @@ -7414,6 +7414,16 @@ } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCalendarCredentialsInput" + } + } + } + }, "responses": { "201": { "description": "" @@ -18472,6 +18482,18 @@ }, "required": ["status", "data"] }, + "CreateCalendarCredentialsInput": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "required": ["username", "password"] + }, "DeleteCalendarCredentialsInputBodyDto": { "type": "object", "properties": { diff --git a/packages/platform/atoms/hooks/connect/useConnect.ts b/packages/platform/atoms/hooks/connect/useConnect.ts index 1401f49b6a..fe261c2bf4 100644 --- a/packages/platform/atoms/hooks/connect/useConnect.ts +++ b/packages/platform/atoms/hooks/connect/useConnect.ts @@ -2,7 +2,7 @@ import { useQuery, useMutation } from "@tanstack/react-query"; import type { CALENDARS } from "@calcom/platform-constants"; import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; -import type { ApiResponse, ApiErrorResponse } from "@calcom/platform-types"; +import type { ApiResponse, ApiErrorResponse, CreateCalendarCredentialsInput } from "@calcom/platform-types"; import http from "../../lib/http"; @@ -67,11 +67,11 @@ export const useSaveCalendarCredentials = ( const mutation = useMutation< ApiResponse<{ status: string }>, unknown, - { username: string; password: string; calendar: (typeof CALENDARS)[number] } + CreateCalendarCredentialsInput & { calendar: (typeof CALENDARS)[number] } >({ mutationFn: (data) => { const { calendar, username, password } = data; - const body = { + const body: CreateCalendarCredentialsInput = { username, password, }; diff --git a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts index b584d66101..328934f48f 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts @@ -17,6 +17,7 @@ import { Min, registerDecorator, Validate, + IsDefined, } from "class-validator"; import { isValidPhoneNumber } from "libphonenumber-js"; @@ -169,6 +170,7 @@ export class CreateBookingInput_2024_08_13 { type: Attendee, description: "The attendee's details.", }) + @IsDefined() @ValidateNested() @Type(() => Attendee) attendee!: Attendee; diff --git a/packages/platform/types/calendars/index.ts b/packages/platform/types/calendars/index.ts new file mode 100644 index 0000000000..1ac83e88a7 --- /dev/null +++ b/packages/platform/types/calendars/index.ts @@ -0,0 +1 @@ +export * from "./inputs"; diff --git a/packages/platform/types/calendars.ts b/packages/platform/types/calendars/inputs/busy-times.input.ts similarity index 100% rename from packages/platform/types/calendars.ts rename to packages/platform/types/calendars/inputs/busy-times.input.ts diff --git a/packages/platform/types/calendars/inputs/create-calendar-credentials.input.ts b/packages/platform/types/calendars/inputs/create-calendar-credentials.input.ts new file mode 100644 index 0000000000..ebfae0e420 --- /dev/null +++ b/packages/platform/types/calendars/inputs/create-calendar-credentials.input.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsString, MinLength } from "class-validator"; + +export class CreateCalendarCredentialsInput { + @IsString() + @MinLength(1) + @ApiProperty() + username!: string; + + @IsString() + @MinLength(1) + @ApiProperty() + password!: string; +} diff --git a/packages/platform/types/calendars/inputs/index.ts b/packages/platform/types/calendars/inputs/index.ts new file mode 100644 index 0000000000..a490a8161a --- /dev/null +++ b/packages/platform/types/calendars/inputs/index.ts @@ -0,0 +1,2 @@ +export * from "./create-calendar-credentials.input"; +export * from "./busy-times.input";