Files
cal.diy/apps/api/v2/src/modules/conferencing/controllers/conferencing.controller.ts
T
Keith WilliamsandDevin AI 739ef153f6 docs: improve English naturalness in API v2 documentation (#22130)
* 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>
2025-07-03 23:47:09 +00:00

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 };
}
}