diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts index f546590a37..02af38d795 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts @@ -25,9 +25,11 @@ import { import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController, + ApiExcludeEndpoint as DocsExcludeEndpoint, ApiOperation as DocsOperation, ApiOkResponse as DocsOkResponse, ApiBadRequestResponse as DocsBadRequestResponse, + ApiHeader as DocsHeader, } from "@nestjs/swagger"; import { Response as ExpressResponse } from "express"; @@ -37,8 +39,7 @@ import { SUCCESS_STATUS, X_CAL_SECRET_KEY } from "@calcom/platform-constants"; path: "/v2/oauth/:clientId", version: API_VERSIONS_VALUES, }) -@DocsExcludeController(getEnv("NODE_ENV") === "production") -@DocsTags("OAuth - development only") +@DocsTags("OAuth") export class OAuthFlowController { constructor( private readonly oauthClientRepository: OAuthClientRepository, @@ -62,6 +63,7 @@ export class OAuthFlowController { description: "Bad request if the OAuth client is not found, if the redirect URI is invalid, or if the user has already authorized the client.", }) + @DocsExcludeEndpoint(getEnv("NODE_ENV") === "production") async authorize( @Param("clientId") clientId: string, @Body() body: OAuthAuthorizeInput, @@ -108,6 +110,7 @@ export class OAuthFlowController { description: "Bad request if the authorization code is missing, invalid, or if the client ID and secret do not match.", }) + @DocsExcludeEndpoint(getEnv("NODE_ENV") === "production") async exchange( @Headers("Authorization") authorization: string, @Param("clientId") clientId: string, @@ -138,6 +141,12 @@ export class OAuthFlowController { @Post("/refresh") @HttpCode(HttpStatus.OK) @UseGuards(ApiAuthGuard) + @DocsTags("Managed users") + @DocsHeader({ + name: X_CAL_SECRET_KEY, + description: "OAuth client secret key.", + required: true, + }) async refreshAccessToken( @Param("clientId") clientId: string, @Headers(X_CAL_SECRET_KEY) secretKey: string, diff --git a/apps/api/v2/src/modules/oauth-clients/inputs/refresh-token.input.ts b/apps/api/v2/src/modules/oauth-clients/inputs/refresh-token.input.ts index 1eb25d3950..7e2cacfdac 100644 --- a/apps/api/v2/src/modules/oauth-clients/inputs/refresh-token.input.ts +++ b/apps/api/v2/src/modules/oauth-clients/inputs/refresh-token.input.ts @@ -1,6 +1,8 @@ +import { ApiProperty as DocsProperty } from "@nestjs/swagger"; import { IsString } from "class-validator"; export class RefreshTokenInput { @IsString() + @DocsProperty({ description: "Managed user's refresh token." }) refreshToken!: string; } diff --git a/apps/api/v2/src/modules/slots/controllers/slots.controller.ts b/apps/api/v2/src/modules/slots/controllers/slots.controller.ts index fa0cca5341..9c4f321e6d 100644 --- a/apps/api/v2/src/modules/slots/controllers/slots.controller.ts +++ b/apps/api/v2/src/modules/slots/controllers/slots.controller.ts @@ -1,7 +1,7 @@ import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { SlotsService } from "@/modules/slots/services/slots.service"; import { Query, Body, Controller, Get, Delete, Post, Req, Res } from "@nestjs/common"; -import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; +import { ApiTags as DocsTags, ApiCreatedResponse, ApiOkResponse, ApiOperation } from "@nestjs/swagger"; import { Response as ExpressResponse, Request as ExpressRequest } from "express"; import { SUCCESS_STATUS } from "@calcom/platform-constants"; @@ -19,6 +19,21 @@ export class SlotsController { constructor(private readonly slotsService: SlotsService) {} @Post("/reserve") + @ApiCreatedResponse({ + description: "Successful response returning uid of reserved slot.", + schema: { + type: "object", + properties: { + status: { type: "string", example: "success" }, + data: { + type: "object", + properties: { + uid: { type: "string", example: "e2a7bcf9-cc7b-40a0-80d3-657d391775a6" }, + }, + }, + }, + }, + }) @ApiOperation({ summary: "Reserve a slot" }) async reserveSlot( @Body() body: ReserveSlotInput, @@ -35,6 +50,15 @@ export class SlotsController { } @Delete("/selected-slot") + @ApiOkResponse({ + description: "Response deleting reserved slot by uid.", + schema: { + type: "object", + properties: { + status: { type: "string", example: "success" }, + }, + }, + }) @ApiOperation({ summary: "Delete a selected slot" }) async deleteSelectedSlot( @Query() params: RemoveSelectedSlotInput, @@ -50,6 +74,45 @@ export class SlotsController { } @Get("/available") + @ApiOkResponse({ + description: "Available time slots retrieved successfully", + schema: { + type: "object", + properties: { + status: { type: "string", example: "success" }, + data: { + type: "object", + properties: { + slots: { + type: "object", + additionalProperties: { + type: "array", + items: { + type: "object", + properties: { + time: { type: "string", format: "date-time", example: "2024-09-25T08:00:00.000Z" }, + }, + }, + }, + }, + }, + }, + }, + example: { + status: "success", + data: { + slots: { + "2024-09-25": [{ time: "2024-09-25T08:00:00.000Z" }, { time: "2024-09-25T08:15:00.000Z" }], + "2024-09-26": [ + { time: "2024-09-26T08:00:00.000Z" }, + { time: "2024-09-26T08:15:00.000Z" }, + { time: "2024-09-26T08:30:00.000Z" }, + ], + }, + }, + }, + }, + }) @ApiOperation({ summary: "Get available slots" }) async getAvailableSlots( @Query() query: GetAvailableSlotsInput, diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 17f6255ac0..7553780068 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -4108,6 +4108,147 @@ ] } }, + "/v2/oauth/{clientId}/authorize": { + "post": { + "operationId": "OAuthFlowController_authorize", + "summary": "Authorize an OAuth client", + "description": "Redirects the user to the specified 'redirect_uri' with an authorization code in query parameter if the client is authorized successfully. The code is then exchanged for access and refresh tokens via the `/exchange` endpoint.", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthAuthorizeInput" + } + } + } + }, + "responses": { + "200": { + "description": "The user is redirected to the 'redirect_uri' with an authorization code in query parameter e.g. `redirectUri?code=secretcode.`" + }, + "400": { + "description": "Bad request if the OAuth client is not found, if the redirect URI is invalid, or if the user has already authorized the client." + } + }, + "tags": [ + "oauth" + ] + } + }, + "/v2/oauth/{clientId}/exchange": { + "post": { + "operationId": "OAuthFlowController_exchange", + "summary": "Exchange authorization code for access tokens", + "description": "Exchanges the authorization code received from the `/authorize` endpoint for access and refresh tokens. The authorization code should be provided in the 'Authorization' header prefixed with 'Bearer '.", + "parameters": [ + { + "name": "Authorization", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExchangeAuthorizationCodeInput" + } + } + } + }, + "responses": { + "200": { + "description": "Successfully exchanged authorization code for access and refresh tokens.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeysResponseDto" + } + } + } + }, + "400": { + "description": "Bad request if the authorization code is missing, invalid, or if the client ID and secret do not match." + } + }, + "tags": [ + "oauth" + ] + } + }, + "/v2/oauth/{clientId}/refresh": { + "post": { + "operationId": "OAuthFlowController_refreshAccessToken", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-secret-key", + "required": true, + "in": "header", + "description": "OAuth client secret key.", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefreshTokenInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeysResponseDto" + } + } + } + } + }, + "tags": [ + "oauth", + "Managed users" + ] + } + }, "/v2/oauth-clients": { "post": { "operationId": "OAuthClientsController_createOAuthClient", @@ -4306,145 +4447,6 @@ ] } }, - "/v2/oauth/{clientId}/authorize": { - "post": { - "operationId": "OAuthFlowController_authorize", - "summary": "Authorize an OAuth client", - "description": "Redirects the user to the specified 'redirect_uri' with an authorization code in query parameter if the client is authorized successfully. The code is then exchanged for access and refresh tokens via the `/exchange` endpoint.", - "parameters": [ - { - "name": "clientId", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OAuthAuthorizeInput" - } - } - } - }, - "responses": { - "200": { - "description": "The user is redirected to the 'redirect_uri' with an authorization code in query parameter e.g. `redirectUri?code=secretcode.`" - }, - "400": { - "description": "Bad request if the OAuth client is not found, if the redirect URI is invalid, or if the user has already authorized the client." - } - }, - "tags": [ - "OAuth - development only" - ] - } - }, - "/v2/oauth/{clientId}/exchange": { - "post": { - "operationId": "OAuthFlowController_exchange", - "summary": "Exchange authorization code for access tokens", - "description": "Exchanges the authorization code received from the `/authorize` endpoint for access and refresh tokens. The authorization code should be provided in the 'Authorization' header prefixed with 'Bearer '.", - "parameters": [ - { - "name": "Authorization", - "required": true, - "in": "header", - "schema": { - "type": "string" - } - }, - { - "name": "clientId", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ExchangeAuthorizationCodeInput" - } - } - } - }, - "responses": { - "200": { - "description": "Successfully exchanged authorization code for access and refresh tokens.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeysResponseDto" - } - } - } - }, - "400": { - "description": "Bad request if the authorization code is missing, invalid, or if the client ID and secret do not match." - } - }, - "tags": [ - "OAuth - development only" - ] - } - }, - "/v2/oauth/{clientId}/refresh": { - "post": { - "operationId": "OAuthFlowController_refreshAccessToken", - "parameters": [ - { - "name": "clientId", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - }, - { - "name": "x-cal-secret-key", - "required": true, - "in": "header", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RefreshTokenInput" - } - } - } - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeysResponseDto" - } - } - } - } - }, - "tags": [ - "OAuth - development only" - ] - } - }, "/v2/schedules": { "post": { "operationId": "SchedulesController_2024_06_11_createSchedule", @@ -4826,11 +4828,26 @@ }, "responses": { "201": { - "description": "", + "description": "Successful response returning uid of reserved slot.", "content": { "application/json": { "schema": { - "type": "object" + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success" + }, + "data": { + "type": "object", + "properties": { + "uid": { + "type": "string", + "example": "e2a7bcf9-cc7b-40a0-80d3-657d391775a6" + } + } + } + } } } } @@ -4845,14 +4862,31 @@ "delete": { "operationId": "SlotsController_deleteSelectedSlot", "summary": "Delete a selected slot", - "parameters": [], + "parameters": [ + { + "name": "uid", + "required": true, + "in": "query", + "description": "Unique identifier for the slot to be removed.", + "example": "e2a7bcf9-cc7b-40a0-80d3-657d391775a6", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { - "description": "", + "description": "Response deleting reserved slot by uid.", "content": { "application/json": { "schema": { - "type": "object" + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success" + } + } } } } @@ -4867,14 +4901,128 @@ "get": { "operationId": "SlotsController_getAvailableSlots", "summary": "Get available slots", - "parameters": [], + "parameters": [ + { + "name": "startTime", + "required": true, + "in": "query", + "description": "Start date string starting from which to fetch slots in UTC timezone.", + "example": "2022-06-14T00:00:00.000Z", + "schema": { + "type": "string" + } + }, + { + "name": "endTime", + "required": true, + "in": "query", + "description": "End date string until which to fetch slots in UTC timezone.", + "example": "2022-06-14T23:59:59.999Z", + "schema": { + "type": "string" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "query", + "description": "Event Type ID for which slots are being fetched.", + "example": 100, + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeSlug", + "required": true, + "in": "query", + "description": "Slug of the event type for which slots are being fetched.", + "schema": { + "type": "string" + } + }, + { + "name": "usernameList", + "required": true, + "in": "query", + "description": "Only for dynamic events - list of usernames for which slots are being fetched.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "duration", + "required": true, + "in": "query", + "description": "Only for dynamic events - length of returned slots.", + "schema": { + "type": "number" + } + } + ], "responses": { "200": { - "description": "", + "description": "Available time slots retrieved successfully", "content": { "application/json": { "schema": { - "type": "object" + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success" + }, + "data": { + "type": "object", + "properties": { + "slots": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "object", + "properties": { + "time": { + "type": "string", + "format": "date-time", + "example": "2024-09-25T08:00:00.000Z" + } + } + } + } + } + } + } + }, + "example": { + "status": "success", + "data": { + "slots": { + "2024-09-25": [ + { + "time": "2024-09-25T08:00:00.000Z" + }, + { + "time": "2024-09-25T08:15:00.000Z" + } + ], + "2024-09-26": [ + { + "time": "2024-09-26T08:00:00.000Z" + }, + { + "time": "2024-09-26T08:15:00.000Z" + }, + { + "time": "2024-09-26T08:30:00.000Z" + } + ] + } + } + } } } } @@ -5693,7 +5841,8 @@ "type": "object", "properties": { "refreshToken": { - "type": "string" + "type": "string", + "description": "Managed user's refresh token." } }, "required": [ @@ -12468,7 +12617,32 @@ }, "ReserveSlotInput": { "type": "object", - "properties": {} + "properties": { + "eventTypeId": { + "type": "number", + "description": "Event Type ID for which timeslot is being reserved.", + "example": 100 + }, + "slotUtcStartDate": { + "type": "string", + "description": "Start date of the slot in UTC timezone.", + "example": "2022-06-14T00:00:00.000Z" + }, + "slotUtcEndDate": { + "type": "string", + "description": "End date of the slot in UTC timezone.", + "example": "2022-06-14T00:30:00.000Z" + }, + "bookingUid": { + "type": "string", + "description": "Optional but only for events with seats. Used to retrieve booking of a seated event." + } + }, + "required": [ + "eventTypeId", + "slotUtcStartDate", + "slotUtcEndDate" + ] }, "UserWebhookOutputDto": { "type": "object", diff --git a/packages/platform/types/slots.ts b/packages/platform/types/slots.ts index d8844f6bbb..3c6b1009b4 100644 --- a/packages/platform/types/slots.ts +++ b/packages/platform/types/slots.ts @@ -1,25 +1,39 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { Transform } from "class-transformer"; import { IsArray, IsBoolean, IsDateString, IsInt, IsNumber, IsOptional, IsString } from "class-validator"; export class GetAvailableSlotsInput { @IsDateString() + @ApiProperty({ + description: "Start date string starting from which to fetch slots in UTC timezone.", + example: "2022-06-14T00:00:00.000Z", + }) startTime!: string; @IsDateString() + @ApiProperty({ + description: "End date string until which to fetch slots in UTC timezone.", + example: "2022-06-14T23:59:59.999Z", + }) endTime!: string; @Transform(({ value }: { value: string }) => value && parseInt(value)) @IsNumber() @IsOptional() + @ApiProperty({ description: "Event Type ID for which slots are being fetched.", example: 100 }) eventTypeId?: number; @IsString() @IsOptional() + @ApiProperty({ description: "Slug of the event type for which slots are being fetched." }) eventTypeSlug?: string; @IsArray() @IsString({ each: true }) @IsOptional() + @ApiProperty({ + description: "Only for dynamic events - list of usernames for which slots are being fetched.", + }) usernameList?: string[]; @IsBoolean() @@ -29,6 +43,7 @@ export class GetAvailableSlotsInput { @Transform(({ value }: { value: string }) => value && parseInt(value)) @IsNumber() @IsOptional() + @ApiProperty({ description: "Only for dynamic events - length of returned slots." }) duration?: number; @IsOptional() @@ -47,20 +62,34 @@ export class GetAvailableSlotsInput { export class RemoveSelectedSlotInput { @IsString() @IsOptional() + @ApiProperty({ + description: "Unique identifier for the slot to be removed.", + example: "e2a7bcf9-cc7b-40a0-80d3-657d391775a6", + required: true, + }) uid?: string; } export class ReserveSlotInput { @IsInt() + @ApiProperty({ description: "Event Type ID for which timeslot is being reserved.", example: 100 }) eventTypeId!: number; @IsDateString() + @ApiProperty({ + description: "Start date of the slot in UTC timezone.", + example: "2022-06-14T00:00:00.000Z", + }) slotUtcStartDate!: string; @IsDateString() + @ApiProperty({ description: "End date of the slot in UTC timezone.", example: "2022-06-14T00:30:00.000Z" }) slotUtcEndDate!: string; @IsString() @IsOptional() + @ApiPropertyOptional({ + description: "Optional but only for events with seats. Used to retrieve booking of a seated event.", + }) bookingUid?: string; }