Skip to content

Commit 4cc1bc6

Browse files
authored
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
1 parent 49a2c60 commit 4cc1bc6

20 files changed

Lines changed: 216 additions & 67 deletions

File tree

apps/api/v2/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ $ yarn run test:cov
112112
### Guards
113113
1. In case a guard would return "false" for "canActivate" instead throw ForbiddenException with an error message containing guard name and the error.
114114
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".
115+
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
116+
all auth methods are allowed.
115117

116118
## Support
117119

apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
22
import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller";
33
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
4+
import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service";
45
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
56
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
67
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
@@ -67,6 +68,7 @@ import { Module } from "@nestjs/common";
6768
SelectedCalendarsRepository,
6869
OrganizationsTeamsRepository,
6970
OrganizationsRepository,
71+
ErrorsBookingsService_2024_08_13,
7072
],
7173
controllers: [BookingsController_2024_08_13],
7274
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],

apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
22
import { CalendarLink } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output";
3+
import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service";
34
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
45
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
56
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
@@ -85,34 +86,19 @@ export class BookingsService_2024_08_13 {
8586
private readonly organizationsTeamsRepository: OrganizationsTeamsRepository,
8687
private readonly organizationsRepository: OrganizationsRepository,
8788
private readonly teamsRepository: TeamsRepository,
88-
private readonly teamsEventTypesRepository: TeamsEventTypesRepository
89+
private readonly teamsEventTypesRepository: TeamsEventTypesRepository,
90+
private readonly errorsBookingsService: ErrorsBookingsService_2024_08_13
8991
) {}
9092

