Skip to content

Commit 68b9338

Browse files
refactor: platform billing controller auth guards (calcom#25559)
* refactor: platform billing controller auth guards * fixup! refactor: platform billing controller auth guards
1 parent e2aa636 commit 68b9338

4 files changed

Lines changed: 64 additions & 13 deletions

File tree

apps/api/v2/src/modules/billing/billing.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/reposito
22
import { BillingProcessor } from "@/modules/billing/billing.processor";
33
import { BillingRepository } from "@/modules/billing/billing.repository";
44
import { BillingController } from "@/modules/billing/controllers/billing.controller";
5+
import { IsUserInBillingOrg } from "@/modules/billing/guards/is-user-in-billing-org";
56
import { BillingServiceCachingProxy } from "@/modules/billing/services/billing-service-caching-proxy";
67
import { BillingConfigService } from "@/modules/billing/services/billing.config.service";
78
import { BillingService } from "@/modules/billing/services/billing.service";
@@ -44,6 +45,7 @@ import { Module } from "@nestjs/common";
4445
ManagedOrganizationsBillingService,
4546
OAuthClientRepository,
4647
BookingsRepository_2024_08_13,
48+
IsUserInBillingOrg,
4749
],
4850
exports: [BillingService, BillingRepository, ManagedOrganizationsBillingService],
4951
controllers: [BillingController],

apps/api/v2/src/modules/billing/controllers/billing.controller.e2e-spec.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { OrganizationRepositoryFixture } from "test/fixtures/repository/organiza
1717
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
1818
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
1919
import { randomString } from "test/utils/randomString";
20-
import { withNextAuth } from "test/utils/withNextAuth";
20+
import { withApiAuth } from "test/utils/withApiAuth";
2121

2222
import type { Team, PlatformBilling } from "@calcom/prisma/client";
2323

