feat: Platform OAuthClient Webhooks (#16134)

* wip

* fixup! Merge branch 'main' into platform-oauth-client-webhooks

* wip

* fixup! Merge branch 'main' into platform-oauth-client-webhooks

* fixup! fixup! Merge branch 'main' into platform-oauth-client-webhooks

* fixup! fixup! fixup! Merge branch 'main' into platform-oauth-client-webhooks

* fixup! fixup! fixup! fixup! Merge branch 'main' into platform-oauth-client-webhooks

* fixup! fixup! fixup! fixup! fixup! Merge branch 'main' into platform-oauth-client-webhooks

* fixup! fixup! fixup! fixup! fixup! fixup! Merge branch 'main' into platform-oauth-client-webhooks

* fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge branch 'main' into platform-oauth-client-webhooks

* fixup! Merge branch 'platform-oauth-client-webhooks' of github.com:calcom/cal.com into platform-oauth-client-webhooks

* fixup! fixup! Merge branch 'platform-oauth-client-webhooks' of github.com:calcom/cal.com into platform-oauth-client-webhooks

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
Morgan
2024-08-09 17:02:39 +02:00
committed by GitHub
co-authored by Peer Richelsen
parent 4def31e98f
commit dda4b17a7c
47 changed files with 1199 additions and 68 deletions
@@ -0,0 +1,69 @@
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
import {
CanActivate,
ExecutionContext,
Injectable,
NotFoundException,
ForbiddenException,
BadRequestException,
} from "@nestjs/common";
import { Request } from "express";
import { PlatformOAuthClient, Webhook } from "@calcom/prisma/client";
@Injectable()
export class IsOAuthClientWebhookGuard implements CanActivate {
constructor(
private readonly webhooksService: WebhooksService,
private readonly oAuthClientRepository: OAuthClientRepository
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context
.switchToHttp()
.getRequest<Request & { webhook: Webhook; oAuthClient: PlatformOAuthClient }>();
const user = request.user as GetUserReturnType;
const webhookId = request.params.webhookId;
const oAuthClientId = request.params.clientId;
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
if (!user) {
throw new ForbiddenException("User not authenticated");
}
if (!webhookId) {
throw new BadRequestException("webhookId parameter not specified in the request");
}
if (!webhookId) {
throw new BadRequestException("oAuthClientId parameter not specified in the request");
}
if (!user || !webhookId || !oAuthClientId) {
return false;
}
const oAuthClient = await this.oAuthClientRepository.getOAuthClient(oAuthClientId);
if (!oAuthClient) {
throw new NotFoundException(`OAuthClient (${oAuthClientId}) not found`);
}
const webhook = await this.webhooksService.getWebhookById(webhookId);
if (oAuthClient?.organizationId !== organizationId) {
return user.isSystemAdmin;
}
if (webhook.platformOAuthClientId !== oAuthClientId) {
throw new ForbiddenException("Webhook does not belong to this oAuthClient");
}
request.webhook = webhook;
request.oAuthClient = oAuthClient;
return true;
}
}
@@ -0,0 +1,26 @@
import { Expose, Type } from "class-transformer";
import { IsInt, ValidateNested } from "class-validator";
import { ApiResponseWithoutData } from "@calcom/platform-types";
import { WebhookOutputDto } from "./webhook.output";
export class OAuthClientWebhookOutputDto extends WebhookOutputDto {
@IsInt()
@Expose()
readonly oAuthClientId!: string;
}
export class OAuthClientWebhookOutputResponseDto extends ApiResponseWithoutData {
@Expose()
@ValidateNested()
@Type(() => WebhookOutputDto)
data!: OAuthClientWebhookOutputDto;
}
export class OAuthClientWebhooksOutputResponseDto extends ApiResponseWithoutData {
@Expose()
@ValidateNested()
@Type(() => WebhookOutputDto)
data!: OAuthClientWebhookOutputDto[];
}
@@ -1,7 +1,7 @@
import { ApiProperty } from "@nestjs/swagger";
import { WebhookTriggerEvents } from "@prisma/client";
import { Expose, Type } from "class-transformer";
import { IsBoolean, IsEnum, IsInt, IsString, ValidateNested } from "class-validator";
import { IsBoolean, IsEnum, IsInt, IsString, ValidateNested, IsArray } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
@@ -25,7 +25,8 @@ export class WebhookOutputDto {
})
readonly payloadTemplate!: string;
@IsEnum(WebhookTriggerEvents)
@IsArray()
@IsEnum(WebhookTriggerEvents, { each: true })
@Expose()
readonly triggers!: WebhookTriggerEvents[];
@@ -36,6 +37,10 @@ export class WebhookOutputDto {
@IsBoolean()
@Expose()
readonly active!: boolean;
@IsString()
@Expose()
readonly secret?: string;
}
export class DeleteManyWebhooksOutputResponseDto {
@@ -4,13 +4,8 @@ import { Webhook } from "@prisma/client";
@Injectable()
export class WebhookOutputPipe implements PipeTransform {
transform(value: Webhook) {
if (value?.eventTriggers) {
const { eventTriggers, ...rest } = value;
const triggers = eventTriggers;
const parsedData = { ...rest, triggers };
return parsedData;
}
return value;
const { eventTriggers, platformOAuthClientId, ...rest } = value;
return { ...rest, triggers: eventTriggers, oAuthClientId: platformOAuthClientId };
}
}
@@ -0,0 +1,32 @@
import { PipedInputWebhookType } from "@/modules/webhooks/pipes/WebhookInputPipe";
import { WebhooksRepository } from "@/modules/webhooks/webhooks.repository";
import { ConflictException, Injectable } from "@nestjs/common";
@Injectable()
export class OAuthClientWebhooksService {
constructor(private readonly webhooksRepository: WebhooksRepository) {}
async createOAuthClientWebhook(platformOAuthClientId: string, body: PipedInputWebhookType) {
const existingWebhook = await this.webhooksRepository.getOAuthClientWebhookByUrl(
platformOAuthClientId,
body.subscriberUrl
);
if (existingWebhook) {
throw new ConflictException("Webhook with this subscriber url already exists for this oAuth client");
}
return this.webhooksRepository.createOAuthClientWebhook(platformOAuthClientId, {
...body,
payloadTemplate: body.payloadTemplate ?? null,
secret: body.secret ?? null,
});
}
async getOAuthClientWebhooksPaginated(platformOAuthClientId: string, skip: number, take: number) {
return this.webhooksRepository.getOAuthClientWebhooksPaginated(platformOAuthClientId, skip, take);
}
async deleteAllOAuthClientWebhooks(platformOAuthClientId: string): Promise<{ count: number }> {
return this.webhooksRepository.deleteAllOAuthClientWebhooks(platformOAuthClientId);
}
}
@@ -1,19 +1,44 @@
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { EventTypeWebhooksController } from "@/modules/event-types/controllers/event-types-webhooks.controller";
import { OAuthClientWebhooksController } from "@/modules/oauth-clients/controllers/oauth-client-webhooks/oauth-client-webhooks.controller";
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
import { Module } from "@nestjs/common";
import { MembershipsModule } from "../memberships/memberships.module";
import { OrganizationsModule } from "../organizations/organizations.module";
import { PrismaModule } from "../prisma/prisma.module";
import { UsersModule } from "../users/users.module";
import { WebhooksController } from "./controllers/webhooks.controller";
import { EventTypeWebhooksService } from "./services/event-type-webhooks.service";
import { OAuthClientWebhooksService } from "./services/oauth-clients-webhooks.service";
import { UserWebhooksService } from "./services/user-webhooks.service";
import { WebhooksService } from "./services/webhooks.service";
import { WebhooksRepository } from "./webhooks.repository";
@Module({
imports: [PrismaModule, UsersModule, EventTypesModule_2024_06_14],
controllers: [WebhooksController, EventTypeWebhooksController],
providers: [WebhooksService, WebhooksRepository, UserWebhooksService, EventTypeWebhooksService],
exports: [WebhooksService, WebhooksRepository, UserWebhooksService, EventTypeWebhooksService],
imports: [
PrismaModule,
UsersModule,
EventTypesModule_2024_06_14,
OAuthClientModule,
OrganizationsModule,
MembershipsModule,
OAuthClientModule,
],
controllers: [WebhooksController, EventTypeWebhooksController, OAuthClientWebhooksController],
providers: [
WebhooksService,
WebhooksRepository,
UserWebhooksService,
EventTypeWebhooksService,
OAuthClientWebhooksService,
],
exports: [
WebhooksService,
WebhooksRepository,
UserWebhooksService,
EventTypeWebhooksService,
OAuthClientWebhooksService,
],
})
export class WebhooksModule {}
@@ -29,6 +29,13 @@ export class WebhooksRepository {
});
}
async createOAuthClientWebhook(platformOAuthClientId: string, data: WebhookInputData) {
const id = uuidv4();
return this.dbWrite.prisma.webhook.create({
data: { ...data, id, platformOAuthClientId },
});
}
async updateWebhook(webhookId: string, data: Partial<WebhookInputData>) {
return this.dbWrite.prisma.webhook.update({
where: { id: webhookId },
@@ -58,12 +65,26 @@ export class WebhooksRepository {
});
}
async getOAuthClientWebhooksPaginated(platformOAuthClientId: string, skip: number, take: number) {
return this.dbRead.prisma.webhook.findMany({
where: { platformOAuthClientId },
skip,
take,
});
}
async getUserWebhookByUrl(userId: number, subscriberUrl: string) {
return this.dbRead.prisma.webhook.findFirst({
where: { userId, subscriberUrl },
});
}
async getOAuthClientWebhookByUrl(platformOAuthClientId: string, subscriberUrl: string) {
return this.dbRead.prisma.webhook.findFirst({
where: { platformOAuthClientId, subscriberUrl },
});
}
async getEventTypeWebhookByUrl(eventTypeId: number, subscriberUrl: string) {
return this.dbRead.prisma.webhook.findFirst({
where: { eventTypeId, subscriberUrl },
@@ -81,4 +102,10 @@ export class WebhooksRepository {
where: { eventTypeId },
});
}
async deleteAllOAuthClientWebhooks(oAuthClientId: string) {
return this.dbWrite.prisma.webhook.deleteMany({
where: { platformOAuthClientId: oAuthClientId },
});
}
}