9193
async createBooking(request: Request, body: CreateBookingInput) {
94+
let bookingTeamEventType = false;
9295
try {
9396
const eventType = await this.getBookedEventType(body);
97+
if (eventType?.team) {
98+
bookingTeamEventType = true;
99+
}
94100
if (!eventType) {
95-
if (body.username && body.eventTypeSlug && !body.organizationSlug) {
96-
throw new NotFoundException(
97-
`Event type with slug ${body.eventTypeSlug} belonging to user ${body.username} not found.`
98-
);
99-
}
100-
if (body.username && body.eventTypeSlug && body.organizationSlug) {
101-
throw new NotFoundException(
102-
`Event type with slug ${body.eventTypeSlug} belonging to user ${body.username} within organization ${body.organizationSlug} not found.`
103-
);
104-
}
105-
if (body.teamSlug && body.eventTypeSlug && !body.organizationSlug) {
106-
throw new NotFoundException(
107-
`Event type with slug ${body.eventTypeSlug} belonging to team ${body.teamSlug} not found.`
108-
);
109-
}
110-
if (body.teamSlug && body.eventTypeSlug && body.organizationSlug) {
111-
throw new NotFoundException(
112-
`Event type with slug ${body.eventTypeSlug} belonging to team ${body.teamSlug} within organization ${body.organizationSlug} not found.`
113-
);
114-
}
115-
throw new NotFoundException(`Event type with id ${body.eventTypeId} not found.`);
101+
this.errorsBookingsService.handleEventTypeToBeBookedNotFound(body);
116102
}
117103

118104
body.eventTypeId = eventType.id;
@@ -138,18 +124,7 @@ export class BookingsService_2024_08_13 {
138124

139125
return await this.createRegularBooking(request, body, eventType);
140126
} catch (error) {
141-
if (error instanceof Error) {
142-
if (error.message === "no_available_users_found_error") {
143-
throw new BadRequestException("User either already has booking at this time or is not available");
144-
} else if (error.message === "booking_time_out_of_bounds_error") {
145-
throw new BadRequestException(
146-
`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`
147-
);
148-
} else if (error.message === "Attempting to book a meeting in the past.") {
149-
throw new BadRequestException("Attempting to book a meeting in the past.");
150-
}
151-
}
152-
throw error;
127+
this.errorsBookingsService.handleBookingError(error, bookingTeamEventType);
153128
}
154129
}
155130

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
2+
import { Logger } from "@nestjs/common";
3+
4+
import { CreateBookingInput } from "@calcom/platform-types";
5+
6+
@Injectable()
7+
export class ErrorsBookingsService_2024_08_13 {
8+
private readonly logger = new Logger("ErrorsBookingsService_2024_08_13");
9+
10+
handleEventTypeToBeBookedNotFound(body: CreateBookingInput): never {
11+
if (body.username && body.eventTypeSlug && !body.organizationSlug) {
12+
throw new NotFoundException(
13+
`Event type with slug ${body.eventTypeSlug} belonging to user ${body.username} not found.`
14+
);
15+
}
16+
if (body.username && body.eventTypeSlug && body.organizationSlug) {
17+
throw new NotFoundException(
18+
`Event type with slug ${body.eventTypeSlug} belonging to user ${body.username} within organization ${body.organizationSlug} not found.`
19+
);
20+
}
21+
if (body.teamSlug && body.eventTypeSlug && !body.organizationSlug) {
22+
throw new NotFoundException(
23+
`Event type with slug ${body.eventTypeSlug} belonging to team ${body.teamSlug} not found.`
24+
);
25+
}
26+
if (body.teamSlug && body.eventTypeSlug && body.organizationSlug) {
27+
throw new NotFoundException(
28+
`Event type with slug ${body.eventTypeSlug} belonging to team ${body.teamSlug} within organization ${body.organizationSlug} not found.`
29+
);
30+
}
31+
throw new NotFoundException(`Event type with id ${body.eventTypeId} not found.`);
32+
}
33+
34+
handleBookingError(error: unknown, bookingTeamEventType: boolean): never {
35+
const hostsUnavaile = "One of the hosts either already has booking at this time or is not available";
36+
37+
if (error instanceof Error) {
38+
if (error.message === "no_available_users_found_error") {
39+
if (bookingTeamEventType) {
40+
throw new BadRequestException(hostsUnavaile);
41+
}
42+
throw new BadRequestException("User either already has booking at this time or is not available");
43+
} else if (error.message === "booking_time_out_of_bounds_error") {
44+
throw new BadRequestException(
45+
`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`
46+
);
47+
} else if (error.message === "Attempting to book a meeting in the past.") {
48+
throw new BadRequestException("Attempting to book a meeting in the past.");
49+
} else if (error.message === "hosts_unavailable_for_booking") {
50+
throw new BadRequestException(hostsUnavaile);
51+
}
52+
}
53+
throw error;
54+
}
55+
}

apps/api/v2/src/ee/calendars/controllers/calendars.controller.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { IcsFeedService } from "@/ee/calendars/services/ics-feed.service";
1515
import { OutlookService } from "@/ee/calendars/services/outlook.service";
1616
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
1717
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
18+
import { ApiAuthGuardOnlyAllow } from "@/modules/auth/decorators/api-auth-guard-only-allow.decorator";
1819
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
1920
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
2021
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
@@ -51,7 +52,7 @@ import {
5152
APPLE_CALENDAR,
5253
CREDENTIAL_CALENDARS,
5354
} from "@calcom/platform-constants";
54-
import { ApiResponse, CalendarBusyTimesInput } from "@calcom/platform-types";
55+
import { ApiResponse, CalendarBusyTimesInput, CreateCalendarCredentialsInput } from "@calcom/platform-types";
5556

5657
@Controller({
5758
path: "/v2/calendars",
@@ -135,6 +136,7 @@ export class CalendarsController {
135136
name: "calendar",
136137
})
137138
@UseGuards(ApiAuthGuard)
139+
@ApiAuthGuardOnlyAllow(["API_KEY", "ACCESS_TOKEN"])
138140
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
139141
@Get("/:calendar/connect")
140142
@HttpCode(HttpStatus.OK)
@@ -173,7 +175,7 @@ export class CalendarsController {
173175
@Get("/:calendar/save")
174176
@HttpCode(HttpStatus.OK)
175177
@Redirect(undefined, 301)
176-
@ApiOperation({ summary: "Save an oAuth calendar credentials" })
178+
@ApiOperation({ summary: "Save Google or Outlook calendar credentials" })
177179
async save(
178180
@Query("state") state: string,
179181
@Query("code") code: string,
@@ -221,11 +223,11 @@ export class CalendarsController {
221223
@UseGuards(ApiAuthGuard)
222224
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
223225
@Post("/:calendar/credentials")
224-
@ApiOperation({ summary: "Sync credentials" })
226+
@ApiOperation({ summary: "Save Apple calendar credentials" })
225227
async syncCredentials(
226228
@GetUser() user: User,
227229
@Param("calendar") calendar: string,
228-
@Body() body: { username: string; password: string }
230+
@Body() body: CreateCalendarCredentialsInput
229231
): Promise<{ status: string }> {
230232
const { username, password } = body;
231233

apps/api/v2/src/ee/calendars/services/apple-calendar.service.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ export class AppleCalendarService implements CredentialSyncCalendarApp {
6060
}
6161

6262
async saveCalendarCredentials(userId: number, userEmail: string, username: string, password: string) {
63-
if (username.length <= 1 || password.length <= 1)
63+
if (!username || !password || username.length <= 1 || password.length <= 1) {
6464
throw new BadRequestException(`Username or password cannot be empty`);
65+
}
6566

6667
const existingAppleCalendarCredentials = await this.credentialRepository.getAllUserCredentialsByTypeAndId(
6768
APPLE_CALENDAR_TYPE,

apps/api/v2/src/ee/gcal/gcal.controller.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d
99
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
1010
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
1111
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
12-
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
13-
import { TokensRepository } from "@/modules/tokens/tokens.repository";
1412
import {
1513
BadRequestException,
1614
Controller,
@@ -48,8 +46,6 @@ export class GcalController {
4846

4947
constructor(
5048
private readonly credentialRepository: CredentialsRepository,
51-
private readonly tokensRepository: TokensRepository,
52-
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
5349
private readonly config: ConfigService,
5450
private readonly gcalService: GCalService,
5551
private readonly calendarsService: CalendarsService
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Reflector } from "@nestjs/core";
2+
3+
export type AllowedAuthMethod = "OAUTH_CLIENT_CREDENTIALS" | "API_KEY" | "ACCESS_TOKEN" | "NEXT_AUTH";
4+
5+
export const ApiAuthGuardOnlyAllow = Reflector.createDecorator<AllowedAuthMethod[]>();
Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
1+
import { ExecutionContext, Inject } from "@nestjs/common";
2+
import { Reflector } from "@nestjs/core";
13
import { AuthGuard } from "@nestjs/passport";
24

5+
import { ApiAuthGuardOnlyAllow } from "../../decorators/api-auth-guard-only-allow.decorator";
6+
37
export class ApiAuthGuard extends AuthGuard("api-auth") {
4-
constructor() {
8+
constructor(@Inject(Reflector) private readonly reflector: Reflector) {
59
super();
610
}
11+
12+
getRequest(context: ExecutionContext) {
13+
const request = context.switchToHttp().getRequest();
14+
15+
const allowedMethods = this.reflector.get(ApiAuthGuardOnlyAllow, context.getHandler());
16+
request.allowedAuthMethods = allowedMethods;
17+
18+
return request;
19+
}
720
}

apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@ import { getToken } from "next-auth/jwt";
1818

1919
import { INVALID_ACCESS_TOKEN, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
2020

21+
import type { AllowedAuthMethod } from "../../decorators/api-auth-guard-only-allow.decorator";
22+
2123
export type ApiAuthGuardUser = UserWithProfile & { isSystemAdmin: boolean };
2224
export type ApiAuthGuardRequest = Request & {
2325
authMethod: AuthMethods;
2426
organizationId: number | null;
2527
user: ApiAuthGuardUser;
28+
allowedAuthMethods?: AllowedAuthMethod[];
2629
};
2730
export const NO_AUTH_PROVIDED_MESSAGE =
2831
"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")
4952
const oAuthClientId = params.clientId || request.get(X_CAL_CLIENT_ID);
5053
const bearerToken = request.get("Authorization")?.replace("Bearer ", "");
5154

52-
if (oAuthClientId && oAuthClientSecret) {
55+
const allowedMethods = request.allowedAuthMethods;
56+
const noSpecificAuthExpected = !allowedMethods || !allowedMethods.length;
57+
const oAuthAllowed = noSpecificAuthExpected || allowedMethods.includes("OAUTH_CLIENT_CREDENTIALS");
58+
const apiKeyAllowed = noSpecificAuthExpected || allowedMethods.includes("API_KEY");
59+
const accessTokenAllowed = noSpecificAuthExpected || allowedMethods.includes("ACCESS_TOKEN");
60+
const nextAuthAllowed = noSpecificAuthExpected || allowedMethods.includes("NEXT_AUTH");
61+
62+
if (oAuthClientId && oAuthClientSecret && oAuthAllowed) {
5363
request.authMethod = AuthMethods["OAUTH_CLIENT"];
5464
return await this.authenticateOAuthClient(oAuthClientId, oAuthClientSecret, request);
5565
}
5666

57-
if (bearerToken) {
67+
if (bearerToken && (apiKeyAllowed || accessTokenAllowed)) {
5868
const requestOrigin = request.get("Origin");
5969
request.authMethod = isApiKey(bearerToken, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")
6070
? AuthMethods["API_KEY"]
@@ -64,13 +74,18 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
6474

6575
const nextAuthSecret = this.config.get("next.authSecret", { infer: true });
6676
const nextAuthToken = await getToken({ req: request, secret: nextAuthSecret });
67-
68-
if (nextAuthToken) {
77+
if (nextAuthToken && nextAuthAllowed) {
6978
request.authMethod = AuthMethods["NEXT_AUTH"];
7079
return await this.authenticateNextAuth(nextAuthToken, request);
7180
}
7281

73-
throw new UnauthorizedException(`ApiAuthStrategy - ${NO_AUTH_PROVIDED_MESSAGE}`);
82+
const noAuthProvided = !oAuthClientId && !oAuthClientSecret && !bearerToken && !nextAuthToken;
83+
if (noAuthProvided) {
84+
throw new UnauthorizedException(`ApiAuthStrategy - ${NO_AUTH_PROVIDED_MESSAGE}`);
85+
}
86+
throw new UnauthorizedException(
87+
`ApiAuthStrategy - Invalid authentication method. Please provide one of the allowed methods: ${allowedMethods}`
88+
);
7489
} catch (err) {
7590
if (err instanceof Error) {
7691
return this.error(err);

0 commit comments

Comments
 (0)