Files
cal.diy/apps/api/v2/src/modules/conferencing/controllers/conferencing.controller.ts
T
fad5497363 feat: allow conferencing apps to be installed on teams (#19784)
* feat: added zoom, office365-video as valid event-type locations

* added docs

* Update stripe.service.ts

* enable conferencing apps for teams initial commit

* fix: make conferencing atom work with teams

* update team events in bulk update default location

* feat: Separate endpoints for team conferencing apps instead of query params

* Display all team events in 'Bulk update existing event types' modal when conferencing atom is enabled for Teams

* fix: bulk update default location for team events

* removed the organizations endpoints

* Refactor: Separate functions for team events to follow SRP

* undo extra changes

* undo extra changes

* fix: platform libraries build breaking

* feat: only allow integrations which are installed in create/update team/user event-type endpoints

* fix: merge conflicts

* remove redundant imports

* Update ConferencingAppsViewPlatformWrapper.tsx

* Update useBanners.ts

* fix: axios error response interceptor

* feat: update :app param docs in `organizations-conferencing.controller`

* Revert "fix: axios error response interceptor"

This reverts commit 729eac3eee9f81c19b5368bbae4b1666884221fd.

* Update useAtomsGetInstalledConferencingApps.ts

* upgrade platform libraries

* Update apps.ts

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
2025-04-02 11:32:11 -04:00

236 lines
7.9 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 { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Logger } from "@nestjs/common";
import {
Controller,
Get,
Query,
HttpCode,
HttpStatus,
UseGuards,
Post,
Param,
BadRequestException,
Delete,
Headers,
Redirect,
UnauthorizedException,
Req,
HttpException,
} from "@nestjs/common";
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 tokensRepository: TokensRepository,
private readonly conferencingService: ConferencingService,
private readonly organizationsConferencingService: OrganizationsConferencingService
) {}
@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),
};
}
@Get("/:app/oauth/callback")
@UseGuards()
@Redirect(undefined, 301)
@ApiOperation({ summary: "conferencing apps oauths 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 {
const userId = await this.tokensRepository.getAccessTokenOwnerId(decodedCallbackState.accessToken);
if (error) {
throw new BadRequestException(error_description);
}
if (!userId) {
throw new UnauthorizedException("Invalid Access token.");
}
if (decodedCallbackState.orgId) {
return this.organizationsConferencingService.connectTeamOauthApps({
app,
code,
userId,
decodedCallbackState,
});
} else {
return this.conferencingService.connectOauthApps(app, code, userId, 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 };
}
}