From 1a70f83d34f14955ddb03d895d4c46117ac83a6b Mon Sep 17 00:00:00 2001 From: Lauris Skraucis Date: Wed, 14 May 2025 09:11:35 +0200 Subject: [PATCH] fix: v2 sentry errors (#21214) * fix: PATCH, POST event-types Address location must have an address * fix: GET event-types Address location must have an address * fix: POST /v2/bookings Address location must have an address * refactor: GET /v2/slots Invalid time range given. * fix: POST v2/event-types Cannot read properties of null (reading 'type') * fix: POST /v2/bookings/:bookingUid/mark-absent Failed to update no-show status --- .../2024-08-13/services/bookings.service.ts | 15 +++++++ .../controllers/slots.controller.ts | 39 +++++++++++------ .../services/slots.service.ts | 43 +++++++++++-------- .../transformers/internal-to-api/locations.ts | 42 ++++++++++++------ .../2024-08-13/inputs/location.input.ts | 4 +- .../inputs/locations.input.ts | 11 ++++- .../slots/slots-2024-04-15/inputs/index.ts | 4 +- .../inputs/get-slots.input.ts | 4 +- 8 files changed, 111 insertions(+), 51 deletions(-) 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 503a10a180..59fae0af57 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 @@ -20,6 +20,7 @@ import { UsersRepository, UserWithProfile } from "@/modules/users/users.reposito import { ConflictException, Injectable, Logger, NotFoundException } from "@nestjs/common"; import { BadRequestException } from "@nestjs/common"; import { Request } from "express"; +import { DateTime } from "luxon"; import { z } from "zod"; import { @@ -594,6 +595,20 @@ export class BookingsService_2024_08_13 { async markAbsent(bookingUid: string, bookingOwnerId: number, body: MarkAbsentBookingInput_2024_08_13) { const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body); const bookingBefore = await this.bookingsRepository.getByUid(bookingUid); + + if (!bookingBefore) { + throw new NotFoundException(`Booking with uid=${bookingUid} not found.`); + } + + const nowUtc = DateTime.utc(); + const bookingStartTimeUtc = DateTime.fromJSDate(bookingBefore.startTime, { zone: "utc" }); + + if (nowUtc < bookingStartTimeUtc) { + throw new BadRequestException( + `Bookings can only be marked as absent after their scheduled start time. Current time in UTC+0: ${nowUtc.toISO()}, Booking start time in UTC+0: ${bookingStartTimeUtc.toISO()}` + ); + } + const platformClientParams = bookingBefore?.eventTypeId ? await this.platformBookingsService.getOAuthClientParams(bookingBefore.eventTypeId) : undefined; diff --git a/apps/api/v2/src/modules/slots/slots-2024-04-15/controllers/slots.controller.ts b/apps/api/v2/src/modules/slots/slots-2024-04-15/controllers/slots.controller.ts index 501503f915..341af28a25 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-04-15/controllers/slots.controller.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-04-15/controllers/slots.controller.ts @@ -1,7 +1,7 @@ import { SlotsOutputService_2024_04_15 } from "@/modules/slots/slots-2024-04-15/services/slots-output.service"; import type { RangeSlots, TimeSlots } from "@/modules/slots/slots-2024-04-15/services/slots-output.service"; import { SlotsService_2024_04_15 } from "@/modules/slots/slots-2024-04-15/services/slots.service"; -import { Query, Body, Controller, Get, Delete, Post, Req, Res } from "@nestjs/common"; +import { Query, Body, Controller, Get, Delete, Post, Req, Res, BadRequestException } from "@nestjs/common"; import { ApiExcludeController as DocsExcludeController } from "@nestjs/swagger"; import { ApiTags as DocsTags, ApiCreatedResponse, ApiOkResponse, ApiOperation } from "@nestjs/swagger"; import { Response as ExpressResponse, Request as ExpressRequest } from "express"; @@ -157,6 +157,7 @@ export class SlotsController_2024_04_15 { @Query() query: GetAvailableSlotsInput_2024_04_15, @Req() req: ExpressRequest ): Promise> { + try { const isTeamEvent = query.isTeamEvent === undefined ? await this.slotsService.checkIfIsTeamEvent(query.eventTypeId) @@ -171,19 +172,29 @@ export class SlotsController_2024_04_15 { }, }); - const { slots } = await this.slotsOutputService.getOutputSlots( - availableSlots, - query.duration, - query.eventTypeId, - query.slotFormat, - query.timeZone - ); + const { slots } = await this.slotsOutputService.getOutputSlots( + availableSlots, + query.duration, + query.eventTypeId, + query.slotFormat, + query.timeZone + ); - return { - data: { - slots, - }, - status: SUCCESS_STATUS, - }; + return { + data: { + slots, + }, + status: SUCCESS_STATUS, + }; + } catch (error) { + if (error instanceof Error) { + if (error.message.includes("Invalid time range given")) { + throw new BadRequestException( + "Invalid time range given - check the 'startTime' and 'endTime' query parameters." + ); + } + } + throw error; + } } } diff --git a/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.ts b/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.ts index dccd6d9f4c..46c231303d 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.ts @@ -42,24 +42,33 @@ export class SlotsService_2024_09_04 { ) {} async getAvailableSlots(query: GetSlotsInput_2024_09_04) { - const queryTransformed = await this.slotsInputService.transformGetSlotsQuery(query); + try { + const queryTransformed = await this.slotsInputService.transformGetSlotsQuery(query); + const availableSlots: TimeSlots = await getAvailableSlots({ + input: { + ...queryTransformed, + }, + ctx: {}, + }); + const formatted = await this.slotsOutputService.getAvailableSlots( + availableSlots, + queryTransformed.eventTypeId, + queryTransformed.duration, + query.format, + queryTransformed.timeZone + ); - const availableSlots: TimeSlots = await getAvailableSlots({ - input: { - ...queryTransformed, - }, - ctx: {}, - }); - - const formatted = await this.slotsOutputService.getAvailableSlots( - availableSlots, - queryTransformed.eventTypeId, - queryTransformed.duration, - query.format, - queryTransformed.timeZone - ); - - return formatted; + return formatted; + } catch (error) { + if (error instanceof Error) { + if (error.message.includes("Invalid time range given")) { + throw new BadRequestException( + "Invalid time range given - check the 'start' and 'end' query parameters." + ); + } + } + throw error; + } } async reserveSlot(input: ReserveSlotInput_2024_09_04, authUserId?: number) { diff --git a/packages/lib/event-types/transformers/internal-to-api/locations.ts b/packages/lib/event-types/transformers/internal-to-api/locations.ts index fa3f1cdfe5..83d2ae47db 100644 --- a/packages/lib/event-types/transformers/internal-to-api/locations.ts +++ b/packages/lib/event-types/transformers/internal-to-api/locations.ts @@ -9,6 +9,7 @@ import type { OutputLinkLocation_2024_06_14, OutputPhoneLocation_2024_06_14, OutputUnknownLocation_2024_06_14, + OutputLocation_2024_06_14, } from "@calcom/platform-types"; import type { InternalLocation } from "../internal/locations"; @@ -50,64 +51,73 @@ export function transformLocationsInternalToApi(internalLocations: InternalLocat return []; } - return internalLocations.map((location) => { + const apiLocations: OutputLocation_2024_06_14[] = []; + + for (const location of internalLocations) { switch (location.type) { case "inPerson": { if (!location.address) { - throw new Error("Address location must have an address"); + continue; } const addressLocation: OutputAddressLocation_2024_06_14 = { type: "address", address: location.address, public: location.displayLocationPublicly, }; - return addressLocation; + apiLocations.push(addressLocation); + break; } case "attendeeInPerson": { const attendeeAddressLocation: OutputAttendeeAddressLocation_2024_06_14 = { type: "attendeeAddress", }; - return attendeeAddressLocation; + apiLocations.push(attendeeAddressLocation); + break; } case "link": { if (!location.link) { - throw new Error("Link location must have a link"); + continue; } const linkLocation: OutputLinkLocation_2024_06_14 = { type: "link", link: location.link, public: location.displayLocationPublicly, }; - return linkLocation; + apiLocations.push(linkLocation); + break; } case "userPhone": { if (!location.hostPhoneNumber) { - throw new Error("Phone location must have a phone number"); + continue; } const phoneLocation: OutputPhoneLocation_2024_06_14 = { type: "phone", phone: location.hostPhoneNumber, public: location.displayLocationPublicly, }; - return phoneLocation; + apiLocations.push(phoneLocation); + break; } case "phone": { const attendeePhoneLocation: OutputAttendeePhoneLocation_2024_06_14 = { type: "attendeePhone", }; - return attendeePhoneLocation; + apiLocations.push(attendeePhoneLocation); + break; } case "somewhereElse": { const attendeeDefinedLocation: OutputAttendeeDefinedLocation_2024_06_14 = { type: "attendeeDefined", }; - return attendeeDefinedLocation; + apiLocations.push(attendeeDefinedLocation); + break; } case "conferencing": { const conferencingLocation: OutputOrganizersDefaultAppLocation_2024_06_14 = { type: "organizersDefaultApp", }; - return conferencingLocation; + apiLocations.push(conferencingLocation); + break; } default: { const integrationType = internalToApiIntegrationsMapping[location.type]; @@ -116,7 +126,8 @@ export function transformLocationsInternalToApi(internalLocations: InternalLocat type: "unknown", location: JSON.stringify(location), }; - return unknown; + apiLocations.push(unknown); + break; } const integration: OutputIntegrationLocation_2024_06_14 = { type: "integration", @@ -124,8 +135,11 @@ export function transformLocationsInternalToApi(internalLocations: InternalLocat link: location.link, credentialId: location.credentialId, }; - return integration; + apiLocations.push(integration); + break; } } - }); + } + + return apiLocations; } diff --git a/packages/platform/types/bookings/2024-08-13/inputs/location.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/location.input.ts index 57711d6759..6f77d504fd 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/location.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/location.input.ts @@ -1,6 +1,6 @@ import { ApiProperty as DocsProperty } from "@nestjs/swagger"; import { plainToInstance } from "class-transformer"; -import { IsString, IsIn, IsPhoneNumber } from "class-validator"; +import { IsString, IsIn, IsPhoneNumber, MinLength } from "class-validator"; import type { ValidationOptions, ValidatorConstraintInterface } from "class-validator"; import { registerDecorator, validate, ValidatorConstraint } from "class-validator"; @@ -104,6 +104,7 @@ export class BookingInputAttendeeAddressLocation_2024_08_13 { type!: "attendeeAddress"; @IsString() + @MinLength(1) @DocsProperty({ example: "123 Example St, City, Country" }) address!: string; } @@ -126,6 +127,7 @@ export class BookingInputAttendeeDefinedLocation_2024_08_13 { type!: "attendeeDefined"; @IsString() + @MinLength(1) @DocsProperty({ example: "321 Example St, City, Country" }) location!: string; } diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/locations.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/locations.input.ts index ef6a9b2a96..e6d280ab69 100644 --- a/packages/platform/types/event-types/event-types_2024_06_14/inputs/locations.input.ts +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/locations.input.ts @@ -1,7 +1,7 @@ import { BadRequestException } from "@nestjs/common"; import { ApiProperty as DocsProperty } from "@nestjs/swagger"; import { plainToInstance } from "class-transformer"; -import { IsString, IsUrl, IsIn, IsPhoneNumber, IsBoolean } from "class-validator"; +import { IsString, IsUrl, IsIn, IsPhoneNumber, IsBoolean, MinLength } from "class-validator"; import type { ValidationOptions, ValidatorConstraintInterface } from "class-validator"; import { registerDecorator, validate, ValidatorConstraint } from "class-validator"; @@ -22,6 +22,7 @@ export class InputAddressLocation_2024_06_14 { type!: "address"; @IsString() + @MinLength(1) @DocsProperty({ example: "123 Example St, City, Country" }) address!: string; @@ -136,6 +137,10 @@ class InputLocationValidator_2024_06_14 implements ValidatorConstraintInterface } for (const location of locations) { + if (!location || typeof location !== "object") { + throw new BadRequestException(`Each object in 'locations' must be an object.`); + } + const { type } = location; if (!type) { throw new BadRequestException(`Each object in 'locations' must have a 'type' property.`); @@ -187,6 +192,10 @@ class InputTeamLocationValidator_2024_06_14 implements ValidatorConstraintInterf } for (const location of locations) { + if (!location || typeof location !== "object") { + throw new BadRequestException(`Each object in 'locations' must be an object.`); + } + const { type } = location; if (!type) { throw new BadRequestException(`Each object in 'locations' must have a 'type' property.`); diff --git a/packages/platform/types/slots/slots-2024-04-15/inputs/index.ts b/packages/platform/types/slots/slots-2024-04-15/inputs/index.ts index cc4f9cd564..cb2b13cc9c 100644 --- a/packages/platform/types/slots/slots-2024-04-15/inputs/index.ts +++ b/packages/platform/types/slots/slots-2024-04-15/inputs/index.ts @@ -15,14 +15,14 @@ import { import { SlotFormat } from "@calcom/platform-enums"; export class GetAvailableSlotsInput_2024_04_15 { - @IsDateString() + @IsDateString({ strict: true }) @ApiProperty({ description: "Start date string starting from which to fetch slots in UTC timezone.", example: "2022-06-14T00:00:00.000Z", }) startTime!: string; - @IsDateString() + @IsDateString({ strict: true }) @ApiProperty({ description: "End date string until which to fetch slots in UTC timezone.", example: "2022-06-14T23:59:59.999Z", diff --git a/packages/platform/types/slots/slots-2024-09-04/inputs/get-slots.input.ts b/packages/platform/types/slots/slots-2024-09-04/inputs/get-slots.input.ts index 8efc8e2493..9168242a22 100644 --- a/packages/platform/types/slots/slots-2024-09-04/inputs/get-slots.input.ts +++ b/packages/platform/types/slots/slots-2024-09-04/inputs/get-slots.input.ts @@ -14,7 +14,7 @@ import { import { SlotFormat } from "@calcom/platform-enums"; export class GetAvailableSlotsInput_2024_09_04 { - @IsDateString() + @IsDateString({ strict: true }) @ApiProperty({ type: String, description: ` @@ -29,7 +29,7 @@ export class GetAvailableSlotsInput_2024_09_04 { }) start!: string; - @IsDateString() + @IsDateString({ strict: true }) @ApiProperty({ type: String, description: `