@@ -32,8 +32,10 @@ describe("Platform Billing Controller (e2e)", () => {
3232
let membershipsRepositoryFixture: MembershipRepositoryFixture;
3333
let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture;
3434
let organization: Team;
35+
let organization2: Team;
36+
3537
beforeAll(async () => {
36-
const moduleRef = await withNextAuth(
38+
const moduleRef = await withApiAuth(
3739
userEmail,
3840
Test.createTestingModule({
3941
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
@@ -49,6 +51,10 @@ describe("Platform Billing Controller (e2e)", () => {
4951
name: `billing-organization-${randomString()}`,
5052
isPlatform: true,
5153
});
54+
organization2 = await organizationsRepositoryFixture.create({
55+
name: `billing-organization-2-${randomString()}`,
56+
isPlatform: true,
57+
});
5258

5359
user = await userRepositoryFixture.create({
5460
email: userEmail,
@@ -133,7 +139,7 @@ describe("Platform Billing Controller (e2e)", () => {
133139
});
134140
});
135141

136-
it("/billing/webhook (GET) should get billing plan for org", () => {
142+
it("/billing/:orgId/check (GET) should check billing plan for org", () => {
137143
return request(app.getHttpServer())
138144
.get(`/v2/billing/${organization.id}/check`)
139145
.expect(200)
@@ -143,6 +149,10 @@ describe("Platform Billing Controller (e2e)", () => {
143149
});
144150
});
145151

152+
it("/billing/:organizationId/check (GET) should not be able to check other org plan", () => {
153+
return request(app.getHttpServer()).get(`/v2/billing/${organization2.id}/check`).expect(403);
154+
});
155+
146156
it("/billing/webhook (POST) failed payment should set billing free plan to overdue", () => {
147157
jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(
148158
() =>

apps/api/v2/src/modules/billing/controllers/billing.controller.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { AppConfig } from "@/config/type";
22
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
3+
import { ApiAuthGuardOnlyAllow } from "@/modules/auth/decorators/api-auth-guard-only-allow.decorator";
34
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
4-
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
5+
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
56
import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard";
67
import { SubscribeToPlanInput } from "@/modules/billing/controllers/inputs/subscribe-to-plan.input";
78
import { CheckPlatformBillingResponseDto } from "@/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto";
89
import { SubscribeTeamToBillingResponseDto } from "@/modules/billing/controllers/outputs/SubscribeTeamToBillingResponse.dto";
10+
import { IsUserInBillingOrg } from "@/modules/billing/guards/is-user-in-billing-org";
911
import { IBillingService } from "@/modules/billing/interfaces/billing-service.interface";
1012
import { StripeService } from "@/modules/stripe/stripe.service";
1113
import {
@@ -49,8 +51,9 @@ export class BillingController {
4951
}
5052

5153
@Get("/:teamId/check")
52-
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
54+
@UseGuards(ApiAuthGuard, OrganizationRolesGuard, IsUserInBillingOrg)
5355
@MembershipRoles(["OWNER", "ADMIN", "MEMBER"])
56+
@ApiAuthGuardOnlyAllow(["NEXT_AUTH"])
5457
async checkTeamBilling(
5558
@Param("teamId", ParseIntPipe) teamId: number
5659
): Promise<ApiResponse<CheckPlatformBillingResponseDto>> {
@@ -66,8 +69,9 @@ export class BillingController {
6669
}
6770

6871
@Post("/:teamId/subscribe")
69-
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
72+
@UseGuards(ApiAuthGuard, OrganizationRolesGuard, IsUserInBillingOrg)
7073
@MembershipRoles(["OWNER", "ADMIN"])
74+
@ApiAuthGuardOnlyAllow(["NEXT_AUTH"])
7175
async subscribeTeamToStripe(
7276
@Param("teamId") teamId: number,
7377
@Body() input: SubscribeToPlanInput
@@ -84,8 +88,9 @@ export class BillingController {
8488
}
8589

8690
@Post("/:teamId/upgrade")
87-
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
91+
@UseGuards(ApiAuthGuard, OrganizationRolesGuard, IsUserInBillingOrg)
8892
@MembershipRoles(["OWNER", "ADMIN"])
93+
@ApiAuthGuardOnlyAllow(["NEXT_AUTH"])
8994
async upgradeTeamBillingInStripe(
9095
@Param("teamId") teamId: number,
9196
@Body() input: SubscribeToPlanInput
@@ -100,13 +105,12 @@ export class BillingController {
100105
};
101106
}
102107

103-
@Delete("/:organizationId/unsubscribe")
104-
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
108+
@Delete("/:teamId/unsubscribe")
109+
@UseGuards(ApiAuthGuard, OrganizationRolesGuard, IsUserInBillingOrg)
105110
@MembershipRoles(["OWNER", "ADMIN"])
106-
async cancelTeamSubscriptionInStripe(
107-
@Param("organizationId") organizationId: number
108-
): Promise<ApiResponse> {
109-
await this.billingService.cancelTeamSubscription(organizationId);
111+
@ApiAuthGuardOnlyAllow(["NEXT_AUTH"])
112+
async cancelTeamSubscriptionInStripe(@Param("teamId") teamId: number): Promise<ApiResponse> {
113+
await this.billingService.cancelTeamSubscription(teamId);
110114

111115
return {
112116
status: "success",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
2+
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
3+
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
4+
import { Request } from "express";
5+
6+
import type { Team } from "@calcom/prisma/client";
7+
8+
@Injectable()
9+
export class IsUserInBillingOrg implements CanActivate {
10+
constructor(private organizationsRepository: OrganizationsRepository) {}
11+
12+
async canActivate(context: ExecutionContext): Promise<boolean> {
13+
const request = context.switchToHttp().getRequest<Request & { team: Team }>();
14+
const orgId: string = request.params.teamId;
15+
const userId = (request.user as ApiAuthGuardUser).id;
16+
17+
if (!userId) {
18+
throw new ForbiddenException("IsUserInBillingOrg - No user id found.");
19+
}
20+
21+
if (!orgId) {
22+
throw new ForbiddenException("IsUserInBillingOrg - No org id found in request params.");
23+
}
24+
25+
const user = await this.organizationsRepository.findOrgUser(Number(orgId), Number(userId));
26+
27+
if (!user) {
28+
throw new ForbiddenException(
29+
`IsUserInBillingOrg - user with id=${userId} is not part of the organization with id=${orgId}.`
30+
);
31+
}
32+
33+
return true;
34+
}
35+
}

0 commit comments

Comments
 (0)