diff --git a/apps/api/v2/src/modules/organizations/organizations/organizations-organizations.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/organizations/organizations-organizations.controller.e2e-spec.ts index 8e96a7efc4..1b7f4e3282 100644 --- a/apps/api/v2/src/modules/organizations/organizations/organizations-organizations.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/organizations/organizations/organizations-organizations.controller.e2e-spec.ts @@ -29,6 +29,7 @@ import { OrganizationRepositoryFixture } from "test/fixtures/repository/organiza import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture"; import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; import { randomString } from "test/utils/randomString"; +import { mockThrottlerGuard } from "test/utils/withNoThrottler"; import { AppModule } from "@/app.module"; import { bootstrap } from "@/bootstrap"; import { getEnv } from "@/env"; @@ -91,12 +92,14 @@ describe("Organizations Organizations Endpoints", () => { const newDate = new Date(2035, 0, 9, 15, 0, 0); - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }).compile(); + beforeAll(async () => { + mockThrottlerGuard(); - userRepositoryFixture = new UserRepositoryFixture(moduleRef); + const moduleRef = await Test.createTestingModule({ + imports: [AppModule, PrismaModule, UsersModule, TokensModule], + }).compile(); + + userRepositoryFixture = new UserRepositoryFixture(moduleRef); organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef); diff --git a/apps/api/v2/src/modules/organizations/organizations/organizations-organizations.controller.ts b/apps/api/v2/src/modules/organizations/organizations/organizations-organizations.controller.ts index f2a957ad34..5fb2dc13fa 100644 --- a/apps/api/v2/src/modules/organizations/organizations/organizations-organizations.controller.ts +++ b/apps/api/v2/src/modules/organizations/organizations/organizations-organizations.controller.ts @@ -1,5 +1,23 @@ +import { SUCCESS_STATUS } from "@calcom/platform-constants"; +import { SkipTakePagination } from "@calcom/platform-types"; +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Param, + ParseIntPipe, + Patch, + Post, + Query, + UseGuards, +} from "@nestjs/common"; +import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { X_CAL_CLIENT_ID_HEADER, X_CAL_SECRET_KEY_HEADER } from "@/lib/docs/headers"; +import { Throttle } from "@/lib/endpoint-throttler-decorator"; import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; @@ -17,24 +35,6 @@ import { CreateManagedOrganizationOutput } from "@/modules/organizations/organiz import { GetManagedOrganizationOutput } from "@/modules/organizations/organizations/outputs/get-managed-organization.output"; import { GetManagedOrganizationsOutput } from "@/modules/organizations/organizations/outputs/get-managed-organizations.output"; import { ManagedOrganizationsService } from "@/modules/organizations/organizations/services/managed-organizations.service"; -import { - Controller, - UseGuards, - Param, - ParseIntPipe, - Post, - Body, - Get, - Patch, - HttpCode, - HttpStatus, - Delete, - Query, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { SkipTakePagination } from "@calcom/platform-types"; const SCALE = "SCALE"; @@ -142,6 +142,7 @@ export class OrganizationsOrganizationsController { @Roles("ORG_ADMIN") @PlatformPlan(SCALE) @Delete("/:managedOrganizationId") + @Throttle({ limit: 1, ttl: 30000, blockDuration: 30000, name: "organizations_delete" }) @ApiOperation({ summary: "Delete an organization within an organization", description: @@ -151,9 +152,8 @@ export class OrganizationsOrganizationsController { async deleteOrganization( @Param("managedOrganizationId", ParseIntPipe) managedOrganizationId: number ): Promise { - const organization = await this.managedOrganizationsService.deleteManagedOrganization( - managedOrganizationId - ); + const organization = + await this.managedOrganizationsService.deleteManagedOrganization(managedOrganizationId); return { status: SUCCESS_STATUS, data: organization, diff --git a/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.controller.e2e-spec.ts index 8b9d4e9959..7030658c35 100644 --- a/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.controller.e2e-spec.ts @@ -12,6 +12,7 @@ import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository. import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; import { randomString } from "test/utils/randomString"; import { withApiAuth } from "test/utils/withApiAuth"; +import { mockThrottlerGuard } from "test/utils/withNoThrottler"; import { AppModule } from "@/app.module"; import { bootstrap } from "@/bootstrap"; import { CreateOrgTeamDto } from "@/modules/organizations/teams/index/inputs/create-organization-team.input"; @@ -39,42 +40,44 @@ describe("Organizations Team Endpoints", () => { const userEmail = `organizations-teams-admin-${randomString()}@api.com`; let user: User; - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); + beforeAll(async () => { + mockThrottlerGuard(); - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); + const moduleRef = await withApiAuth( + userEmail, + Test.createTestingModule({ + imports: [AppModule, PrismaModule, UsersModule, TokensModule], + }) + ).compile(); - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - }); + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); + teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); + membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - org = await organizationsRepositoryFixture.create({ - name: `organizations-teams-organization-${randomString()}`, - isOrganization: true, - }); + user = await userRepositoryFixture.create({ + email: userEmail, + username: userEmail, + }); - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: user.id } }, - team: { connect: { id: org.id } }, - }); + org = await organizationsRepositoryFixture.create({ + name: `organizations-teams-organization-${randomString()}`, + isOrganization: true, + }); - team = await teamsRepositoryFixture.create({ - name: `organizations-teams-team1-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); + await membershipsRepositoryFixture.create({ + role: "ADMIN", + user: { connect: { id: user.id } }, + team: { connect: { id: org.id } }, + }); - team2 = await teamsRepositoryFixture.create({ + team = await teamsRepositoryFixture.create({ + name: `organizations-teams-team1-${randomString()}`, + isOrganization: false, + parent: { connect: { id: org.id } }, + }); + + team2 = await teamsRepositoryFixture.create({ name: `organizations-teams-team2-${randomString()}`, isOrganization: false, parent: { connect: { id: org.id } }, @@ -300,93 +303,95 @@ describe("Organizations Team Endpoints", () => { const userEmail = `organizations-teams-member-${randomString()}@api.com`; let user: User; - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); + beforeAll(async () => { + mockThrottlerGuard(); - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); + const moduleRef = await withApiAuth( + userEmail, + Test.createTestingModule({ + imports: [AppModule, PrismaModule, UsersModule, TokensModule], + }) + ).compile(); - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - }); + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); + teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); + membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - org = await organizationsRepositoryFixture.create({ - name: `organizations-teams-organization-${randomString()}`, - isOrganization: true, - }); + user = await userRepositoryFixture.create({ + email: userEmail, + username: userEmail, + }); - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: user.id } }, - team: { connect: { id: org.id } }, - }); + org = await organizationsRepositoryFixture.create({ + name: `organizations-teams-organization-${randomString()}`, + isOrganization: true, + }); - team = await teamsRepositoryFixture.create({ - name: `organizations-teams-team1-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); + await membershipsRepositoryFixture.create({ + role: "MEMBER", + user: { connect: { id: user.id } }, + team: { connect: { id: org.id } }, + }); - team2 = await teamsRepositoryFixture.create({ - name: `organizations-teams-team2-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); + team = await teamsRepositoryFixture.create({ + name: `organizations-teams-team1-${randomString()}`, + isOrganization: false, + parent: { connect: { id: org.id } }, + }); - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); + team2 = await teamsRepositoryFixture.create({ + name: `organizations-teams-team2-${randomString()}`, + isOrganization: false, + parent: { connect: { id: org.id } }, + }); - await app.init(); - }); + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(teamsRepositoryFixture).toBeDefined(); - expect(user).toBeDefined(); - expect(org).toBeDefined(); - }); + await app.init(); + }); - it("should deny get all the teams of the org", async () => { - return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams`).expect(403); - }); + it("should be defined", () => { + expect(userRepositoryFixture).toBeDefined(); + expect(teamsRepositoryFixture).toBeDefined(); + expect(user).toBeDefined(); + expect(org).toBeDefined(); + }); - it("should deny get all the teams of the org paginated", async () => { - return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams?skip=1&take=1`).expect(403); - }); + it("should deny get all the teams of the org", async () => { + return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams`).expect(403); + }); - it("should deny get the team of the org", async () => { - return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams/${team.id}`).expect(403); - }); + it("should deny get all the teams of the org paginated", async () => { + return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams?skip=1&take=1`).expect(403); + }); - it("should deny create the team of the org", async () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams`) - .send({ - name: `organizations-teams-api-team1-${randomString()}`, - } satisfies CreateOrgTeamDto) - .expect(403); - }); + it("should deny get the team of the org", async () => { + return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams/${team.id}`).expect(403); + }); - it("should deny update the team of the org", async () => { - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${team.id}`) - .send({ - name: `organizations-teams-api-team1-${randomString()}-updated`, - } satisfies CreateOrgTeamDto) - .expect(403); - }); + it("should deny create the team of the org", async () => { + return request(app.getHttpServer()) + .post(`/v2/organizations/${org.id}/teams`) + .send({ + name: `organizations-teams-api-team1-${randomString()}`, + } satisfies CreateOrgTeamDto) + .expect(403); + }); - it("should deny delete the team of the org we created via api", async () => { - return request(app.getHttpServer()).delete(`/v2/organizations/${org.id}/teams/${team2.id}`).expect(403); - }); + it("should deny update the team of the org", async () => { + return request(app.getHttpServer()) + .patch(`/v2/organizations/${org.id}/teams/${team.id}`) + .send({ + name: `organizations-teams-api-team1-${randomString()}-updated`, + } satisfies CreateOrgTeamDto) + .expect(403); + }); + + it("should deny delete the team of the org we created via api", async () => { + return request(app.getHttpServer()).delete(`/v2/organizations/${org.id}/teams/${team2.id}`).expect(403); + }); afterAll(async () => { await userRepositoryFixture.deleteByEmail(user.email); @@ -414,105 +419,107 @@ describe("Organizations Team Endpoints", () => { const userEmail = `organizations-teams-owner-${randomString()}@api.com`; let user: User; - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); + beforeAll(async () => { + mockThrottlerGuard(); - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); + const moduleRef = await withApiAuth( + userEmail, + Test.createTestingModule({ + imports: [AppModule, PrismaModule, UsersModule, TokensModule], + }) + ).compile(); - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - }); + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); + teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); + membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - org = await organizationsRepositoryFixture.create({ - name: `organizations-teams-organization-${randomString()}`, - isOrganization: true, - }); + user = await userRepositoryFixture.create({ + email: userEmail, + username: userEmail, + }); - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: user.id } }, - team: { connect: { id: org.id } }, - }); + org = await organizationsRepositoryFixture.create({ + name: `organizations-teams-organization-${randomString()}`, + isOrganization: true, + }); - team = await teamsRepositoryFixture.create({ - name: `organizations-teams-team1-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); + await membershipsRepositoryFixture.create({ + role: "MEMBER", + user: { connect: { id: user.id } }, + team: { connect: { id: org.id } }, + }); - team2 = await teamsRepositoryFixture.create({ - name: `organizations-teams-team2-${randomString()}`, - isOrganization: false, - parent: { connect: { id: org.id } }, - }); + team = await teamsRepositoryFixture.create({ + name: `organizations-teams-team1-${randomString()}`, + isOrganization: false, + parent: { connect: { id: org.id } }, + }); - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: user.id } }, - team: { connect: { id: team.id } }, - }); + team2 = await teamsRepositoryFixture.create({ + name: `organizations-teams-team2-${randomString()}`, + isOrganization: false, + parent: { connect: { id: org.id } }, + }); - await membershipsRepositoryFixture.create({ - role: "OWNER", - user: { connect: { id: user.id } }, - team: { connect: { id: team2.id } }, - }); + await membershipsRepositoryFixture.create({ + role: "OWNER", + user: { connect: { id: user.id } }, + team: { connect: { id: team.id } }, + }); - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); + await membershipsRepositoryFixture.create({ + role: "OWNER", + user: { connect: { id: user.id } }, + team: { connect: { id: team2.id } }, + }); - await app.init(); - }); + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(organizationsRepositoryFixture).toBeDefined(); - expect(user).toBeDefined(); - expect(org).toBeDefined(); - }); + await app.init(); + }); - it("should deny get all the teams of the org", async () => { - return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams`).expect(403); - }); + it("should be defined", () => { + expect(userRepositoryFixture).toBeDefined(); + expect(organizationsRepositoryFixture).toBeDefined(); + expect(user).toBeDefined(); + expect(org).toBeDefined(); + }); - it("should deny get all the teams of the org paginated", async () => { - return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams?skip=1&take=1`).expect(403); - }); + it("should deny get all the teams of the org", async () => { + return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams`).expect(403); + }); - it("should get the team of the org for which the user is team owner", async () => { - return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams/${team.id}`).expect(200); - }); + it("should deny get all the teams of the org paginated", async () => { + return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams?skip=1&take=1`).expect(403); + }); - it("should deny create the team of the org", async () => { - return request(app.getHttpServer()) - .post(`/v2/organizations/${org.id}/teams`) - .send({ - name: `organizations-teams-api-team1-${randomString()}`, - } satisfies CreateOrgTeamDto) - .expect(403); - }); + it("should get the team of the org for which the user is team owner", async () => { + return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams/${team.id}`).expect(200); + }); - it("should deny update the team of the org", async () => { - return request(app.getHttpServer()) - .patch(`/v2/organizations/${org.id}/teams/${team.id}`) - .send({ - name: `organizations-teams-api-team1-${randomString()}-updated`, - } satisfies CreateOrgTeamDto) - .expect(403); - }); + it("should deny create the team of the org", async () => { + return request(app.getHttpServer()) + .post(`/v2/organizations/${org.id}/teams`) + .send({ + name: `organizations-teams-api-team1-${randomString()}`, + } satisfies CreateOrgTeamDto) + .expect(403); + }); - it("should deny delete the team of the org we created via api", async () => { - return request(app.getHttpServer()).delete(`/v2/organizations/${org.id}/teams/${team2.id}`).expect(403); - }); + it("should deny update the team of the org", async () => { + return request(app.getHttpServer()) + .patch(`/v2/organizations/${org.id}/teams/${team.id}`) + .send({ + name: `organizations-teams-api-team1-${randomString()}-updated`, + } satisfies CreateOrgTeamDto) + .expect(403); + }); + + it("should deny delete the team of the org we created via api", async () => { + return request(app.getHttpServer()).delete(`/v2/organizations/${org.id}/teams/${team2.id}`).expect(403); + }); afterAll(async () => { await userRepositoryFixture.deleteByEmail(user.email); @@ -544,43 +551,45 @@ describe("Organizations Team Endpoints", () => { const userEmail = `organizations-teams-platform-owner-${randomString()}@api.com`; let user: User; - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, TokensModule], - }) - ).compile(); + beforeAll(async () => { + mockThrottlerGuard(); - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); - orgRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - user = await userRepositoryFixture.create({ - email: userEmail, - username: userEmail, - }); + const moduleRef = await withApiAuth( + userEmail, + Test.createTestingModule({ + imports: [AppModule, PrismaModule, UsersModule, TokensModule], + }) + ).compile(); - org = await orgRepositoryFixture.create({ - name: `organizations-teams-platform-organization-${randomString()}`, - isOrganization: true, - }); + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); + membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); + oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); + orgRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); + user = await userRepositoryFixture.create({ + email: userEmail, + username: userEmail, + }); - await membershipsRepositoryFixture.create({ - role: "ADMIN", - user: { connect: { id: user.id } }, - team: { connect: { id: org.id } }, - }); + org = await orgRepositoryFixture.create({ + name: `organizations-teams-platform-organization-${randomString()}`, + isOrganization: true, + }); - oAuthClient1 = await createOAuthClient(org.id); - oAuthClient2 = await createOAuthClient(org.id); + await membershipsRepositoryFixture.create({ + role: "ADMIN", + user: { connect: { id: user.id } }, + team: { connect: { id: org.id } }, + }); - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); + oAuthClient1 = await createOAuthClient(org.id); + oAuthClient2 = await createOAuthClient(org.id); - await app.init(); - }); + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); + + await app.init(); + }); async function createOAuthClient(organizationId: number) { const data = { diff --git a/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.controller.ts b/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.controller.ts index 479a422d5a..8916dedd4d 100644 --- a/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.controller.ts +++ b/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.controller.ts @@ -1,9 +1,29 @@ +import { SUCCESS_STATUS, X_CAL_CLIENT_ID } from "@calcom/platform-constants"; +import { OrgTeamOutputDto, SkipTakePagination } from "@calcom/platform-types"; +import type { Team } from "@calcom/prisma/client"; +import { + Body, + Controller, + Delete, + Get, + Param, + ParseIntPipe, + Patch, + Post, + Query, + Req, + UseGuards, +} from "@nestjs/common"; +import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; +import { plainToClass } from "class-transformer"; +import { Request } from "express"; import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { OPTIONAL_API_KEY_HEADER, OPTIONAL_X_CAL_CLIENT_ID_HEADER, OPTIONAL_X_CAL_SECRET_KEY_HEADER, } from "@/lib/docs/headers"; +import { Throttle } from "@/lib/endpoint-throttler-decorator"; import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; import { GetTeam } from "@/modules/auth/decorators/get-team/get-team.decorator"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; @@ -25,26 +45,6 @@ import { } from "@/modules/organizations/teams/index/outputs/organization-team.output"; import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service"; import { UserWithProfile } from "@/modules/users/users.repository"; -import { - Controller, - UseGuards, - Get, - Param, - ParseIntPipe, - Query, - Delete, - Patch, - Post, - Body, - Req, -} from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; -import { plainToClass } from "class-transformer"; -import { Request } from "express"; - -import { SUCCESS_STATUS, X_CAL_CLIENT_ID } from "@calcom/platform-constants"; -import { OrgTeamOutputDto, SkipTakePagination } from "@calcom/platform-types"; -import type { Team } from "@calcom/prisma/client"; @Controller({ path: "/v2/organizations/:orgId/teams", @@ -121,6 +121,7 @@ export class OrganizationsTeamsController { @Roles("ORG_ADMIN") @PlatformPlan("ESSENTIALS") @Delete("/:teamId") + @Throttle({ limit: 1, ttl: 30000, blockDuration: 30000, name: "org_teams_delete" }) @ApiOperation({ summary: "Delete a team" }) async deleteTeam( @Param("orgId", ParseIntPipe) orgId: number, diff --git a/apps/api/v2/src/modules/teams/teams/controllers/teams.controller.e2e-spec.ts b/apps/api/v2/src/modules/teams/teams/controllers/teams.controller.e2e-spec.ts index 210ceecf57..84a1c652ad 100644 --- a/apps/api/v2/src/modules/teams/teams/controllers/teams.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/teams/teams/controllers/teams.controller.e2e-spec.ts @@ -12,6 +12,7 @@ import { MembershipRepositoryFixture } from "test/fixtures/repository/membership import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; import { randomString } from "test/utils/randomString"; +import { mockThrottlerGuard } from "test/utils/withNoThrottler"; import { AppModule } from "@/app.module"; import { bootstrap } from "@/bootstrap"; import { StripeService } from "@/modules/stripe/stripe.service"; @@ -40,12 +41,14 @@ describe("Teams endpoint", () => { let team1: TeamOutputDto; let team2: TeamOutputDto; - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [AppModule, TeamsModule], - }).compile(); + beforeAll(async () => { + mockThrottlerGuard(); - jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(() => ({}) as unknown as Stripe); + const moduleRef = await Test.createTestingModule({ + imports: [AppModule, TeamsModule], + }).compile(); + + jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(() => ({}) as unknown as Stripe); userRepositoryFixture = new UserRepositoryFixture(moduleRef); teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); diff --git a/apps/api/v2/src/modules/teams/teams/controllers/teams.controller.ts b/apps/api/v2/src/modules/teams/teams/controllers/teams.controller.ts index 1f747b073c..3b16b790a3 100644 --- a/apps/api/v2/src/modules/teams/teams/controllers/teams.controller.ts +++ b/apps/api/v2/src/modules/teams/teams/controllers/teams.controller.ts @@ -1,5 +1,11 @@ +import { SUCCESS_STATUS } from "@calcom/platform-constants"; +import { TeamOutputDto } from "@calcom/platform-types"; +import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common"; +import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; +import { plainToClass } from "class-transformer"; import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { API_KEY_HEADER } from "@/lib/docs/headers"; +import { Throttle } from "@/lib/endpoint-throttler-decorator"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; @@ -14,12 +20,6 @@ import { UpdateTeamOutput } from "@/modules/teams/teams/outputs/teams/update-tea import { TeamsService } from "@/modules/teams/teams/services/teams.service"; import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; import { UserWithProfile } from "@/modules/users/users.repository"; -import { Controller, UseGuards, Get, Param, ParseIntPipe, Delete, Patch, Post, Body } from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; -import { plainToClass } from "class-transformer"; - -import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { TeamOutputDto } from "@calcom/platform-types"; @Controller({ path: "/v2/teams", @@ -29,7 +29,10 @@ import { TeamOutputDto } from "@calcom/platform-types"; @DocsTags("Teams") @ApiHeader(API_KEY_HEADER) export class TeamsController { - constructor(private teamsService: TeamsService, private teamsRepository: TeamsRepository) {} + constructor( + private teamsService: TeamsService, + private teamsRepository: TeamsRepository + ) {} @Post() @ApiOperation({ summary: "Create a team" }) @@ -95,6 +98,7 @@ export class TeamsController { @UseGuards(RolesGuard) @Delete("/:teamId") + @Throttle({ limit: 1, ttl: 30000, blockDuration: 30000, name: "teams_delete" }) @ApiOperation({ summary: "Delete a team" }) @Roles("TEAM_OWNER") async deleteTeam(@Param("teamId", ParseIntPipe) teamId: number): Promise { diff --git a/apps/api/v2/test/utils/withNoThrottler.ts b/apps/api/v2/test/utils/withNoThrottler.ts new file mode 100644 index 0000000000..ce4e3cdd5d --- /dev/null +++ b/apps/api/v2/test/utils/withNoThrottler.ts @@ -0,0 +1,5 @@ +import { CustomThrottlerGuard } from "@/lib/throttler-guard"; + +export const mockThrottlerGuard = (): void => { + jest.spyOn(CustomThrottlerGuard.prototype, "handleRequest").mockResolvedValue(true); +};