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:
@@ -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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user