Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions backend/src/entities/company-info/company-info.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
3 changes: 2 additions & 1 deletion backend/src/exceptions/text/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.`,
};
1 change: 1 addition & 0 deletions backend/src/guards/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
59 changes: 59 additions & 0 deletions backend/src/guards/paid-feature.guard.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> | Observable<boolean> {
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;
});
}
}