diff --git a/src/common/decorators/auth-guard.decorator.ts b/src/common/decorators/auth-guard.decorator.ts index 944f15d..7d398fa 100755 --- a/src/common/decorators/auth-guard.decorator.ts +++ b/src/common/decorators/auth-guard.decorator.ts @@ -1,5 +1,5 @@ import { UseGuards, applyDecorators } from "@nestjs/common"; -import { ApiBearerAuth } from "@nestjs/swagger"; +import { ApiBearerAuth, ApiHeader } from "@nestjs/swagger"; import { AdminRouteGuard } from "../../modules/auth/guards/admin.guard"; import { JwtAuthGuard } from "../../modules/auth/guards/auth.guard"; @@ -8,6 +8,6 @@ export function AuthGuards() { return applyDecorators( UseGuards(JwtAuthGuard, AdminRouteGuard), ApiBearerAuth("authorization"), - // ApiHeader({ name: "x-business-id", description: "Business ID" }), + ApiHeader({ name: "x-business-id", description: "Business ID" }), ); } diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 112c9c7..52e2a3d 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -422,6 +422,7 @@ export const enum BusinessMessage { QUOTA_PURCHASE_FAILED = "خرید حجم با خطا مواجه شد", PAYMENT_GATEWAY_ERROR = "خطا در اتصال به درگاه پرداخت", QUOTA_UPDATED = "حجم ایمیل با موفقیت به روز شد", + USER_DOES_NOT_HAVE_ACCESS_TO_SERVICE = "کاربر دسترسی به این سرویس را ندارد", } export const enum DomainMessage { diff --git a/src/configs/swagger.config.ts b/src/configs/swagger.config.ts index 9888beb..4d0ae9c 100755 --- a/src/configs/swagger.config.ts +++ b/src/configs/swagger.config.ts @@ -23,6 +23,9 @@ export function getSwaggerDocument(app: NestFastifyApplication) { SwaggerModule.setup("api-docs", app, swaggerDocument, { swaggerOptions: { persistAuthorization: true, + displayRequestDuration: true, + filter: true, + showExtensions: true, }, }); } diff --git a/src/core/interceptors/business.interceptor.ts b/src/core/interceptors/business.interceptor.ts index 0456abb..cac458c 100644 --- a/src/core/interceptors/business.interceptor.ts +++ b/src/core/interceptors/business.interceptor.ts @@ -1,4 +1,4 @@ -import { BadRequestException, CallHandler, ExecutionContext, ForbiddenException, Injectable, Logger, NestInterceptor } from "@nestjs/common"; +import { CallHandler, ExecutionContext, ForbiddenException, Injectable, NestInterceptor } from "@nestjs/common"; import { FastifyRequest } from "fastify"; import { Observable } from "rxjs"; @@ -15,7 +15,6 @@ declare module "fastify" { @Injectable() export class BusinessInterceptor implements NestInterceptor { private readonly headerName = "x-business-id"; - private readonly logger = new Logger(BusinessInterceptor.name); constructor(private readonly businessesService: BusinessesService) {} @@ -24,15 +23,16 @@ export class BusinessInterceptor implements NestInterceptor { const businessId = request.headers[this.headerName.toLocaleLowerCase()] as string; - if (businessId) { - try { - const business = await this.businessesService.getBusinessByDanakSubscriptionId(businessId); + const userId = request.user?.id; - request.business = business; - } catch (error) { - this.logger.error(error); - throw new BadRequestException(BusinessMessage.NOT_FOUND); - } + if (businessId) { + const business = await this.businessesService.getBusinessByDanakSubscriptionId(businessId); + + const hasAccess = await this.businessesService.hasAccessToService(business, userId!); + + if (!hasAccess) throw new ForbiddenException(BusinessMessage.USER_DOES_NOT_HAVE_ACCESS_TO_SERVICE); + + request.business = business; } else { throw new ForbiddenException(BusinessMessage.BUSINESS_ID_REQUIRED); } diff --git a/src/modules/businesses/constant/index.ts b/src/modules/businesses/constant/index.ts index c398437..4f2eafe 100644 --- a/src/modules/businesses/constant/index.ts +++ b/src/modules/businesses/constant/index.ts @@ -5,7 +5,7 @@ export const SUBSCRIPTIONS = Object.freeze({ // SERVICE_SLUG: "Dmail", - SERVICE_ID: "e51afdc3-ea0b-49cf-8f49-2a6f131b024e", + DMAIL_SERVICE_ID: "e51afdc3-ea0b-49cf-8f49-2a6f131b024e", }); export const QUOTA_CONSTANTS = Object.freeze({ diff --git a/src/modules/businesses/entities/business.entity.ts b/src/modules/businesses/entities/business.entity.ts index e8c9763..f875b47 100644 --- a/src/modules/businesses/entities/business.entity.ts +++ b/src/modules/businesses/entities/business.entity.ts @@ -22,13 +22,13 @@ export class Business extends BaseEntity { @Property({ type: "varchar", length: 255, nullable: true }) logoUrl?: string; - @Property({ type: "bigint", default: 1_073_741_824 }) // 1GB default + @Property({ type: "bigint", default: 10_737_418_240 }) // 10GB default quota!: number; @Property({ type: "bigint", default: 0 }) usedQuota!: number & Opt; - @Property({ type: "bigint", default: 1_073_741_824 }) + @Property({ type: "bigint", default: 10_737_418_240 }) remainingQuota!: number & Opt; //========================= diff --git a/src/modules/businesses/interfaces/IBusiness.ts b/src/modules/businesses/interfaces/IBusiness.ts index 9e43106..e64eab4 100644 --- a/src/modules/businesses/interfaces/IBusiness.ts +++ b/src/modules/businesses/interfaces/IBusiness.ts @@ -19,6 +19,15 @@ export enum InvoiceStatus { OVERDUE = "OVERDUE", } +export interface IDanakCheckAccessResponse { + statusCode: number; + success: boolean; + data?: { + hasAccess: boolean; + }; + error?: string; +} + export interface IExternalInvoiceJob { invoice: { id: string; diff --git a/src/modules/businesses/interfaces/IQuotaPurchase.ts b/src/modules/businesses/interfaces/IQuotaPurchase.ts index 15e1dd2..b1ac47a 100644 --- a/src/modules/businesses/interfaces/IQuotaPurchase.ts +++ b/src/modules/businesses/interfaces/IQuotaPurchase.ts @@ -14,6 +14,7 @@ export interface IDanakCorpQuotaPurchaseRequest { } export interface IDanakCorpQuotaPurchaseResponse { + statusCode: number; success: boolean; data?: { invoice: { diff --git a/src/modules/businesses/queue/business-provisioning.processor.ts b/src/modules/businesses/queue/business-provisioning.processor.ts index 3627fed..15983b9 100644 --- a/src/modules/businesses/queue/business-provisioning.processor.ts +++ b/src/modules/businesses/queue/business-provisioning.processor.ts @@ -33,7 +33,7 @@ export class BusinessProvisioningProcessor extends WorkerProcessor { const { subscriptionId, serviceId, serviceName, businessName, slug, userId, fullName, phone, email } = job.data; // Only act if the event is for this service - if (serviceId !== SUBSCRIPTIONS.SERVICE_ID) { + if (serviceId !== SUBSCRIPTIONS.DMAIL_SERVICE_ID) { this.logger.debug(`Skipping job for service: ${serviceName}`); return; } diff --git a/src/modules/businesses/services/businesses.service.ts b/src/modules/businesses/services/businesses.service.ts index 51935da..f1b3db5 100644 --- a/src/modules/businesses/services/businesses.service.ts +++ b/src/modules/businesses/services/businesses.service.ts @@ -1,18 +1,27 @@ import { EntityManager } from "@mikro-orm/postgresql"; -import { BadRequestException, Injectable } from "@nestjs/common"; +import { HttpService } from "@nestjs/axios"; +import { BadRequestException, Inject, Injectable, InternalServerErrorException, Logger } from "@nestjs/common"; +import { AxiosError } from "axios"; +import { catchError, firstValueFrom } from "rxjs"; import { QuotaPurchaseService } from "./quota-purchase.service"; import { BusinessMessage } from "../../../common/enums/message.enum"; -import { QUOTA_CONSTANTS } from "../constant"; +import { DANAKCORP_CONFIG, IDanakCorpConfig } from "../../../configs/danakcorp.config"; +import { QUOTA_CONSTANTS, SUBSCRIPTIONS } from "../constant"; import { PurchaseQuotaDto } from "../DTO/purchase-quota.dto"; import { UpdateBusinessSettingsDto } from "../DTO/update-business-settings.dto"; import { BusinessStaff } from "../entities/business-staff.entity"; +import { Business } from "../entities/business.entity"; +import { IDanakCheckAccessResponse } from "../interfaces/IBusiness"; import { BusinessRepository } from "../repositories/business.repository"; @Injectable() export class BusinessesService { + private readonly logger = new Logger(BusinessesService.name); constructor( private readonly businessRepository: BusinessRepository, + @Inject(DANAKCORP_CONFIG) private readonly danakCorpConfig: IDanakCorpConfig, + private readonly httpService: HttpService, private readonly em: EntityManager, private readonly quotaPurchaseService: QuotaPurchaseService, ) {} @@ -23,6 +32,41 @@ export class BusinessesService { return business; } + + //************************************************** */ + async hasAccessToService(business: Business, userId: string) { + try { + const { data } = await firstValueFrom( + this.httpService + .get(`${this.danakCorpConfig.apiUrl}/subscriptions/check-access/${SUBSCRIPTIONS.DMAIL_SERVICE_ID}`, { + params: { + userId, + danakSubscriptionId: business.danakSubscriptionId, + }, + headers: { + "X-API-Key": this.danakCorpConfig.apiKey, + }, + timeout: this.danakCorpConfig.timeout, + }) + .pipe( + catchError((error: AxiosError) => { + this.logger.error(`Failed to check access to service for business ${business.id}:`, error.message); + throw new InternalServerErrorException(BusinessMessage.USER_DOES_NOT_HAVE_ACCESS_TO_SERVICE); + }), + ), + ); + + if (!data.success || !data.data?.hasAccess) { + this.logger.error(`DanakCorp API returned error for business ${business.id}:`, data.error); + throw new InternalServerErrorException(BusinessMessage.QUOTA_PURCHASE_FAILED); + } + + return data.data?.hasAccess; + } catch (error) { + this.logger.error(`Failed to check access to service for business ${business.id}:`, error); + throw new InternalServerErrorException(BusinessMessage.USER_DOES_NOT_HAVE_ACCESS_TO_SERVICE); + } + } //************************************************** */ async getBusinessBySlug(slug: string) { @@ -48,7 +92,6 @@ export class BusinessesService { async getBusinessSettings(businessId: string) { const business = await this.businessRepository.findOne({ id: businessId, deletedAt: null }); - console.log("business", business); if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND); return { business };