* docs: improve English naturalness in API v2 documentation - Fix awkward API summary 'Find out when is an event type ready to be booked' to 'Get available time slots for an event type' - Correct 'setup' vs 'set up' usage throughout documentation - Fix OAuth capitalization consistency - Improve 'api' to 'API' capitalization in v2 docs - Enhance readability while preserving technical accuracy Fixes unnatural English phrasing in titles, headers, descriptions, and summaries across API v2 documentation files. Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> * docs: add missing articles 'the' in API v2 documentation - Fix 'How to Set Up API' to 'How to Set Up the API' in titles - Add 'the' before 'API' in descriptions and summaries - Fix grammar error 'all you need to is' to 'all you need to do is' - Improve naturalness while maintaining technical accuracy Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> * docs: fix unnatural English in API v2 @ApiOperation summaries - Fix grammatically incorrect 'conferencing apps oauths callback' to 'Conferencing app OAuth callback' - Replace 'ooo' with 'out-of-office' in user-facing summaries for clarity - Ensure consistent sentence case capitalization (only first letter capitalized) - Remove trailing periods from summaries for consistency - Fix awkward phrasing like 'Get by attribute id all of...' to more natural English - Revert 'Introduction to the API v2' back to 'Introduction to API v2' - Improve naturalness while maintaining technical accuracy Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> * Update apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/slots.controller.ts * Update docs/api-reference/v2/openapi.json * docs: fix setup/set up usage - use 'backend setup' (noun) not 'backend set up' (verb) Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> * docs: fix setup/set up verb usage - use 'set up' (verb) not 'setup' (verb) - Fix 'You have to setup' → 'You have to set up' in quickstart.mdx - Fix 'Setup environment variables' → 'Set up environment variables' - Fix 'Setup root of your app' → 'Set up root of your app' - Fix 'prompted to setup' → 'prompted to set up' in setup.mdx Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> * Ran v2 locally to regen the doc json files --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
248 lines
8.6 KiB
TypeScript
248 lines
8.6 KiB
TypeScript
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
|
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
|
import {
|
|
ConferencingAppsOauthUrlOutputDto,
|
|
GetConferencingAppsOauthUrlResponseDto,
|
|
} from "@/modules/conferencing/outputs/get-conferencing-apps-oauth-url";
|
|
import {
|
|
ConferencingAppsOutputResponseDto,
|
|
ConferencingAppOutputResponseDto,
|
|
ConferencingAppsOutputDto,
|
|
DisconnectConferencingAppOutputResponseDto,
|
|
} from "@/modules/conferencing/outputs/get-conferencing-apps.output";
|
|
import { GetDefaultConferencingAppOutputResponseDto } from "@/modules/conferencing/outputs/get-default-conferencing-app.output";
|
|
import { SetDefaultConferencingAppOutputResponseDto } from "@/modules/conferencing/outputs/set-default-conferencing-app.output";
|
|
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
|
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
|
import { HttpService } from "@nestjs/axios";
|
|
import { Logger } from "@nestjs/common";
|
|
import {
|
|
Controller,
|
|
Get,
|
|
Query,
|
|
HttpCode,
|
|
HttpStatus,
|
|
UseGuards,
|
|
Post,
|
|
Param,
|
|
BadRequestException,
|
|
Delete,
|
|
Headers,
|
|
Redirect,
|
|
Req,
|
|
HttpException,
|
|
} from "@nestjs/common";
|
|
import { ConfigService } from "@nestjs/config";
|
|
import { ApiHeader, ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
|
|
import { plainToInstance } from "class-transformer";
|
|
import { Request } from "express";
|
|
|
|
import { GOOGLE_MEET, ZOOM, SUCCESS_STATUS, OFFICE_365_VIDEO, CAL_VIDEO } from "@calcom/platform-constants";
|
|
|
|
export type OAuthCallbackState = {
|
|
accessToken: string;
|
|
teamId?: string;
|
|
orgId?: string;
|
|
fromApp?: boolean;
|
|
returnTo?: string;
|
|
onErrorReturnTo?: string;
|
|
};
|
|
|
|
@Controller({
|
|
path: "/v2/conferencing",
|
|
version: API_VERSIONS_VALUES,
|
|
})
|
|
@DocsTags("Conferencing")
|
|
export class ConferencingController {
|
|
private readonly logger = new Logger("ConferencingController");
|
|
|
|
constructor(
|
|
private readonly conferencingService: ConferencingService,
|
|
private readonly config: ConfigService,
|
|
private readonly httpService: HttpService
|
|
) {}
|
|
|
|
@Post("/:app/connect")
|
|
@HttpCode(HttpStatus.OK)
|
|
@UseGuards(ApiAuthGuard)
|
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
|
@ApiOperation({ summary: "Connect your conferencing application" })
|
|
@ApiParam({
|
|
name: "app",
|
|
description: "Conferencing application type",
|
|
enum: [GOOGLE_MEET],
|
|
required: true,
|
|
})
|
|
async connect(
|
|
@GetUser() user: UserWithProfile,
|
|
@Param("app") app: string
|
|
): Promise<ConferencingAppOutputResponseDto> {
|
|
const credential = await this.conferencingService.connectUserNonOauthApp(app, user.id);
|
|
return { status: SUCCESS_STATUS, data: plainToInstance(ConferencingAppsOutputDto, credential) };
|
|
}
|
|
|
|
@Get("/:app/oauth/auth-url")
|
|
@HttpCode(HttpStatus.OK)
|
|
@UseGuards(ApiAuthGuard)
|
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
|
@ApiOperation({ summary: "Get OAuth conferencing app auth URL" })
|
|
@ApiParam({
|
|
name: "app",
|
|
description: "Conferencing application type",
|
|
enum: [ZOOM, OFFICE_365_VIDEO],
|
|
required: true,
|
|
})
|
|
async redirect(
|
|
@Req() req: Request,
|
|
@Headers("Authorization") authorization: string,
|
|
@Param("app") app: string,
|
|
@Query("returnTo") returnTo?: string,
|
|
@Query("onErrorReturnTo") onErrorReturnTo?: string
|
|
): Promise<GetConferencingAppsOauthUrlResponseDto> {
|
|
const origin = req.headers.origin;
|
|
const accessToken = authorization.replace("Bearer ", "");
|
|
|
|
const state: OAuthCallbackState = {
|
|
returnTo: returnTo ?? origin,
|
|
onErrorReturnTo: onErrorReturnTo ?? origin,
|
|
fromApp: false,
|
|
accessToken,
|
|
};
|
|
|
|
const credential = await this.conferencingService.generateOAuthUrl(app, state);
|
|
|
|
return {
|
|
status: SUCCESS_STATUS,
|
|
data: plainToInstance(ConferencingAppsOauthUrlOutputDto, credential),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Handles saving conferencing app credentials.
|
|
* If both orgId and teamId are present in the callback state, the request is proxied to the organization/team-level endpoint;
|
|
* otherwise, credentials are saved at the user level.
|
|
*
|
|
* Proxying ensures that permission checks—such as whether the user is allowed to install conferencing app for a team or organization—
|
|
* are enforced via controller route guards, avoiding duplication of this logic within the service layer.
|
|
*/
|
|
@Get("/:app/oauth/callback")
|
|
@UseGuards()
|
|
@Redirect(undefined, 301)
|
|
@ApiOperation({ summary: "Conferencing app OAuth callback" })
|
|
@ApiParam({
|
|
name: "app",
|
|
description: "Conferencing application type",
|
|
enum: [ZOOM, OFFICE_365_VIDEO],
|
|
required: true,
|
|
})
|
|
async save(
|
|
@Query("state") state: string,
|
|
@Param("app") app: string,
|
|
@Query("code") code: string,
|
|
@Query("error") error: string | undefined,
|
|
@Query("error_description") error_description: string | undefined
|
|
): Promise<{ url: string }> {
|
|
if (!state) {
|
|
throw new BadRequestException("Missing `state` query param");
|
|
}
|
|
|
|
const decodedCallbackState: OAuthCallbackState = JSON.parse(state);
|
|
try {
|
|
if (error) {
|
|
throw new BadRequestException(error_description);
|
|
}
|
|
|
|
if (decodedCallbackState.teamId && decodedCallbackState.orgId) {
|
|
const apiUrl = this.config.get("api.url");
|
|
const url = `${apiUrl}/organizations/${decodedCallbackState.orgId}/teams/${decodedCallbackState.teamId}/conferencing/${app}/oauth/callback`;
|
|
const params: Record<string, string | undefined> = { state, code, error, error_description };
|
|
const headers = {
|
|
Authorization: `Bearer ${decodedCallbackState.accessToken}`,
|
|
};
|
|
try {
|
|
const response = await this.httpService.axiosRef.get(url, { params, headers });
|
|
const redirectUrl = response.data?.url || decodedCallbackState.onErrorReturnTo || "";
|
|
return { url: redirectUrl };
|
|
} catch (err) {
|
|
const fallbackUrl = decodedCallbackState.onErrorReturnTo || "";
|
|
return { url: fallbackUrl };
|
|
}
|
|
}
|
|
|
|
return this.conferencingService.connectOauthApps(app, code, decodedCallbackState);
|
|
} catch (error) {
|
|
if (error instanceof HttpException || error instanceof Error) {
|
|
this.logger.error(error.message);
|
|
}
|
|
return {
|
|
url: decodedCallbackState.onErrorReturnTo ?? "",
|
|
};
|
|
}
|
|
}
|
|
|
|
@Get("/")
|
|
@HttpCode(HttpStatus.OK)
|
|
@UseGuards(ApiAuthGuard)
|
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
|
@ApiOperation({ summary: "List your conferencing applications" })
|
|
async listInstalledConferencingApps(
|
|
@GetUser() user: UserWithProfile
|
|
): Promise<ConferencingAppsOutputResponseDto> {
|
|
const conferencingApps = await this.conferencingService.getConferencingApps(user.id);
|
|
return {
|
|
status: SUCCESS_STATUS,
|
|
data: conferencingApps.map((app) => plainToInstance(ConferencingAppsOutputDto, app)),
|
|
};
|
|
}
|
|
|
|
@Post("/:app/default")
|
|
@HttpCode(HttpStatus.OK)
|
|
@UseGuards(ApiAuthGuard)
|
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
|
@ApiOperation({ summary: "Set your default conferencing application" })
|
|
@ApiParam({
|
|
name: "app",
|
|
description: "Conferencing application type",
|
|
enum: [GOOGLE_MEET, ZOOM, OFFICE_365_VIDEO, CAL_VIDEO],
|
|
required: true,
|
|
})
|
|
async default(
|
|
@GetUser() user: UserWithProfile,
|
|
@Param("app") app: string
|
|
): Promise<SetDefaultConferencingAppOutputResponseDto> {
|
|
await this.conferencingService.setDefaultConferencingApp(user, app);
|
|
return { status: SUCCESS_STATUS };
|
|
}
|
|
|
|
@Get("/default")
|
|
@HttpCode(HttpStatus.OK)
|
|
@UseGuards(ApiAuthGuard)
|
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
|
@ApiOperation({ summary: "Get your default conferencing application" })
|
|
async getDefault(@GetUser() user: UserWithProfile): Promise<GetDefaultConferencingAppOutputResponseDto> {
|
|
const defaultconferencingApp = await this.conferencingService.getUserDefaultConferencingApp(user.id);
|
|
return { status: SUCCESS_STATUS, data: defaultconferencingApp };
|
|
}
|
|
|
|
@Delete("/:app/disconnect")
|
|
@HttpCode(HttpStatus.OK)
|
|
@UseGuards(ApiAuthGuard)
|
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
|
@ApiOperation({ summary: "Disconnect your conferencing application" })
|
|
@ApiParam({
|
|
name: "app",
|
|
description: "Conferencing application type",
|
|
enum: [GOOGLE_MEET, ZOOM, OFFICE_365_VIDEO],
|
|
required: true,
|
|
})
|
|
async disconnect(
|
|
@GetUser() user: UserWithProfile,
|
|
@Param("app") app: string
|
|
): Promise<DisconnectConferencingAppOutputResponseDto> {
|
|
await this.conferencingService.disconnectConferencingApp(user, app);
|
|
return { status: SUCCESS_STATUS };
|
|
}
|
|
}
|