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:
@@ -25,8 +25,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.27",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-libraries-0.0.26": "npm:@calcom/platform-libraries@0.0.26",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
|
||||
@@ -20,7 +20,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { handleNewBooking } from "@calcom/platform-libraries-0.0.26";
|
||||
import { handleNewBooking } from "@calcom/platform-libraries";
|
||||
import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
describe("Bookings Endpoints", () => {
|
||||
|
||||
@@ -46,7 +46,7 @@ import {
|
||||
getBookingInfo,
|
||||
handleCancelBooking,
|
||||
getBookingForReschedule,
|
||||
} from "@calcom/platform-libraries-0.0.26";
|
||||
} from "@calcom/platform-libraries";
|
||||
import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-types";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
||||
import { IsBoolean, IsNumber, IsOptional } from "class-validator";
|
||||
|
||||
import type { AppsStatus } from "@calcom/platform-libraries-0.0.26";
|
||||
import type { AppsStatus } from "@calcom/platform-libraries";
|
||||
|
||||
export class CreateRecurringBookingInput extends CreateBookingInput {
|
||||
@IsBoolean()
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BadRequestException, UnauthorizedException } from "@nestjs/common";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { SUCCESS_STATUS, APPLE_CALENDAR_TYPE, APPLE_CALENDAR_ID } from "@calcom/platform-constants";
|
||||
import { symmetricEncrypt, CalendarService } from "@calcom/platform-libraries-0.0.26";
|
||||
import { symmetricEncrypt, CalendarService } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class AppleCalendarService implements CredentialSyncCalendarApp {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { User } from "@prisma/client";
|
||||
import { DateTime } from "luxon";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConnectedDestinationCalendars, getBusyCalendarTimes } from "@calcom/platform-libraries-0.0.26";
|
||||
import { getConnectedDestinationCalendars, getBusyCalendarTimes } from "@calcom/platform-libraries";
|
||||
import { Calendar } from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ import {
|
||||
EventTypesPublic,
|
||||
eventTypeBookingFields,
|
||||
eventTypeLocations,
|
||||
} from "@calcom/platform-libraries-0.0.26";
|
||||
} from "@calcom/platform-libraries";
|
||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
describe("Event types Endpoints", () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { getEventTypeById } from "@calcom/platform-libraries-0.0.26";
|
||||
import { getEventTypeById } from "@calcom/platform-libraries";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Injectable()
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import {
|
||||
updateEventType,
|
||||
EventTypesPublic,
|
||||
getEventTypesPublic,
|
||||
} from "@calcom/platform-libraries-0.0.26";
|
||||
} from "@calcom/platform-libraries";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
getEventTypeById,
|
||||
transformApiEventTypeBookingFields,
|
||||
transformApiEventTypeLocations,
|
||||
} from "@calcom/platform-libraries-0.0.26";
|
||||
} from "@calcom/platform-libraries";
|
||||
import { CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
|
||||
+3
-3
@@ -10,9 +10,9 @@ import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { createEventType, updateEventType } from "@calcom/platform-libraries-0.0.26";
|
||||
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries-0.0.26";
|
||||
import { dynamicEvent } from "@calcom/platform-libraries-0.0.26";
|
||||
import { createEventType, updateEventType } from "@calcom/platform-libraries";
|
||||
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries";
|
||||
import { dynamicEvent } from "@calcom/platform-libraries";
|
||||
import {
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { Injectable } from "@nestjs/common";
|
||||
import {
|
||||
transformApiEventTypeBookingFields,
|
||||
transformApiEventTypeLocations,
|
||||
} from "@calcom/platform-libraries-0.0.26";
|
||||
} from "@calcom/platform-libraries";
|
||||
import { CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import {
|
||||
BookingFieldsSchema,
|
||||
SystemField,
|
||||
UserField,
|
||||
} from "@calcom/platform-libraries-0.0.26";
|
||||
} from "@calcom/platform-libraries";
|
||||
|
||||
type EventTypeRelations = { users: User[]; schedule: Schedule | null };
|
||||
type DatabaseEventType = EventType & EventTypeRelations;
|
||||
|
||||
+1
-4
@@ -1,9 +1,6 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import {
|
||||
transformApiScheduleOverrides,
|
||||
transformApiScheduleAvailability,
|
||||
} from "@calcom/platform-libraries-0.0.26";
|
||||
import { transformApiScheduleOverrides, transformApiScheduleAvailability } from "@calcom/platform-libraries";
|
||||
import { CreateScheduleInput_2024_06_11, ScheduleAvailabilityInput_2024_06_11 } from "@calcom/platform-types";
|
||||
import { ScheduleOverrideInput_2024_06_11 } from "@calcom/platform-types";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { TRPCError } from "@calcom/platform-libraries-0.0.26";
|
||||
import { TRPCError } from "@calcom/platform-libraries";
|
||||
import { Response } from "@calcom/platform-types";
|
||||
|
||||
@Catch(TRPCError)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { sendSignupToOrganizationEmail, getTranslation } from "@calcom/platform-libraries-0.0.26";
|
||||
import { sendSignupToOrganizationEmail, getTranslation } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class EmailService {
|
||||
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
|
||||
import {
|
||||
OAuthClientWebhooksOutputResponseDto,
|
||||
OAuthClientWebhookOutputResponseDto,
|
||||
} from "@/modules/webhooks/outputs/oauth-client-webhook.output";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { PlatformBillingRepositoryFixture } from "test/fixtures/repository/billing.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { WebhookRepositoryFixture } from "test/fixtures/repository/webhooks.repository.fixture";
|
||||
import { withNextAuth } from "test/utils/withNextAuth";
|
||||
|
||||
import { PlatformOAuthClient, Team, Webhook } from "@calcom/prisma/client";
|
||||
|
||||
describe("EventTypes WebhooksController (e2e)", () => {
|
||||
let app: INestApplication;
|
||||
const userEmail = "event-types-webhook-controller-e2e@api.com";
|
||||
const otherUserEmail = "other-event-types-webhook-controller-e2e@api.com";
|
||||
let user: UserWithProfile;
|
||||
let otherUser: UserWithProfile;
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let otherOAuthClient: PlatformOAuthClient;
|
||||
let org: Team;
|
||||
let otherOrg: Team;
|
||||
let oAuthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let webhookRepositoryFixture: WebhookRepositoryFixture;
|
||||
let otherOAuthClientWebhook: Webhook;
|
||||
let membershipRepositoryFixture: MembershipRepositoryFixture;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
let orgRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture;
|
||||
|
||||
let webhook: OAuthClientWebhookOutputResponseDto["data"];
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withNextAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
})
|
||||
).compile();
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
webhookRepositoryFixture = new WebhookRepositoryFixture(moduleRef);
|
||||
oAuthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
orgRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef);
|
||||
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
});
|
||||
otherUser = await userRepositoryFixture.create({
|
||||
email: otherUserEmail,
|
||||
username: otherUserEmail,
|
||||
});
|
||||
|
||||
org = await orgRepositoryFixture.create({
|
||||
name: "apiOrg",
|
||||
isOrganization: true,
|
||||
metadata: {
|
||||
isOrganization: true,
|
||||
orgAutoAcceptEmail: "api.com",
|
||||
isOrganizationVerified: true,
|
||||
isOrganizationConfigured: true,
|
||||
},
|
||||
isPlatform: true,
|
||||
});
|
||||
otherOrg = await orgRepositoryFixture.create({
|
||||
name: "otherOrg",
|
||||
isOrganization: true,
|
||||
metadata: {
|
||||
isOrganization: true,
|
||||
orgAutoAcceptEmail: "api.com",
|
||||
isOrganizationVerified: true,
|
||||
isOrganizationConfigured: true,
|
||||
},
|
||||
isPlatform: true,
|
||||
});
|
||||
await platformBillingRepositoryFixture.create(org.id);
|
||||
await platformBillingRepositoryFixture.create(otherOrg.id);
|
||||
await membershipRepositoryFixture.create({
|
||||
role: "OWNER",
|
||||
user: { connect: { id: user.id } },
|
||||
team: { connect: { id: org.id } },
|
||||
accepted: true,
|
||||
});
|
||||
await membershipRepositoryFixture.create({
|
||||
role: "OWNER",
|
||||
user: { connect: { id: otherUser.id } },
|
||||
team: { connect: { id: otherOrg.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${user.id}`,
|
||||
username: userEmail,
|
||||
organization: {
|
||||
connect: {
|
||||
id: org.id,
|
||||
},
|
||||
},
|
||||
movedFromUser: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: { id: user.id },
|
||||
},
|
||||
});
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${otherUser.id}`,
|
||||
username: otherUserEmail,
|
||||
organization: {
|
||||
connect: {
|
||||
id: otherOrg.id,
|
||||
},
|
||||
},
|
||||
movedFromUser: {
|
||||
connect: {
|
||||
id: otherUser.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: { id: otherUser.id },
|
||||
},
|
||||
});
|
||||
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["redirect-uri"],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
oAuthClient = await oAuthClientRepositoryFixture.create(org.id, data, secret);
|
||||
otherOAuthClient = await oAuthClientRepositoryFixture.create(otherOrg.id, data, secret);
|
||||
otherOAuthClientWebhook = await webhookRepositoryFixture.create({
|
||||
id: "123abc-123abc-123abc-123abc",
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
subscriberUrl: "https://example.com",
|
||||
eventTriggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
platformOAuthClient: { connect: { id: otherOAuthClient.id } },
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await orgRepositoryFixture.delete(org.id);
|
||||
await userRepositoryFixture.deleteByEmail(otherUser.email);
|
||||
await orgRepositoryFixture.delete(otherOrg.id);
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("/webhooks (POST)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/oauth-clients/${oAuthClient.id}/webhooks`)
|
||||
.send({
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: true,
|
||||
payloadTemplate: "string",
|
||||
} satisfies CreateWebhookInputDto)
|
||||
.expect(201)
|
||||
.then(async (res) => {
|
||||
expect(res.body).toMatchObject({
|
||||
status: "success",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
subscriberUrl: "https://example.com",
|
||||
active: true,
|
||||
oAuthClientId: oAuthClient.id,
|
||||
payloadTemplate: "string",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
},
|
||||
} satisfies OAuthClientWebhookOutputResponseDto);
|
||||
webhook = res.body.data;
|
||||
});
|
||||
});
|
||||
|
||||
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (PATCH)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/oauth-clients/${oAuthClient.id}/webhooks/${webhook.id}`)
|
||||
.send({
|
||||
active: false,
|
||||
} satisfies UpdateWebhookInputDto)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body.data.active).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (GET)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/oauth-clients/${oAuthClient.id}/webhooks/${webhook.id}`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body).toMatchObject({
|
||||
status: "success",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: false,
|
||||
payloadTemplate: "string",
|
||||
oAuthClientId: oAuthClient.id,
|
||||
},
|
||||
} satisfies OAuthClientWebhookOutputResponseDto);
|
||||
});
|
||||
});
|
||||
|
||||
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (GET) should fail to get a webhook that does not exist", () => {
|
||||
return request(app.getHttpServer()).get(`/v2/oauth-clients/${oAuthClient.id}/webhooks/90284`).expect(404);
|
||||
});
|
||||
|
||||
it("/webhooks (GET)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/oauth-clients/${oAuthClient.id}/webhooks`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
const responseBody = res.body as OAuthClientWebhooksOutputResponseDto;
|
||||
responseBody.data.forEach((webhook) => {
|
||||
expect(webhook.oAuthClientId).toBe(oAuthClient.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
it("/webhooks (GET) should fail to get webhooks of oauth client that doesn't belong to you", () => {
|
||||
return request(app.getHttpServer()).get(`/v2/oauth-clients/${otherOAuthClient.id}/webhooks`).expect(403);
|
||||
});
|
||||
|
||||
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (DELETE)", () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/oauth-clients/${oAuthClient.id}/webhooks/${webhook.id}`)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body).toMatchObject({
|
||||
status: "success",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
subscriberUrl: "https://example.com",
|
||||
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
|
||||
active: false,
|
||||
payloadTemplate: "string",
|
||||
oAuthClientId: oAuthClient.id,
|
||||
},
|
||||
} satisfies OAuthClientWebhookOutputResponseDto);
|
||||
});
|
||||
});
|
||||
|
||||
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (DELETE) should fail to delete webhooks of an oauth client that doesn't belong to you", () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/oauth-clients/${otherOAuthClient.id}/webhooks/${otherOAuthClientWebhook.id}`)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it("/oauth-clients/:oAuthClientId/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not exist", () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/oauth-clients/${oAuthClient.id}/webhooks/1234453`)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
|
||||
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
||||
import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard";
|
||||
import { GetWebhook } from "@/modules/webhooks/decorators/get-webhook-decorator";
|
||||
import { IsOAuthClientWebhookGuard } from "@/modules/webhooks/guards/is-oauth-client-webhook-guard";
|
||||
import { CreateWebhookInputDto, UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
|
||||
import {
|
||||
OAuthClientWebhookOutputResponseDto,
|
||||
OAuthClientWebhookOutputDto,
|
||||
OAuthClientWebhooksOutputResponseDto,
|
||||
} from "@/modules/webhooks/outputs/oauth-client-webhook.output";
|
||||
import { DeleteManyWebhooksOutputResponseDto } from "@/modules/webhooks/outputs/webhook.output";
|
||||
import { PartialWebhookInputPipe, WebhookInputPipe } from "@/modules/webhooks/pipes/WebhookInputPipe";
|
||||
import { WebhookOutputPipe } from "@/modules/webhooks/pipes/WebhookOutputPipe";
|
||||
import { OAuthClientWebhooksService } from "@/modules/webhooks/services/oauth-clients-webhooks.service";
|
||||
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
|
||||
import { Controller, Post, Body, UseGuards, Get, Param, Query, Delete, Patch } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
import { Webhook, MembershipRole } from "@prisma/client";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { SkipTakePagination } from "@calcom/platform-types";
|
||||
|
||||
import { OAuthClientGuard } from "../../guards/oauth-client-guard";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/oauth-clients/:clientId/webhooks",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(NextAuthGuard, OrganizationRolesGuard, OAuthClientGuard)
|
||||
@ApiTags("OAuthClients Webhooks")
|
||||
export class OAuthClientWebhooksController {
|
||||
constructor(
|
||||
private readonly webhooksService: WebhooksService,
|
||||
private readonly oAuthClientWebhooksService: OAuthClientWebhooksService
|
||||
) {}
|
||||
|
||||
@Post("/")
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
@ApiOperation({ summary: "Create a webhook for an oAuthClient" })
|
||||
async createOAuthClientWebhook(
|
||||
@Body() body: CreateWebhookInputDto,
|
||||
@Param("clientId") oAuthClientId: string
|
||||
): Promise<OAuthClientWebhookOutputResponseDto> {
|
||||
const webhook = await this.oAuthClientWebhooksService.createOAuthClientWebhook(
|
||||
oAuthClientId,
|
||||
new WebhookInputPipe().transform(body)
|
||||
);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(OAuthClientWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Patch("/:webhookId")
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
@ApiOperation({ summary: "Update a webhook of an oAuthClient" })
|
||||
@UseGuards(IsOAuthClientWebhookGuard)
|
||||
async updateOAuthClientWebhook(
|
||||
@Body() body: UpdateWebhookInputDto,
|
||||
@Param("webhookId") webhookId: string
|
||||
): Promise<OAuthClientWebhookOutputResponseDto> {
|
||||
const webhook = await this.webhooksService.updateWebhook(
|
||||
webhookId,
|
||||
new PartialWebhookInputPipe().transform(body)
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(OAuthClientWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/:webhookId")
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
||||
@ApiOperation({ summary: "Get a webhook of an oAuthClient" })
|
||||
@UseGuards(IsOAuthClientWebhookGuard)
|
||||
async getOAuthClientWebhook(@GetWebhook() webhook: Webhook): Promise<OAuthClientWebhookOutputResponseDto> {
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(OAuthClientWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/")
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
||||
@ApiOperation({ summary: "Get all webhooks of an oAuthClient" })
|
||||
async getOAuthClientWebhooks(
|
||||
@Param("clientId") oAuthClientId: string,
|
||||
@Query() pagination: SkipTakePagination
|
||||
): Promise<OAuthClientWebhooksOutputResponseDto> {
|
||||
const webhooks = await this.oAuthClientWebhooksService.getOAuthClientWebhooksPaginated(
|
||||
oAuthClientId,
|
||||
pagination.skip ?? 0,
|
||||
pagination.take ?? 250
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: webhooks.map((webhook) =>
|
||||
plainToClass(OAuthClientWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
})
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@Delete("/:webhookId")
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
@ApiOperation({ summary: "Delete a webhook of an oAuthClient" })
|
||||
@UseGuards(IsOAuthClientWebhookGuard)
|
||||
async deleteOAuthClientWebhook(
|
||||
@GetWebhook() webhook: Webhook
|
||||
): Promise<OAuthClientWebhookOutputResponseDto> {
|
||||
await this.webhooksService.deleteWebhook(webhook.id);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(OAuthClientWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
|
||||
strategy: "excludeAll",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Delete("/")
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
@ApiOperation({ summary: "Delete all webhooks of an oAuthClient" })
|
||||
async deleteAllOAuthClientWebhooks(
|
||||
@Param("clientId") oAuthClientId: string
|
||||
): Promise<DeleteManyWebhooksOutputResponseDto> {
|
||||
const data = await this.oAuthClientWebhooksService.deleteAllOAuthClientWebhooks(oAuthClientId);
|
||||
return { status: SUCCESS_STATUS, data: `${data.count} webhooks deleted` };
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -37,8 +37,7 @@ import {
|
||||
ApiOperation as DocsOperation,
|
||||
ApiCreatedResponse as DocsCreatedResponse,
|
||||
} from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { User } from "@prisma/client";
|
||||
import { User, MembershipRole } from "@prisma/client";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { CreateOAuthClientInput } from "@calcom/platform-types";
|
||||
@@ -86,6 +85,7 @@ export class OAuthClientsController {
|
||||
}
|
||||
|
||||
const { id, secret } = await this.oauthClientRepository.createOAuthClient(organizationId, body);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: {
|
||||
@@ -116,6 +116,7 @@ export class OAuthClientsController {
|
||||
if (!client) {
|
||||
throw new NotFoundException(`OAuth client with ID ${clientId} not found`);
|
||||
}
|
||||
|
||||
return { status: SUCCESS_STATUS, data: client };
|
||||
}
|
||||
|
||||
@@ -149,6 +150,7 @@ export class OAuthClientsController {
|
||||
): Promise<GetOAuthClientResponseDto> {
|
||||
this.logger.log(`For client ${clientId} updating OAuth Client with data: ${JSON.stringify(body)}`);
|
||||
const client = await this.oauthClientRepository.updateOAuthClient(clientId, body);
|
||||
|
||||
return { status: SUCCESS_STATUS, data: client };
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries-0.0.26";
|
||||
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthClientUsersService {
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { createEventType, updateEventType } from "@calcom/platform-libraries-0.0.26";
|
||||
import { createEventType, updateEventType } from "@calcom/platform-libraries";
|
||||
import {
|
||||
CreateTeamEventTypeInput_2024_06_14,
|
||||
UpdateTeamEventTypeInput_2024_06_14,
|
||||
|
||||
@@ -3,9 +3,9 @@ import { OrganizationsEventTypesRepository } from "@/modules/organizations/repos
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import type { EventType, User, Schedule, Host } from "@prisma/client";
|
||||
import { SchedulingType } from "@prisma/client";
|
||||
|
||||
import { HostPriority, TeamEventTypeResponseHost } from "@calcom/platform-types";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
type EventTypeRelations = { users: User[]; schedule: Schedule | null; hosts: Host[] };
|
||||
type DatabaseEventType = EventType & EventTypeRelations;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { OrganizationsTeamsRepository } from "@/modules/organizations/repositori
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries-0.0.26";
|
||||
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsTeamsService {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { CreateUserInput } from "@/modules/users/inputs/create-user.input";
|
||||
import { Injectable, ConflictException } from "@nestjs/common";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
|
||||
import { createNewUsersConnectToOrgIfExists } from "@calcom/platform-libraries-0.0.26";
|
||||
import { createNewUsersConnectToOrgIfExists } from "@calcom/platform-libraries";
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -5,8 +5,8 @@ import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { Response as ExpressResponse, Request as ExpressRequest } from "express";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { getAvailableSlots } from "@calcom/platform-libraries-0.0.26";
|
||||
import type { AvailableSlotsType } from "@calcom/platform-libraries-0.0.26";
|
||||
import { getAvailableSlots } from "@calcom/platform-libraries";
|
||||
import type { AvailableSlotsType } from "@calcom/platform-libraries";
|
||||
import { RemoveSelectedSlotInput, ReserveSlotInput } from "@calcom/platform-types";
|
||||
import { ApiResponse, GetAvailableSlotsInput } from "@calcom/platform-types";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import { MINUTES_TO_BOOK } from "@calcom/platform-libraries-0.0.26";
|
||||
import { MINUTES_TO_BOOK } from "@calcom/platform-libraries";
|
||||
import { ReserveSlotInput } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Controller, Get } from "@nestjs/common";
|
||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { CityTimezones } from "@calcom/platform-libraries-0.0.26";
|
||||
import type { CityTimezones } from "@calcom/platform-libraries";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { cityTimezonesHandler } from "@calcom/platform-libraries-0.0.26";
|
||||
import type { CityTimezones } from "@calcom/platform-libraries-0.0.26";
|
||||
import { cityTimezonesHandler } from "@calcom/platform-libraries";
|
||||
import type { CityTimezones } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class TimezonesService {
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3624,6 +3624,206 @@
|
||||
"Users' EventTypes Webhooks"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/oauth-clients/{clientId}/webhooks": {
|
||||
"post": {
|
||||
"operationId": "OAuthClientWebhooksController_createOAuthClientWebhook",
|
||||
"summary": "Create a webhook for an oAuthClient",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "clientId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateWebhookInputDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"OAuthClients Webhooks"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "OAuthClientWebhooksController_getOAuthClientWebhooks",
|
||||
"summary": "Get all webhooks of an oAuthClient",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "clientId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "take",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to return",
|
||||
"example": 10,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "skip",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to skip",
|
||||
"example": 0,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OAuthClientWebhooksOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"OAuthClients Webhooks"
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "OAuthClientWebhooksController_deleteAllOAuthClientWebhooks",
|
||||
"summary": "Delete all webhooks of an oAuthClient",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "clientId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DeleteManyWebhooksOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"OAuthClients Webhooks"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/oauth-clients/{clientId}/webhooks/{webhookId}": {
|
||||
"patch": {
|
||||
"operationId": "OAuthClientWebhooksController_updateOAuthClientWebhook",
|
||||
"summary": "Update a webhook of an oAuthClient",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "webhookId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateWebhookInputDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"OAuthClients Webhooks"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "OAuthClientWebhooksController_getOAuthClientWebhook",
|
||||
"summary": "Get a webhook of an oAuthClient",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"OAuthClients Webhooks"
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "OAuthClientWebhooksController_deleteOAuthClientWebhook",
|
||||
"summary": "Delete a webhook of an oAuthClient",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"OAuthClients Webhooks"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
@@ -8864,6 +9064,9 @@
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -8991,6 +9194,9 @@
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -9064,6 +9270,88 @@
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"OAuthClientWebhookOutputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"payloadTemplate": {
|
||||
"type": "string",
|
||||
"description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information",
|
||||
"example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}"
|
||||
},
|
||||
"oAuthClientId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"triggers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"subscriberUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"payloadTemplate",
|
||||
"oAuthClientId",
|
||||
"id",
|
||||
"triggers",
|
||||
"subscriberUrl",
|
||||
"active"
|
||||
]
|
||||
},
|
||||
"OAuthClientWebhookOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/OAuthClientWebhookOutputDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"OAuthClientWebhooksOutputResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/OAuthClientWebhookOutputDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +158,13 @@ export const OAuthClientCard = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-4">
|
||||
<Button
|
||||
className="bg-subtle hover:bg-emphasis text-white"
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
onClick={() => router.push(`/settings/platform/oauth-clients/${id}/edit/webhooks`)}>
|
||||
Webhooks
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-subtle hover:bg-emphasis text-white"
|
||||
loading={isLoading}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
|
||||
import type { ApiSuccessResponse } from "@calcom/platform-types";
|
||||
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
|
||||
type Input = {
|
||||
active: boolean;
|
||||
subscriberUrl: string;
|
||||
triggers: WebhookTriggerEvents[];
|
||||
secret?: string;
|
||||
payloadTemplate?: string;
|
||||
};
|
||||
|
||||
type Output = {
|
||||
id: string;
|
||||
OAuthClientId: string;
|
||||
active: boolean;
|
||||
subscriberUrl: string;
|
||||
triggers: WebhookTriggerEvents[];
|
||||
secret?: string;
|
||||
payloadTemplate: string | undefined | null;
|
||||
};
|
||||
|
||||
export const useOAuthClientWebhooks = (clientId: string) => {
|
||||
const query = useQuery<ApiSuccessResponse<Output[]>>({
|
||||
queryKey: ["oauth-clients-webhooks", "findMany", clientId],
|
||||
queryFn: () => {
|
||||
return fetch(`/api/v2/oauth-clients/${clientId}/webhooks`, {
|
||||
method: "get",
|
||||
headers: { "Content-type": "application/json" },
|
||||
}).then((res) => res.json());
|
||||
},
|
||||
enabled: !!clientId,
|
||||
});
|
||||
|
||||
return { ...query, data: query.data?.data ?? [], status: query.data?.status };
|
||||
};
|
||||
|
||||
export const useOAuthClientWebhook = (clientId: string, webhookId: string) => {
|
||||
const query = useQuery<ApiSuccessResponse<Output>>({
|
||||
queryKey: ["oauth-clients-webhooks", "findOne", clientId, webhookId],
|
||||
queryFn: () => {
|
||||
return fetch(`/api/v2/oauth-clients/${clientId}/webhooks/${webhookId}`, {
|
||||
method: "get",
|
||||
headers: { "Content-type": "application/json" },
|
||||
}).then((res) => res.json());
|
||||
},
|
||||
enabled: !!clientId,
|
||||
});
|
||||
|
||||
return { ...query, data: query.data?.data ?? [], status: query.data?.status };
|
||||
};
|
||||
|
||||
export const useUpdateOAuthClientWebhook = (clientId: string) => {
|
||||
const mutation = useMutation<
|
||||
ApiSuccessResponse<Output>,
|
||||
unknown,
|
||||
{ webhookId: string; body: Partial<Input> }
|
||||
>({
|
||||
mutationFn: ({ webhookId, body }) => {
|
||||
return fetch(`/api/v2/oauth-clients/${clientId}/webhooks/${webhookId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
}).then((res) => res.json());
|
||||
},
|
||||
});
|
||||
|
||||
return mutation;
|
||||
};
|
||||
|
||||
export const useCreateOAuthClientWebhook = (clientId: string) => {
|
||||
const mutation = useMutation<ApiSuccessResponse<Output>, unknown, Input>({
|
||||
mutationFn: (body: Input) => {
|
||||
return fetch(`/api/v2/oauth-clients/${clientId}/webhooks`, {
|
||||
method: "POST",
|
||||
headers: { "Content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
}).then((res) => res.json());
|
||||
},
|
||||
});
|
||||
|
||||
return mutation;
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
import { useParams } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { WebhookForm } from "@calcom/features/webhooks/components";
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { showToast } from "@calcom/ui";
|
||||
|
||||
import {
|
||||
useOAuthClientWebhooks,
|
||||
useCreateOAuthClientWebhook,
|
||||
useUpdateOAuthClientWebhook,
|
||||
} from "@lib/hooks/settings/platform/oauth-clients/useOAuthClientWebhooks";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
import NoPlatformPlan from "@components/settings/platform/dashboard/NoPlatformPlan";
|
||||
import { useGetUserAttributes } from "@components/settings/platform/hooks/useGetUserAttributes";
|
||||
|
||||
export default function EditOAuthClientWebhooks() {
|
||||
const router = useRouter();
|
||||
const params = useParams<{ clientId: string }>();
|
||||
const clientId = params?.clientId || "";
|
||||
|
||||
const { isUserLoading, isPlatformUser, isPaidUser } = useGetUserAttributes();
|
||||
|
||||
const {
|
||||
data: webhooks,
|
||||
status: webhooksStatus,
|
||||
isFetched: isWebhooksFetched,
|
||||
refetch: refetchWebhooks,
|
||||
} = useOAuthClientWebhooks(clientId);
|
||||
const webhookId = webhooks?.[0]?.id ?? "";
|
||||
const webhook = webhooks?.[0];
|
||||
const { mutateAsync: createWebhook } = useCreateOAuthClientWebhook(clientId);
|
||||
const { mutateAsync: updateWebhook } = useUpdateOAuthClientWebhook(clientId);
|
||||
|
||||
if (isUserLoading) return <div className="m-5">Loading...</div>;
|
||||
|
||||
if (isPlatformUser && isPaidUser) {
|
||||
return (
|
||||
<div>
|
||||
<Shell title="OAuth client updation form" isPlatformUser={true}>
|
||||
<div className="m-2 md:mx-5">
|
||||
<div className="border-subtle mx-auto block justify-between rounded-t-lg border px-4 py-6 sm:flex sm:px-6">
|
||||
<div className="flex w-full flex-col">
|
||||
<h1 className="font-cal text-emphasis mb-1 text-xl font-semibold leading-5 tracking-wide">
|
||||
Webhook update form
|
||||
</h1>
|
||||
<p className="text-default text-sm ltr:mr-4 rtl:ml-4">Add a webhook to your OAuthClient.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{webhooksStatus !== "success" && <p>Error while trying to access webhooks.</p>}
|
||||
|
||||
{isWebhooksFetched && webhooksStatus === "success" && (
|
||||
<WebhookForm
|
||||
overrideTriggerOptions={[
|
||||
{ value: WebhookTriggerEvents.BOOKING_CANCELLED, label: "booking_cancelled" },
|
||||
{ value: WebhookTriggerEvents.BOOKING_CREATED, label: "booking_created" },
|
||||
{ value: WebhookTriggerEvents.BOOKING_REJECTED, label: "booking_rejected" },
|
||||
{ value: WebhookTriggerEvents.BOOKING_REQUESTED, label: "booking_requested" },
|
||||
{ value: WebhookTriggerEvents.BOOKING_RESCHEDULED, label: "booking_rescheduled" },
|
||||
{ value: WebhookTriggerEvents.BOOKING_NO_SHOW_UPDATED, label: "booking_no_show_updated" },
|
||||
{ value: WebhookTriggerEvents.MEETING_ENDED, label: "meeting_ended" },
|
||||
{ value: WebhookTriggerEvents.MEETING_STARTED, label: "meeting_started" },
|
||||
{ value: WebhookTriggerEvents.RECORDING_READY, label: "recording_ready" },
|
||||
{
|
||||
value: WebhookTriggerEvents.RECORDING_TRANSCRIPTION_GENERATED,
|
||||
label: "recording_transcription_generated",
|
||||
},
|
||||
]}
|
||||
onSubmit={async (data) => {
|
||||
try {
|
||||
const body = {
|
||||
active: data.active,
|
||||
payloadTemplate: data.payloadTemplate ?? undefined,
|
||||
subscriberUrl: data.subscriberUrl,
|
||||
triggers: data.eventTriggers,
|
||||
secret: data.secret ?? undefined,
|
||||
};
|
||||
if (webhook) {
|
||||
await updateWebhook({
|
||||
webhookId,
|
||||
body,
|
||||
});
|
||||
showToast("Webhook updated successfully.", "success");
|
||||
} else {
|
||||
await createWebhook(body);
|
||||
showToast("Webhook created successfully.", "success");
|
||||
}
|
||||
await refetchWebhooks();
|
||||
router.push("/settings/platform/");
|
||||
} catch (err) {
|
||||
showToast(`Failed to ${webhookId ? "update" : "create"} webhook.`, "error");
|
||||
}
|
||||
}}
|
||||
onCancel={() => {
|
||||
router.push("/settings/platform/");
|
||||
}}
|
||||
noRoutingFormTriggers={true}
|
||||
webhook={
|
||||
webhook
|
||||
? { ...webhook, eventTriggers: webhook.triggers, secret: webhook.secret ?? null }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Shell>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Shell isPlatformUser={true} hideHeadingOnMobile withoutMain={false} SidebarContainer={<></>}>
|
||||
<NoPlatformPlan />
|
||||
</Shell>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
EditOAuthClientWebhooks.PageWrapper = PageWrapper;
|
||||
@@ -62,7 +62,7 @@ export default function CreateOAuthClient() {
|
||||
return (
|
||||
<div>
|
||||
<Shell title={`OAuth client ${!!clientId ? "updation" : "creation"} form`} isPlatformUser={true}>
|
||||
<div className="m-2 md:mx-14 md:mx-5">
|
||||
<div className="m-2 md:mx-5">
|
||||
<div className="border-subtle mx-auto block justify-between rounded-t-lg border px-4 py-6 sm:flex sm:px-6">
|
||||
<div className="flex w-full flex-col">
|
||||
<h1 className="font-cal text-emphasis mb-1 text-xl font-semibold leading-5 tracking-wide">
|
||||
|
||||
@@ -932,6 +932,7 @@ async function handler(
|
||||
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
|
||||
teamId,
|
||||
orgId,
|
||||
oAuthClientId: platformClientId,
|
||||
};
|
||||
|
||||
const eventTrigger: WebhookTriggerEvents = rescheduleUid
|
||||
@@ -1601,6 +1602,7 @@ async function handler(
|
||||
triggerEvent: WebhookTriggerEvents.BOOKING_PAYMENT_INITIATED,
|
||||
teamId,
|
||||
orgId,
|
||||
oAuthClientId: platformClientId,
|
||||
};
|
||||
await handleWebhookTrigger({
|
||||
subscriberOptions: subscriberOptionsPaymentInitiated,
|
||||
|
||||
@@ -54,15 +54,18 @@ const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2: Record<string, WebhookTriggerEve
|
||||
const WebhookForm = (props: {
|
||||
webhook?: WebhookFormData;
|
||||
apps?: (keyof typeof WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2)[];
|
||||
overrideTriggerOptions?: (typeof WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2)["core"];
|
||||
onSubmit: (event: WebhookFormSubmitData) => void;
|
||||
onCancel?: () => void;
|
||||
noRoutingFormTriggers: boolean;
|
||||
selectOnlyInstantMeetingOption?: boolean;
|
||||
}) => {
|
||||
const { apps = [], selectOnlyInstantMeetingOption = false } = props;
|
||||
const { apps = [], selectOnlyInstantMeetingOption = false, overrideTriggerOptions } = props;
|
||||
const { t } = useLocale();
|
||||
|
||||
const triggerOptions = [...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2["core"]];
|
||||
const triggerOptions = overrideTriggerOptions
|
||||
? [...overrideTriggerOptions]
|
||||
: [...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2["core"]];
|
||||
if (apps) {
|
||||
for (const app of apps) {
|
||||
if (app === "routing-forms" && props.noRoutingFormTriggers) continue;
|
||||
|
||||
@@ -8,6 +8,7 @@ export type GetSubscriberOptions = {
|
||||
triggerEvent: WebhookTriggerEvents;
|
||||
teamId?: number | null;
|
||||
orgId?: number | null;
|
||||
oAuthClientId?: string | null;
|
||||
};
|
||||
|
||||
const getWebhooks = async (options: GetSubscriberOptions, prisma: PrismaClient = defaultPrisma) => {
|
||||
@@ -15,6 +16,7 @@ const getWebhooks = async (options: GetSubscriberOptions, prisma: PrismaClient =
|
||||
const eventTypeId = options.eventTypeId ?? 0;
|
||||
const teamId = options.teamId ?? 0;
|
||||
const orgId = options.orgId ?? 0;
|
||||
const oAuthClientId = options.oAuthClientId ?? "";
|
||||
|
||||
// if we have userId and teamId it is a managed event type and should trigger for team and user
|
||||
const allWebhooks = await prisma.webhook.findMany({
|
||||
@@ -34,6 +36,7 @@ const getWebhooks = async (options: GetSubscriberOptions, prisma: PrismaClient =
|
||||
in: [teamId, orgId],
|
||||
},
|
||||
},
|
||||
{ platformOAuthClientId: oAuthClientId },
|
||||
],
|
||||
AND: {
|
||||
eventTriggers: {
|
||||
|
||||
@@ -142,6 +142,7 @@ export const buildWebhook = (webhook?: Partial<Webhook>): Webhook => {
|
||||
active: true,
|
||||
eventTriggers: [],
|
||||
teamId: null,
|
||||
platformOAuthClientId: null,
|
||||
...webhook,
|
||||
platform: false,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[platformOAuthClientId,subscriberUrl]` on the table `Webhook` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Webhook" ADD COLUMN "platformOAuthClientId" TEXT;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Webhook_platformOAuthClientId_subscriberUrl_key" ON "Webhook"("platformOAuthClientId", "subscriberUrl");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Webhook" ADD CONSTRAINT "Webhook_platformOAuthClientId_fkey" FOREIGN KEY ("platformOAuthClientId") REFERENCES "PlatformOAuthClient"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -727,26 +727,29 @@ enum WebhookTriggerEvents {
|
||||
}
|
||||
|
||||
model Webhook {
|
||||
id String @id @unique
|
||||
userId Int?
|
||||
teamId Int?
|
||||
eventTypeId Int?
|
||||
id String @id @unique
|
||||
userId Int?
|
||||
teamId Int?
|
||||
eventTypeId Int?
|
||||
platformOAuthClientId String?
|
||||
/// @zod.url()
|
||||
subscriberUrl String
|
||||
payloadTemplate String?
|
||||
createdAt DateTime @default(now())
|
||||
active Boolean @default(true)
|
||||
eventTriggers WebhookTriggerEvents[]
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
eventType EventType? @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
|
||||
app App? @relation(fields: [appId], references: [slug], onDelete: Cascade)
|
||||
appId String?
|
||||
secret String?
|
||||
platform Boolean @default(false)
|
||||
scheduledTriggers WebhookScheduledTriggers[]
|
||||
subscriberUrl String
|
||||
payloadTemplate String?
|
||||
createdAt DateTime @default(now())
|
||||
active Boolean @default(true)
|
||||
eventTriggers WebhookTriggerEvents[]
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
eventType EventType? @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
|
||||
platformOAuthClient PlatformOAuthClient? @relation(fields: [platformOAuthClientId], references: [id], onDelete: Cascade)
|
||||
app App? @relation(fields: [appId], references: [slug], onDelete: Cascade)
|
||||
appId String?
|
||||
secret String?
|
||||
platform Boolean @default(false)
|
||||
scheduledTriggers WebhookScheduledTriggers[]
|
||||
|
||||
@@unique([userId, subscriberUrl], name: "courseIdentifier")
|
||||
@@unique([platformOAuthClientId, subscriberUrl], name: "oauthclientwebhook")
|
||||
}
|
||||
|
||||
model Impersonations {
|
||||
@@ -1297,6 +1300,7 @@ model PlatformOAuthClient {
|
||||
accessTokens AccessToken[]
|
||||
refreshToken RefreshToken[]
|
||||
authorizationTokens PlatformAuthorizationToken[]
|
||||
webhook Webhook[]
|
||||
|
||||
bookingRedirectUri String?
|
||||
bookingCancelRedirectUri String?
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import * as z from "zod"
|
||||
import * as imports from "../zod-utils"
|
||||
import { WebhookTriggerEvents } from "@prisma/client"
|
||||
import { CompleteUser, UserModel, CompleteTeam, TeamModel, CompleteEventType, EventTypeModel, CompleteApp, AppModel, CompleteWebhookScheduledTriggers, WebhookScheduledTriggersModel } from "./index"
|
||||
import { CompleteUser, UserModel, CompleteTeam, TeamModel, CompleteEventType, EventTypeModel, CompletePlatformOAuthClient, PlatformOAuthClientModel, CompleteApp, AppModel, CompleteWebhookScheduledTriggers, WebhookScheduledTriggersModel } from "./index"
|
||||
|
||||
export const _WebhookModel = z.object({
|
||||
id: z.string(),
|
||||
userId: z.number().int().nullish(),
|
||||
teamId: z.number().int().nullish(),
|
||||
eventTypeId: z.number().int().nullish(),
|
||||
platformOAuthClientId: z.string().nullish(),
|
||||
subscriberUrl: z.string().url(),
|
||||
payloadTemplate: z.string().nullish(),
|
||||
createdAt: z.date(),
|
||||
@@ -22,6 +23,7 @@ export interface CompleteWebhook extends z.infer<typeof _WebhookModel> {
|
||||
user?: CompleteUser | null
|
||||
team?: CompleteTeam | null
|
||||
eventType?: CompleteEventType | null
|
||||
platformOAuthClient?: CompletePlatformOAuthClient | null
|
||||
app?: CompleteApp | null
|
||||
scheduledTriggers: CompleteWebhookScheduledTriggers[]
|
||||
}
|
||||
@@ -35,6 +37,7 @@ export const WebhookModel: z.ZodSchema<CompleteWebhook> = z.lazy(() => _WebhookM
|
||||
user: UserModel.nullish(),
|
||||
team: TeamModel.nullish(),
|
||||
eventType: EventTypeModel.nullish(),
|
||||
platformOAuthClient: PlatformOAuthClientModel.nullish(),
|
||||
app: AppModel.nullish(),
|
||||
scheduledTriggers: WebhookScheduledTriggersModel.array(),
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user