diff --git a/backend/src/entities/company-info/company-info.controller.ts b/backend/src/entities/company-info/company-info.controller.ts index 3cc94741a..e17e927a4 100644 --- a/backend/src/entities/company-info/company-info.controller.ts +++ b/backend/src/entities/company-info/company-info.controller.ts @@ -83,6 +83,7 @@ import { import { AddCompanyTabTitleDto } from './application/data-structures/add-company-tab-title.dto.js'; import { FoundCompanyTabTitleRO } from './application/data-structures/found-company-tab-title.ro.js'; import { FoundCompanyWhiteLabelPropertiesRO } from './application/dto/found-company-white-label-properties.ro.js'; +import { PaidFeatureGuard } from '../../guards/paid-feature.guard.js'; @UseInterceptors(SentryInterceptor) @Controller('company') @@ -497,7 +498,7 @@ export class CompanyInfoController { type: SuccessResponse, }) @ApiParam({ name: 'companyId', required: true }) - @UseGuards(CompanyAdminGuard) + @UseGuards(CompanyAdminGuard, PaidFeatureGuard) @Post('/logo/:companyId') @UseInterceptors(FileInterceptor('file')) async uploadCompanyLogo( @@ -549,7 +550,7 @@ export class CompanyInfoController { type: SuccessResponse, }) @ApiParam({ name: 'companyId', required: true }) - @UseGuards(CompanyAdminGuard) + @UseGuards(CompanyAdminGuard, PaidFeatureGuard) @Post('/favicon/:companyId') @UseInterceptors(FileInterceptor('file')) async uploadCompanyFavicon( @@ -602,7 +603,7 @@ export class CompanyInfoController { }) @ApiBody({ type: AddCompanyTabTitleDto }) @ApiParam({ name: 'companyId', required: true }) - @UseGuards(CompanyAdminGuard) + @UseGuards(CompanyAdminGuard, PaidFeatureGuard) @Post('/tab-title/:companyId') async addCompanyTabTitle( @SlugUuid('companyId') companyId: string, diff --git a/backend/src/exceptions/custom-exceptions/non-available-in-free-plan-exception.ts b/backend/src/exceptions/custom-exceptions/non-available-in-free-plan-exception.ts index 19b54d02b..3c3869c25 100644 --- a/backend/src/exceptions/custom-exceptions/non-available-in-free-plan-exception.ts +++ b/backend/src/exceptions/custom-exceptions/non-available-in-free-plan-exception.ts @@ -1,7 +1,8 @@ import { HttpException, HttpStatus } from '@nestjs/common'; +import { Messages } from '../text/messages.js'; export class NonAvailableInFreePlanException extends HttpException { - constructor(message: string) { + constructor(message: string = Messages.FEATURE_NON_AVAILABLE_IN_FREE_PLAN) { super(message, HttpStatus.PAYMENT_REQUIRED); } } diff --git a/backend/src/exceptions/text/messages.ts b/backend/src/exceptions/text/messages.ts index 87fe94110..d47adc5d6 100644 --- a/backend/src/exceptions/text/messages.ts +++ b/backend/src/exceptions/text/messages.ts @@ -6,7 +6,7 @@ import { QueryOrderingEnum, TableActionTypeEnum, UserActionEnum, - WidgetTypeEnum + WidgetTypeEnum, } from '../../enums/index.js'; import { TableActionEventEnum } from '../../enums/table-action-event-enum.js'; import { TableActionMethodEnum } from '../../enums/table-action-method-enum.js'; @@ -364,4 +364,5 @@ export const Messages = { `Invalid event type ${type}, supported types are ${enumToString(TableActionEventEnum)}`, INVALID_REQUEST_DOMAIN: `Invalid request domain`, INVALID_REQUEST_DOMAIN_FORMAT: `Invalid request domain format`, + FEATURE_NON_AVAILABLE_IN_FREE_PLAN: `This feature is not available in free plan.`, }; diff --git a/backend/src/guards/index.ts b/backend/src/guards/index.ts index 6833c7d72..f9844701e 100644 --- a/backend/src/guards/index.ts +++ b/backend/src/guards/index.ts @@ -6,3 +6,4 @@ export { TableAddGuard } from './table-add.guard.js'; export { TableDeleteGuard } from './table-delete.guard.js'; export { TableEditGuard } from './table-edit.guard.js'; export { TableReadGuard } from './table-read.guard.js'; +export { PaidFeatureGuard } from './paid-feature.guard.js'; diff --git a/backend/src/guards/paid-feature.guard.ts b/backend/src/guards/paid-feature.guard.ts new file mode 100644 index 000000000..c6d1c06a9 --- /dev/null +++ b/backend/src/guards/paid-feature.guard.ts @@ -0,0 +1,59 @@ +import { BadRequestException, CanActivate, ExecutionContext, Inject, Injectable } from '@nestjs/common'; +import { BaseType } from '../common/data-injection.tokens.js'; +import { IGlobalDatabaseContext } from '../common/application/global-database-context.interface.js'; +import { Observable } from 'rxjs'; +import { IRequestWithCognitoInfo } from '../authorization/cognito-decoded.interface.js'; +import { validateUuidByRegex } from './utils/validate-uuid-by-regex.js'; +import { SaasCompanyGatewayService } from '../microservices/gateways/saas-gateway.ts/saas-company-gateway.service.js'; +import { isSaaS } from '../helpers/app/is-saas.js'; +import { SubscriptionLevelEnum } from '../enums/subscription-level.enum.js'; +import { Messages } from '../exceptions/text/messages.js'; +import { NonAvailableInFreePlanException } from '../exceptions/custom-exceptions/non-available-in-free-plan-exception.js'; + +@Injectable() +export class PaidFeatureGuard implements CanActivate { + constructor( + @Inject(BaseType.GLOBAL_DB_CONTEXT) + protected _dbContext: IGlobalDatabaseContext, + private readonly saasCompanyGatewayService: SaasCompanyGatewayService, + ) {} + + canActivate(context: ExecutionContext): boolean | Promise | Observable { + return new Promise(async (resolve, reject) => { + const request: IRequestWithCognitoInfo = context.switchToHttp().getRequest(); + const userId: string = request.decoded.sub; + let companyId: string = request.params?.companyId || request.params?.slug; + if (!companyId || !validateUuidByRegex(companyId)) { + companyId = request.body?.['companyId']; + } + if (!companyId || !validateUuidByRegex(companyId)) { + const foundCompanyInfo = await this._dbContext.companyInfoRepository.findCompanyInfoByUserId(userId); + companyId = foundCompanyInfo?.id; + } + if (!companyId || !validateUuidByRegex(companyId)) { + reject(new BadRequestException(Messages.COMPANY_ID_MISSING)); + return; + } + if (!isSaaS()) { + resolve(true); + return; + } + try { + const companyInfo = await this.saasCompanyGatewayService.getCompanyInfo(companyId); + if (!companyInfo) { + reject(new BadRequestException(Messages.COMPANY_NOT_FOUND)); + return; + } + if (companyInfo.subscriptionLevel === SubscriptionLevelEnum.FREE_PLAN) { + reject(new NonAvailableInFreePlanException()); + return; + } + console.log('PaidFeatureGuard: Company has a paid subscription'); + resolve(true); + } catch (e) { + reject(e); + } + return; + }); + } +}