* 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
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<AllowedAuthMethod[]>();
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string>("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);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./inputs";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./create-calendar-credentials.input";
|
||||
export * from "./busy-times.input";
|
||||
Reference in New Issue
Block a user