docs: v2 refresh tokens and slots endpoints swagger (#16788)

* docs: oauth refresh endpoint

* docs: slots reserve and delete

* docs: get slots

* swagger

* fix: reserving slot

* Revert "fix: reserving slot"

This reverts commit 8a3ce8464d5dbf67e58da6d3b1aeb51df97240cb.

* Update oauth-flow.controller.ts
This commit is contained in:
Lauris Skraucis
2024-10-02 08:44:05 +00:00
committed by GitHub
parent 182d0fa228
commit bdea436f8b
5 changed files with 429 additions and 152 deletions
@@ -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,
@@ -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;
}
@@ -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,
+323 -149
View File
@@ -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",
+29
View File
@@ -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;
}