diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index 72f7ad94a9..4a1e2b362d 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -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": "*", diff --git a/apps/api/v2/src/ee/bookings/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/controllers/bookings.controller.e2e-spec.ts index 06deaa99de..c8731539f5 100644 --- a/apps/api/v2/src/ee/bookings/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/controllers/bookings.controller.e2e-spec.ts @@ -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", () => { diff --git a/apps/api/v2/src/ee/bookings/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/controllers/bookings.controller.ts index 6c912f87f3..dfe7b26c70 100644 --- a/apps/api/v2/src/ee/bookings/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/controllers/bookings.controller.ts @@ -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"; diff --git a/apps/api/v2/src/ee/bookings/inputs/create-recurring-booking.input.ts b/apps/api/v2/src/ee/bookings/inputs/create-recurring-booking.input.ts index 18fd674dff..471e8ab8df 100644 --- a/apps/api/v2/src/ee/bookings/inputs/create-recurring-booking.input.ts +++ b/apps/api/v2/src/ee/bookings/inputs/create-recurring-booking.input.ts @@ -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() diff --git a/apps/api/v2/src/ee/calendars/services/apple-calendar.service.ts b/apps/api/v2/src/ee/calendars/services/apple-calendar.service.ts index 504ef05f5c..e04ee36b7f 100644 --- a/apps/api/v2/src/ee/calendars/services/apple-calendar.service.ts +++ b/apps/api/v2/src/ee/calendars/services/apple-calendar.service.ts @@ -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 { diff --git a/apps/api/v2/src/ee/calendars/services/calendars.service.ts b/apps/api/v2/src/ee/calendars/services/calendars.service.ts index 8bcd484488..4f509f1935 100644 --- a/apps/api/v2/src/ee/calendars/services/calendars.service.ts +++ b/apps/api/v2/src/ee/calendars/services/calendars.service.ts @@ -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"; diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.e2e-spec.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.e2e-spec.ts index a1a4f01a3e..d332e4a88b 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.e2e-spec.ts @@ -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", () => { diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.repository.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.repository.ts index f7c2672739..b56bf84bed 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.repository.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.repository.ts @@ -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() diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts index fbb1f401fa..a5eeb38615 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts @@ -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() diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts index fc03184e54..a20b989060 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts @@ -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"; diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts index 9cb0af13f9..a72667123f 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts @@ -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, diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts index 418739c6bb..8e4c3ed273 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts @@ -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() diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts index 1fe6b55e15..e648164a94 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts @@ -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; diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/services/input-schedules.service.ts b/apps/api/v2/src/ee/schedules/schedules_2024_06_11/services/input-schedules.service.ts index 74cfa9d93b..137987670f 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/services/input-schedules.service.ts +++ b/apps/api/v2/src/ee/schedules/schedules_2024_06_11/services/input-schedules.service.ts @@ -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"; diff --git a/apps/api/v2/src/filters/trpc-exception.filter.ts b/apps/api/v2/src/filters/trpc-exception.filter.ts index c487b0ed4b..47dbcd00b4 100644 --- a/apps/api/v2/src/filters/trpc-exception.filter.ts +++ b/apps/api/v2/src/filters/trpc-exception.filter.ts @@ -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) diff --git a/apps/api/v2/src/modules/email/email.service.ts b/apps/api/v2/src/modules/email/email.service.ts index 568cc4b676..c689b1aa7c 100644 --- a/apps/api/v2/src/modules/email/email.service.ts +++ b/apps/api/v2/src/modules/email/email.service.ts @@ -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 { diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-webhooks/oauth-client-webhooks.controller.e2e-spec.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-webhooks/oauth-client-webhooks.controller.e2e-spec.ts new file mode 100644 index 0000000000..8daf728796 --- /dev/null +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-webhooks/oauth-client-webhooks.controller.e2e-spec.ts @@ -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); + }); +}); diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-webhooks/oauth-client-webhooks.controller.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-webhooks/oauth-client-webhooks.controller.ts new file mode 100644 index 0000000000..87ae92d542 --- /dev/null +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-webhooks/oauth-client-webhooks.controller.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + const data = await this.oAuthClientWebhooksService.deleteAllOAuthClientWebhooks(oAuthClientId); + return { status: SUCCESS_STATUS, data: `${data.count} webhooks deleted` }; + } +} diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts index 8521a2b342..c64d310c5b 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts @@ -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 { 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 }; } diff --git a/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts b/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts index 02fa235ed1..b2a421077f 100644 --- a/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts +++ b/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts @@ -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 { diff --git a/apps/api/v2/src/modules/organizations/services/event-types/organizations-event-types.service.ts b/apps/api/v2/src/modules/organizations/services/event-types/organizations-event-types.service.ts index 8fa2563079..5cfc8517fd 100644 --- a/apps/api/v2/src/modules/organizations/services/event-types/organizations-event-types.service.ts +++ b/apps/api/v2/src/modules/organizations/services/event-types/organizations-event-types.service.ts @@ -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, diff --git a/apps/api/v2/src/modules/organizations/services/event-types/output.service.ts b/apps/api/v2/src/modules/organizations/services/event-types/output.service.ts index ff764eb9a7..e9409e73e6 100644 --- a/apps/api/v2/src/modules/organizations/services/event-types/output.service.ts +++ b/apps/api/v2/src/modules/organizations/services/event-types/output.service.ts @@ -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; diff --git a/apps/api/v2/src/modules/organizations/services/organizations-teams.service.ts b/apps/api/v2/src/modules/organizations/services/organizations-teams.service.ts index 8edaffdbf6..9ec2f3edc1 100644 --- a/apps/api/v2/src/modules/organizations/services/organizations-teams.service.ts +++ b/apps/api/v2/src/modules/organizations/services/organizations-teams.service.ts @@ -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 { diff --git a/apps/api/v2/src/modules/organizations/services/organizations-users-service.ts b/apps/api/v2/src/modules/organizations/services/organizations-users-service.ts index 5266750c0d..6a8622c681 100644 --- a/apps/api/v2/src/modules/organizations/services/organizations-users-service.ts +++ b/apps/api/v2/src/modules/organizations/services/organizations-users-service.ts @@ -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() diff --git a/apps/api/v2/src/modules/slots/controllers/slots.controller.ts b/apps/api/v2/src/modules/slots/controllers/slots.controller.ts index 62a150a891..57383e265f 100644 --- a/apps/api/v2/src/modules/slots/controllers/slots.controller.ts +++ b/apps/api/v2/src/modules/slots/controllers/slots.controller.ts @@ -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"; diff --git a/apps/api/v2/src/modules/slots/slots.repository.ts b/apps/api/v2/src/modules/slots/slots.repository.ts index 189bf2953e..8ef589f9f8 100644 --- a/apps/api/v2/src/modules/slots/slots.repository.ts +++ b/apps/api/v2/src/modules/slots/slots.repository.ts @@ -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() diff --git a/apps/api/v2/src/modules/timezones/controllers/timezones.controller.ts b/apps/api/v2/src/modules/timezones/controllers/timezones.controller.ts index 49d680cc16..ba2bd85d80 100644 --- a/apps/api/v2/src/modules/timezones/controllers/timezones.controller.ts +++ b/apps/api/v2/src/modules/timezones/controllers/timezones.controller.ts @@ -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({ diff --git a/apps/api/v2/src/modules/timezones/services/timezones.service.ts b/apps/api/v2/src/modules/timezones/services/timezones.service.ts index 08df1b826e..be623bebdc 100644 --- a/apps/api/v2/src/modules/timezones/services/timezones.service.ts +++ b/apps/api/v2/src/modules/timezones/services/timezones.service.ts @@ -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 { diff --git a/apps/api/v2/src/modules/webhooks/guards/is-oauth-client-webhook-guard.ts b/apps/api/v2/src/modules/webhooks/guards/is-oauth-client-webhook-guard.ts new file mode 100644 index 0000000000..19fb842622 --- /dev/null +++ b/apps/api/v2/src/modules/webhooks/guards/is-oauth-client-webhook-guard.ts @@ -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 { + const request = context + .switchToHttp() + .getRequest(); + 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; + } +} diff --git a/apps/api/v2/src/modules/webhooks/outputs/oauth-client-webhook.output.ts b/apps/api/v2/src/modules/webhooks/outputs/oauth-client-webhook.output.ts new file mode 100644 index 0000000000..e5405d4dc6 --- /dev/null +++ b/apps/api/v2/src/modules/webhooks/outputs/oauth-client-webhook.output.ts @@ -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[]; +} diff --git a/apps/api/v2/src/modules/webhooks/outputs/webhook.output.ts b/apps/api/v2/src/modules/webhooks/outputs/webhook.output.ts index 1fafa31583..0a9af83814 100644 --- a/apps/api/v2/src/modules/webhooks/outputs/webhook.output.ts +++ b/apps/api/v2/src/modules/webhooks/outputs/webhook.output.ts @@ -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 { diff --git a/apps/api/v2/src/modules/webhooks/pipes/WebhookOutputPipe.ts b/apps/api/v2/src/modules/webhooks/pipes/WebhookOutputPipe.ts index 73f2d82747..b3272cd470 100644 --- a/apps/api/v2/src/modules/webhooks/pipes/WebhookOutputPipe.ts +++ b/apps/api/v2/src/modules/webhooks/pipes/WebhookOutputPipe.ts @@ -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 }; } } diff --git a/apps/api/v2/src/modules/webhooks/services/oauth-clients-webhooks.service.ts b/apps/api/v2/src/modules/webhooks/services/oauth-clients-webhooks.service.ts new file mode 100644 index 0000000000..1e995d8677 --- /dev/null +++ b/apps/api/v2/src/modules/webhooks/services/oauth-clients-webhooks.service.ts @@ -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); + } +} diff --git a/apps/api/v2/src/modules/webhooks/webhooks.module.ts b/apps/api/v2/src/modules/webhooks/webhooks.module.ts index 1774646e20..03ccd8d0c2 100644 --- a/apps/api/v2/src/modules/webhooks/webhooks.module.ts +++ b/apps/api/v2/src/modules/webhooks/webhooks.module.ts @@ -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 {} diff --git a/apps/api/v2/src/modules/webhooks/webhooks.repository.ts b/apps/api/v2/src/modules/webhooks/webhooks.repository.ts index 14d2e7162f..bac197ae6e 100644 --- a/apps/api/v2/src/modules/webhooks/webhooks.repository.ts +++ b/apps/api/v2/src/modules/webhooks/webhooks.repository.ts @@ -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) { 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 }, + }); + } } diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 77f6a2e93c..a293fb3d6c 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -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" + ] } } } diff --git a/apps/web/components/settings/platform/oauth-clients/OAuthClientCard.tsx b/apps/web/components/settings/platform/oauth-clients/OAuthClientCard.tsx index 54a3410cf7..9fb4cc576e 100644 --- a/apps/web/components/settings/platform/oauth-clients/OAuthClientCard.tsx +++ b/apps/web/components/settings/platform/oauth-clients/OAuthClientCard.tsx @@ -158,6 +158,13 @@ export const OAuthClientCard = ({
+