refactor: platform billing controller auth guards (#25559)

* refactor: platform billing controller auth guards

* fixup! refactor: platform billing controller auth guards
This commit is contained in:
Morgan
2025-12-02 20:38:29 +00:00
committed by GitHub
parent e2aa6361d5
commit 68b93385eb
4 changed files with 64 additions and 13 deletions
@@ -2,6 +2,7 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/reposito
import { BillingProcessor } from "@/modules/billing/billing.processor";
import { BillingRepository } from "@/modules/billing/billing.repository";
import { BillingController } from "@/modules/billing/controllers/billing.controller";
import { IsUserInBillingOrg } from "@/modules/billing/guards/is-user-in-billing-org";
import { BillingServiceCachingProxy } from "@/modules/billing/services/billing-service-caching-proxy";
import { BillingConfigService } from "@/modules/billing/services/billing.config.service";
import { BillingService } from "@/modules/billing/services/billing.service";
@@ -44,6 +45,7 @@ import { Module } from "@nestjs/common";
ManagedOrganizationsBillingService,
OAuthClientRepository,
BookingsRepository_2024_08_13,
IsUserInBillingOrg,
],
exports: [BillingService, BillingRepository, ManagedOrganizationsBillingService],
controllers: [BillingController],
@@ -17,7 +17,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 { withNextAuth } from "test/utils/withNextAuth";
import { withApiAuth } from "test/utils/withApiAuth";
import type { Team, PlatformBilling } from "@calcom/prisma/client";
@@ -32,8 +32,10 @@ describe("Platform Billing Controller (e2e)", () => {
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture;
let organization: Team;
let organization2: Team;
beforeAll(async () => {
const moduleRef = await withNextAuth(
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
@@ -49,6 +51,10 @@ describe("Platform Billing Controller (e2e)", () => {
name: `billing-organization-${randomString()}`,
isPlatform: true,
});
organization2 = await organizationsRepositoryFixture.create({
name: `billing-organization-2-${randomString()}`,
isPlatform: true,
});
user = await userRepositoryFixture.create({
email: userEmail,
@@ -133,7 +139,7 @@ describe("Platform Billing Controller (e2e)", () => {
});
});
it("/billing/webhook (GET) should get billing plan for org", () => {
it("/billing/:orgId/check (GET) should check billing plan for org", () => {
return request(app.getHttpServer())
.get(`/v2/billing/${organization.id}/check`)
.expect(200)
@@ -143,6 +149,10 @@ describe("Platform Billing Controller (e2e)", () => {
});
});
it("/billing/:organizationId/check (GET) should not be able to check other org plan", () => {
return request(app.getHttpServer()).get(`/v2/billing/${organization2.id}/check`).expect(403);
});
it("/billing/webhook (POST) failed payment should set billing free plan to overdue", () => {
jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(
() =>
@@ -1,11 +1,13 @@
import { AppConfig } from "@/config/type";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { ApiAuthGuardOnlyAllow } from "@/modules/auth/decorators/api-auth-guard-only-allow.decorator";
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard";
import { SubscribeToPlanInput } from "@/modules/billing/controllers/inputs/subscribe-to-plan.input";
import { CheckPlatformBillingResponseDto } from "@/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto";
import { SubscribeTeamToBillingResponseDto } from "@/modules/billing/controllers/outputs/SubscribeTeamToBillingResponse.dto";
import { IsUserInBillingOrg } from "@/modules/billing/guards/is-user-in-billing-org";
import { IBillingService } from "@/modules/billing/interfaces/billing-service.interface";
import { StripeService } from "@/modules/stripe/stripe.service";
import {
@@ -49,8 +51,9 @@ export class BillingController {
}
@Get("/:teamId/check")
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
@UseGuards(ApiAuthGuard, OrganizationRolesGuard, IsUserInBillingOrg)
@MembershipRoles(["OWNER", "ADMIN", "MEMBER"])
@ApiAuthGuardOnlyAllow(["NEXT_AUTH"])
async checkTeamBilling(
@Param("teamId", ParseIntPipe) teamId: number
): Promise<ApiResponse<CheckPlatformBillingResponseDto>> {
@@ -66,8 +69,9 @@ export class BillingController {
}
@Post("/:teamId/subscribe")
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
@UseGuards(ApiAuthGuard, OrganizationRolesGuard, IsUserInBillingOrg)
@MembershipRoles(["OWNER", "ADMIN"])
@ApiAuthGuardOnlyAllow(["NEXT_AUTH"])
async subscribeTeamToStripe(
@Param("teamId") teamId: number,
@Body() input: SubscribeToPlanInput
@@ -84,8 +88,9 @@ export class BillingController {
}
@Post("/:teamId/upgrade")
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
@UseGuards(ApiAuthGuard, OrganizationRolesGuard, IsUserInBillingOrg)
@MembershipRoles(["OWNER", "ADMIN"])
@ApiAuthGuardOnlyAllow(["NEXT_AUTH"])
async upgradeTeamBillingInStripe(
@Param("teamId") teamId: number,
@Body() input: SubscribeToPlanInput
@@ -100,13 +105,12 @@ export class BillingController {
};
}
@Delete("/:organizationId/unsubscribe")
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
@Delete("/:teamId/unsubscribe")
@UseGuards(ApiAuthGuard, OrganizationRolesGuard, IsUserInBillingOrg)
@MembershipRoles(["OWNER", "ADMIN"])
async cancelTeamSubscriptionInStripe(
@Param("organizationId") organizationId: number
): Promise<ApiResponse> {
await this.billingService.cancelTeamSubscription(organizationId);
@ApiAuthGuardOnlyAllow(["NEXT_AUTH"])
async cancelTeamSubscriptionInStripe(@Param("teamId") teamId: number): Promise<ApiResponse> {
await this.billingService.cancelTeamSubscription(teamId);
return {
status: "success",
@@ -0,0 +1,35 @@
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
import { Request } from "express";
import type { Team } from "@calcom/prisma/client";
@Injectable()
export class IsUserInBillingOrg implements CanActivate {
constructor(private organizationsRepository: OrganizationsRepository) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request & { team: Team }>();
const orgId: string = request.params.teamId;
const userId = (request.user as ApiAuthGuardUser).id;
if (!userId) {
throw new ForbiddenException("IsUserInBillingOrg - No user id found.");
}
if (!orgId) {
throw new ForbiddenException("IsUserInBillingOrg - No org id found in request params.");
}
const user = await this.organizationsRepository.findOrgUser(Number(orgId), Number(userId));
if (!user) {
throw new ForbiddenException(
`IsUserInBillingOrg - user with id=${userId} is not part of the organization with id=${orgId}.`
);
}
return true;
}
}