feat: Org Webhooks API V2 (#16274)

* --init

* add is webhook in org guard

* --

* doc

* e2e

* --feedback

* --

* fix typo

* delete unnecessary file

* fix v2 e2e

* fix v2 e2e

* fix e2e

* fix v2 e2e

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
Syed Ali Shahbaz
2024-08-29 17:52:42 +04:00
committed by GitHub
co-authored by Morgan
parent 4424aa5b38
commit cf50566a10
9 changed files with 922 additions and 5 deletions
@@ -0,0 +1,67 @@
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
import { OrganizationsWebhooksRepository } from "@/modules/organizations/repositories/organizations-webhooks.repository";
import { RedisService } from "@/modules/redis/redis.service";
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
import { Request } from "express";
import { Team } from "@calcom/prisma/client";
type CachedData = {
org?: Team;
canAccess?: boolean;
};
@Injectable()
export class IsWebhookInOrg implements CanActivate {
constructor(
private organizationsRepository: OrganizationsRepository,
private organizationsWebhooksRepository: OrganizationsWebhooksRepository,
private readonly redisService: RedisService
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
let canAccess = false;
const request = context.switchToHttp().getRequest<Request & { organization: Team }>();
const webhookId: string = request.params.webhookId;
const organizationId: string = request.params.orgId;
if (!organizationId) {
throw new ForbiddenException("No organization id found in request params.");
}
if (!webhookId) {
throw new ForbiddenException("No webhook id found in request params.");
}
const REDIS_CACHE_KEY = `apiv2:org:${webhookId}:guard:isWebhookInOrg`;
const cachedData = await this.redisService.redis.get(REDIS_CACHE_KEY);
if (cachedData) {
const { org: cachedOrg, canAccess: cachedCanAccess } = JSON.parse(cachedData) as CachedData;
if (cachedOrg?.id === Number(organizationId) && cachedCanAccess !== undefined) {
request.organization = cachedOrg;
return cachedCanAccess;
}
}
const org = await this.organizationsRepository.findById(Number(organizationId));
if (org?.isOrganization) {
const isWebhookInOrg = await this.organizationsWebhooksRepository.findWebhook(
Number(organizationId),
webhookId
);
if (isWebhookInOrg) canAccess = true;
}
if (org) {
await this.redisService.redis.set(
REDIS_CACHE_KEY,
JSON.stringify({ org: org, canAccess } satisfies CachedData),
"EX",
300
);
}
return canAccess;
}
}
@@ -1,6 +1,5 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { GetMembership } from "@/modules/auth/decorators/get-membership/get-membership.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
@@ -36,7 +35,6 @@ import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { SkipTakePagination } from "@calcom/platform-types";
import { Membership } from "@calcom/prisma/client";
@Controller({
path: "/v2/organizations/:orgId/memberships",
@@ -89,7 +87,11 @@ export class OrganizationsMembershipsController {
@UseGuards(IsMembershipInOrg)
@Get("/:membershipId")
@HttpCode(HttpStatus.OK)
async getUserSchedule(@GetMembership() membership: Membership): Promise<GetOrgMembership> {
async getOrgMembership(
@Param("orgId", ParseIntPipe) orgId: number,
@Param("membershipId", ParseIntPipe) membershipId: number
): Promise<GetOrgMembership> {
const membership = await this.organizationsMembershipService.getOrgMembership(orgId, membershipId);
return {
status: SUCCESS_STATUS,
data: plainToClass(OrgMembershipOutputDto, membership, { strategy: "excludeAll" }),
@@ -0,0 +1,147 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { IsWebhookInOrg } from "@/modules/auth/guards/organizations/is-webhook-in-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { OrganizationsWebhooksService } from "@/modules/organizations/services/organizations-webhooks.service";
import { CreateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
import { UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
import {
TeamWebhookOutputDto as OrgWebhookOutputDto,
TeamWebhookOutputResponseDto as OrgWebhookOutputResponseDto,
TeamWebhooksOutputResponseDto as OrgWebhooksOutputResponseDto,
} from "@/modules/webhooks/outputs/team-webhook.output";
import { PartialWebhookInputPipe, WebhookInputPipe } from "@/modules/webhooks/pipes/WebhookInputPipe";
import { WebhookOutputPipe } from "@/modules/webhooks/pipes/WebhookOutputPipe";
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
import {
Controller,
UseGuards,
Get,
Param,
ParseIntPipe,
Query,
Delete,
Patch,
Post,
Body,
HttpCode,
HttpStatus,
} from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { SkipTakePagination } from "@calcom/platform-types";
@Controller({
path: "/v2/organizations/:orgId/webhooks",
version: API_VERSIONS_VALUES,
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Organizations Webhooks")
export class OrganizationsWebhooksController {
constructor(
private organizationsWebhooksService: OrganizationsWebhooksService,
private webhooksService: WebhooksService
) {}
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@Get("/")
@HttpCode(HttpStatus.OK)
async getAllOrganizationWebhooks(
@Param("orgId", ParseIntPipe) orgId: number,
@Query() queryParams: SkipTakePagination
): Promise<OrgWebhooksOutputResponseDto> {
const { skip, take } = queryParams;
const webhooks = await this.organizationsWebhooksService.getWebhooksPaginated(
orgId,
skip ?? 0,
take ?? 250
);
return {
status: SUCCESS_STATUS,
data: webhooks.map((webhook) =>
plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
strategy: "excludeAll",
})
),
};
}
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@Post("/")
@HttpCode(HttpStatus.CREATED)
async createOrganizationWebhook(
@Param("orgId", ParseIntPipe) orgId: number,
@Body() body: CreateWebhookInputDto
): Promise<OrgWebhookOutputResponseDto> {
const webhook = await this.organizationsWebhooksService.createWebhook(
orgId,
new WebhookInputPipe().transform(body)
);
return {
status: SUCCESS_STATUS,
data: plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
strategy: "excludeAll",
}),
};
}
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(IsWebhookInOrg)
@Get("/:webhookId")
@HttpCode(HttpStatus.OK)
async getOrganizationWebhook(@Param("webhookId") webhookId: string): Promise<OrgWebhookOutputResponseDto> {
const webhook = await this.organizationsWebhooksService.getWebhook(webhookId);
return {
status: SUCCESS_STATUS,
data: plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
strategy: "excludeAll",
}),
};
}
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(IsWebhookInOrg)
@Delete("/:webhookId")
@HttpCode(HttpStatus.OK)
async deleteWebhook(@Param("webhookId") webhookId: string): Promise<OrgWebhookOutputResponseDto> {
const webhook = await this.webhooksService.deleteWebhook(webhookId);
return {
status: SUCCESS_STATUS,
data: plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
strategy: "excludeAll",
}),
};
}
@Roles("ORG_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(IsWebhookInOrg)
@Patch("/:webhookId")
@HttpCode(HttpStatus.OK)
async updateOrgWebhook(
@Param("webhookId") webhookId: string,
@Body() body: UpdateWebhookInputDto
): Promise<OrgWebhookOutputResponseDto> {
const webhook = await this.organizationsWebhooksService.updateWebhook(
webhookId,
new PartialWebhookInputPipe().transform(body)
);
return {
status: SUCCESS_STATUS,
data: plainToClass(OrgWebhookOutputDto, new WebhookOutputPipe().transform(webhook), {
strategy: "excludeAll",
}),
};
}
}
@@ -0,0 +1,211 @@
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 {
TeamWebhookOutputResponseDto,
TeamWebhooksOutputResponseDto,
} from "@/modules/webhooks/outputs/team-webhook.output";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { WebhookRepositoryFixture } from "test/fixtures/repository/webhooks.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";
import { Team, Webhook } from "@calcom/prisma/client";
describe("WebhooksController (e2e)", () => {
let app: INestApplication;
const userEmail = "webhook-controller-e2e@api.com";
let org: Team;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let webhookRepositoryFixture: WebhookRepositoryFixture;
let userRepositoryFixture: UserRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let webhook: TeamWebhookOutputResponseDto["data"];
let otherWebhook: Webhook;
let user: UserWithProfile;
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
})
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
webhookRepositoryFixture = new WebhookRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
user = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
});
org = await organizationsRepositoryFixture.create({
name: "Test Organization",
isOrganization: true,
});
await membershipsRepositoryFixture.create({
role: "ADMIN",
user: { connect: { id: user.id } },
team: { connect: { id: org.id } },
});
otherWebhook = await webhookRepositoryFixture.create({
id: "2mdfnn2",
subscriberUrl: "https://example.com",
eventTriggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
active: true,
payloadTemplate: "string",
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
afterAll(async () => {
userRepositoryFixture.deleteByEmail(user.email);
webhookRepositoryFixture.delete(otherWebhook.id);
await app.close();
});
it("should be defined", () => {
expect(userRepositoryFixture).toBeDefined();
expect(user).toBeDefined();
expect(org).toBeDefined();
});
it("/organizations/:orgId/webhooks (POST)", () => {
return request(app.getHttpServer())
.post(`/v2/organizations/${org.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) => {
process.stdout.write(JSON.stringify(res.body));
expect(res.body).toMatchObject({
status: "success",
data: {
id: expect.any(String),
subscriberUrl: "https://example.com",
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
active: true,
payloadTemplate: "string",
teamId: org.id,
},
} satisfies TeamWebhookOutputResponseDto);
webhook = res.body.data;
});
});
it("/organizations/:orgId/webhooks (POST) should fail to create a webhook that already has same orgId / subcriberUrl combo", () => {
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/webhooks`)
.send({
subscriberUrl: "https://example.com",
triggers: ["BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CANCELLED"],
active: true,
payloadTemplate: "string",
} satisfies CreateWebhookInputDto)
.expect(409);
});
it("/organizations/:orgId/webhooks/:webhookId (PATCH)", () => {
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/webhooks/${webhook.id}`)
.send({
active: false,
} satisfies UpdateWebhookInputDto)
.expect(200)
.then((res) => {
expect(res.body.data.active).toBe(false);
});
});
it("/organizations/:orgId/webhooks/:webhookId (GET)", () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.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",
teamId: org.id,
},
} satisfies TeamWebhookOutputResponseDto);
});
});
it("/organizations/:orgId/webhooks/:webhookId (GET) should say forbidden to get a webhook that does not exist", () => {
return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/webhooks/90284`).expect(403);
});
it("/organizations/:orgId/webhooks/:webhookId (GET) should fail to get a webhook that does not belong to org", () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/webhooks/${otherWebhook.id}`)
.expect(403);
});
it("/organizations/:orgId/webhooks (GET)", () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/webhooks`)
.expect(200)
.then((res) => {
const responseBody = res.body as TeamWebhooksOutputResponseDto;
responseBody.data.forEach((webhook) => {
expect(webhook.teamId).toBe(org.id);
});
});
});
it("/organizations/:orgId/webhooks/:webhookId (DELETE)", () => {
return request(app.getHttpServer())
.delete(`/v2/organizations/${org.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",
teamId: org.id,
},
} satisfies TeamWebhookOutputResponseDto);
});
});
it("/organizations/:orgId/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not exist", () => {
return request(app.getHttpServer()).delete(`/v2/organizations/${org.id}/webhooks/12993`).expect(403);
});
it("/organizations/:orgId/webhooks/:webhookId (DELETE) shoud fail to delete a webhook that does not belong to org", () => {
return request(app.getHttpServer())
.delete(`/v2/organizations/${org.id}/webhooks/${otherWebhook.id}`)
.expect(403);
});
});
@@ -9,6 +9,7 @@ import { OrganizationsSchedulesController } from "@/modules/organizations/contro
import { OrganizationsTeamsMembershipsController } from "@/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller";
import { OrganizationsTeamsController } from "@/modules/organizations/controllers/teams/organizations-teams.controller";
import { OrganizationsUsersController } from "@/modules/organizations/controllers/users/organizations-users.controller";
import { OrganizationsWebhooksController } from "@/modules/organizations/controllers/webhooks/organizations-webhooks.controller";
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository";
import { OrganizationsMembershipRepository } from "@/modules/organizations/repositories/organizations-membership.repository";
@@ -16,6 +17,7 @@ import { OrganizationSchedulesRepository } from "@/modules/organizations/reposit
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/repositories/organizations-teams-memberships.repository";
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
import { OrganizationsUsersRepository } from "@/modules/organizations/repositories/organizations-users.repository";
import { OrganizationsWebhooksRepository } from "@/modules/organizations/repositories/organizations-webhooks.repository";
import { InputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/input.service";
import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service";
import { OutputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/output.service";
@@ -24,11 +26,14 @@ import { OrganizationsSchedulesService } from "@/modules/organizations/services/
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/services/organizations-teams-memberships.service";
import { OrganizationsTeamsService } from "@/modules/organizations/services/organizations-teams.service";
import { OrganizationsUsersService } from "@/modules/organizations/services/organizations-users-service";
import { OrganizationsWebhooksService } from "@/modules/organizations/services/organizations-webhooks.service";
import { OrganizationsService } from "@/modules/organizations/services/organizations.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { UsersModule } from "@/modules/users/users.module";
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
import { WebhooksRepository } from "@/modules/webhooks/webhooks.repository";
import { Module } from "@nestjs/common";
@Module({
@@ -60,6 +65,10 @@ import { Module } from "@nestjs/common";
OrganizationsEventTypesRepository,
OrganizationsTeamsMembershipsRepository,
OrganizationsTeamsMembershipsService,
OrganizationsWebhooksRepository,
OrganizationsWebhooksService,
WebhooksRepository,
WebhooksService,
],
exports: [
OrganizationsService,
@@ -71,6 +80,10 @@ import { Module } from "@nestjs/common";
OrganizationsMembershipService,
OrganizationsTeamsMembershipsRepository,
OrganizationsTeamsMembershipsService,
OrganizationsWebhooksRepository,
OrganizationsWebhooksService,
WebhooksRepository,
WebhooksService,
],
controllers: [
OrganizationsTeamsController,
@@ -79,6 +92,7 @@ import { Module } from "@nestjs/common";
OrganizationsMembershipsController,
OrganizationsEventTypesController,
OrganizationsTeamsMembershipsController,
OrganizationsWebhooksController,
],
})
export class OrganizationsModule {}
@@ -0,0 +1,71 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import { v4 as uuidv4 } from "uuid";
import { Webhook } from "@calcom/prisma/client";
type WebhookInputData = Pick<
Webhook,
"payloadTemplate" | "eventTriggers" | "subscriberUrl" | "secret" | "active"
>;
@Injectable()
export class OrganizationsWebhooksRepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
async findWebhookByUrl(organizationId: number, subscriberUrl: string) {
return this.dbRead.prisma.webhook.findFirst({
where: { teamId: organizationId, subscriberUrl },
});
}
async findWebhook(organizationId: number, webhookId: string) {
return this.dbRead.prisma.webhook.findUnique({
where: {
id: webhookId,
teamId: organizationId,
},
});
}
async findWebhooks(organizationId: number) {
return this.dbRead.prisma.webhook.findMany({
where: {
teamId: organizationId,
},
});
}
async deleteWebhook(organizationId: number, webhookId: string) {
return this.dbRead.prisma.webhook.delete({
where: {
id: webhookId,
teamId: organizationId,
},
});
}
async createWebhook(organizationId: number, data: WebhookInputData) {
const id = uuidv4();
return this.dbWrite.prisma.webhook.create({
data: { ...data, id, teamId: organizationId },
});
}
async updateWebhook(organizationId: number, webhookId: string, data: Partial<WebhookInputData>) {
return this.dbRead.prisma.webhook.update({
data: { ...data },
where: { id: webhookId, teamId: organizationId },
});
}
async findWebhooksPaginated(organizationId: number, skip: number, take: number) {
return this.dbRead.prisma.webhook.findMany({
where: {
teamId: organizationId,
},
skip,
take,
});
}
}
@@ -0,0 +1,45 @@
import { OrganizationsWebhooksRepository } from "@/modules/organizations/repositories/organizations-webhooks.repository";
import { UpdateWebhookInputDto } from "@/modules/webhooks/inputs/webhook.input";
import { PipedInputWebhookType } from "@/modules/webhooks/pipes/WebhookInputPipe";
import { WebhooksRepository } from "@/modules/webhooks/webhooks.repository";
import { ConflictException, Injectable, NotFoundException } from "@nestjs/common";
@Injectable()
export class OrganizationsWebhooksService {
constructor(
private readonly organizationsWebhooksRepository: OrganizationsWebhooksRepository,
private readonly webhooksRepository: WebhooksRepository
) {}
async createWebhook(orgId: number, body: PipedInputWebhookType) {
const existingWebhook = await this.organizationsWebhooksRepository.findWebhookByUrl(
orgId,
body.subscriberUrl
);
if (existingWebhook) {
throw new ConflictException("Webhook with this subscriber url already exists for this user");
}
return this.organizationsWebhooksRepository.createWebhook(orgId, {
...body,
payloadTemplate: body.payloadTemplate ?? null,
secret: body.secret ?? null,
});
}
async getWebhooksPaginated(orgId: number, skip: number, take: number) {
return this.organizationsWebhooksRepository.findWebhooksPaginated(orgId, skip, take);
}
async getWebhook(webhookId: string) {
const webhook = await this.webhooksRepository.getWebhookById(webhookId);
if (!webhook) {
throw new NotFoundException(`Webhook (${webhookId}) not found`);
}
return webhook;
}
async updateWebhook(webhookId: string, body: UpdateWebhookInputDto) {
return this.webhooksRepository.updateWebhook(webhookId, body);
}
}
@@ -0,0 +1,37 @@
import { ApiProperty } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { IsInt, IsEnum, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
import { WebhookOutputDto } from "./webhook.output";
export class TeamWebhookOutputDto extends WebhookOutputDto {
@IsInt()
@Expose()
readonly teamId!: number;
}
export class TeamWebhookOutputResponseDto {
@Expose()
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@Expose()
@ValidateNested()
@Type(() => WebhookOutputDto)
data!: TeamWebhookOutputDto;
}
export class TeamWebhooksOutputResponseDto {
@Expose()
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@Expose()
@ValidateNested()
@Type(() => WebhookOutputDto)
data!: TeamWebhookOutputDto[];
}
+325 -2
View File
@@ -1668,8 +1668,25 @@
},
"/v2/organizations/{orgId}/memberships/{membershipId}": {
"get": {
"operationId": "OrganizationsMembershipsController_getUserSchedule",
"parameters": [],
"operationId": "OrganizationsMembershipsController_getOrgMembership",
"parameters": [
{
"name": "orgId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "membershipId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
@@ -2268,6 +2285,214 @@
]
}
},
"/v2/organizations/{orgId}/webhooks": {
"get": {
"operationId": "OrganizationsWebhooksController_getAllWebhooks",
"parameters": [
{
"name": "orgId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"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/OrgWebhooksResponseDto"
}
}
}
}
},
"tags": [
"Organizations Webhooks"
]
},
"post": {
"operationId": "OrganizationsWebhooksController_createWebhook",
"parameters": [
{
"name": "orgId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateWebhookInputDto"
}
}
}
},
"responses": {
"201": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OrgWebhooksResponseDto"
}
}
}
}
},
"tags": [
"Organizations Webhooks"
]
}
},
"/v2/organizations/{orgId}/webhooks/{webhookId}": {
"get": {
"operationId": "OrganizationsWebhooksController_getOrgWebhook",
"parameters": [
{
"name": "orgId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "webhookId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OrgWebhooksResponseDto"
}
}
}
}
},
"tags": [
"Organizations Webhooks"
]
},
"delete": {
"operationId": "OrganizationsWebhooksController_deleteWebhook",
"parameters": [
{
"name": "orgId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "webhookId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OrgWebhooksResponseDto"
}
}
}
}
},
"tags": [
"Organizations Webhooks"
]
},
"patch": {
"operationId": "OrganizationsWebhooksController_updateWebhook",
"parameters": [
{
"name": "orgId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"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/OrgWebhooksResponseDto"
}
}
}
}
},
"tags": [
"Organizations Webhooks"
]
}
},
"/v2/schedules": {
"post": {
"operationId": "SchedulesController_2024_04_15_createSchedule",
@@ -9163,6 +9388,84 @@
"active"
]
},
"OrgWebhookOutputDto": {
"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}}\"}"
},
"teamId": {
"type": "number"
},
"id": {
"type": "number"
},
"triggers": {
"type": "array",
"items": {
"type": "object"
}
},
"subscriberUrl": {
"type": "string"
},
"active": {
"type": "boolean"
},
"secret": {
"type": "string"
}
},
"required": [
"payloadTemplate",
"userId",
"id",
"triggers",
"subscriberUrl",
"active"
]
},
"GetAllOrgWebhooksDto": {
"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}}\"}"
},
"teamId": {
"type": "number"
},
"id": {
"type": "number"
},
"triggers": {
"type": "array",
"items": {
"type": "object"
}
},
"subscriberUrl": {
"type": "string"
},
"active": {
"type": "boolean"
},
"secret": {
"type": "string"
}
},
"required": [
"payloadTemplate",
"userId",
"id",
"triggers",
"subscriberUrl",
"active"
]
},
"UserWebhookOutputResponseDto": {
"type": "object",
"properties": {
@@ -9183,6 +9486,26 @@
"data"
]
},
"OrgWebhooksResponseDto": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"$ref": "#/components/schemas/GetAllOrgWebhooksDto"
}
},
"required": [
"status",
"data"
]
},
"UpdateWebhookInputDto": {
"type": "object",
"properties": {