chore: add new feature for checking the user has access to the service or not
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { UseGuards, applyDecorators } from "@nestjs/common";
|
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 { AdminRouteGuard } from "../../modules/auth/guards/admin.guard";
|
||||||
import { JwtAuthGuard } from "../../modules/auth/guards/auth.guard";
|
import { JwtAuthGuard } from "../../modules/auth/guards/auth.guard";
|
||||||
@@ -8,6 +8,6 @@ export function AuthGuards() {
|
|||||||
return applyDecorators(
|
return applyDecorators(
|
||||||
UseGuards(JwtAuthGuard, AdminRouteGuard),
|
UseGuards(JwtAuthGuard, AdminRouteGuard),
|
||||||
ApiBearerAuth("authorization"),
|
ApiBearerAuth("authorization"),
|
||||||
// ApiHeader({ name: "x-business-id", description: "Business ID" }),
|
ApiHeader({ name: "x-business-id", description: "Business ID" }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -422,6 +422,7 @@ export const enum BusinessMessage {
|
|||||||
QUOTA_PURCHASE_FAILED = "خرید حجم با خطا مواجه شد",
|
QUOTA_PURCHASE_FAILED = "خرید حجم با خطا مواجه شد",
|
||||||
PAYMENT_GATEWAY_ERROR = "خطا در اتصال به درگاه پرداخت",
|
PAYMENT_GATEWAY_ERROR = "خطا در اتصال به درگاه پرداخت",
|
||||||
QUOTA_UPDATED = "حجم ایمیل با موفقیت به روز شد",
|
QUOTA_UPDATED = "حجم ایمیل با موفقیت به روز شد",
|
||||||
|
USER_DOES_NOT_HAVE_ACCESS_TO_SERVICE = "کاربر دسترسی به این سرویس را ندارد",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum DomainMessage {
|
export const enum DomainMessage {
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ export function getSwaggerDocument(app: NestFastifyApplication) {
|
|||||||
SwaggerModule.setup("api-docs", app, swaggerDocument, {
|
SwaggerModule.setup("api-docs", app, swaggerDocument, {
|
||||||
swaggerOptions: {
|
swaggerOptions: {
|
||||||
persistAuthorization: true,
|
persistAuthorization: true,
|
||||||
|
displayRequestDuration: true,
|
||||||
|
filter: true,
|
||||||
|
showExtensions: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { FastifyRequest } from "fastify";
|
||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
@@ -15,7 +15,6 @@ declare module "fastify" {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class BusinessInterceptor implements NestInterceptor {
|
export class BusinessInterceptor implements NestInterceptor {
|
||||||
private readonly headerName = "x-business-id";
|
private readonly headerName = "x-business-id";
|
||||||
private readonly logger = new Logger(BusinessInterceptor.name);
|
|
||||||
|
|
||||||
constructor(private readonly businessesService: BusinessesService) {}
|
constructor(private readonly businessesService: BusinessesService) {}
|
||||||
|
|
||||||
@@ -24,15 +23,16 @@ export class BusinessInterceptor implements NestInterceptor {
|
|||||||
|
|
||||||
const businessId = request.headers[this.headerName.toLocaleLowerCase()] as string;
|
const businessId = request.headers[this.headerName.toLocaleLowerCase()] as string;
|
||||||
|
|
||||||
|
const userId = request.user?.id;
|
||||||
|
|
||||||
if (businessId) {
|
if (businessId) {
|
||||||
try {
|
|
||||||
const business = await this.businessesService.getBusinessByDanakSubscriptionId(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;
|
request.business = business;
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(error);
|
|
||||||
throw new BadRequestException(BusinessMessage.NOT_FOUND);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw new ForbiddenException(BusinessMessage.BUSINESS_ID_REQUIRED);
|
throw new ForbiddenException(BusinessMessage.BUSINESS_ID_REQUIRED);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const SUBSCRIPTIONS = Object.freeze({
|
|||||||
|
|
||||||
//
|
//
|
||||||
SERVICE_SLUG: "Dmail",
|
SERVICE_SLUG: "Dmail",
|
||||||
SERVICE_ID: "e51afdc3-ea0b-49cf-8f49-2a6f131b024e",
|
DMAIL_SERVICE_ID: "e51afdc3-ea0b-49cf-8f49-2a6f131b024e",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const QUOTA_CONSTANTS = Object.freeze({
|
export const QUOTA_CONSTANTS = Object.freeze({
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ export class Business extends BaseEntity {
|
|||||||
@Property({ type: "varchar", length: 255, nullable: true })
|
@Property({ type: "varchar", length: 255, nullable: true })
|
||||||
logoUrl?: string;
|
logoUrl?: string;
|
||||||
|
|
||||||
@Property({ type: "bigint", default: 1_073_741_824 }) // 1GB default
|
@Property({ type: "bigint", default: 10_737_418_240 }) // 10GB default
|
||||||
quota!: number;
|
quota!: number;
|
||||||
|
|
||||||
@Property({ type: "bigint", default: 0 })
|
@Property({ type: "bigint", default: 0 })
|
||||||
usedQuota!: number & Opt;
|
usedQuota!: number & Opt;
|
||||||
|
|
||||||
@Property({ type: "bigint", default: 1_073_741_824 })
|
@Property({ type: "bigint", default: 10_737_418_240 })
|
||||||
remainingQuota!: number & Opt;
|
remainingQuota!: number & Opt;
|
||||||
|
|
||||||
//=========================
|
//=========================
|
||||||
|
|||||||
@@ -19,6 +19,15 @@ export enum InvoiceStatus {
|
|||||||
OVERDUE = "OVERDUE",
|
OVERDUE = "OVERDUE",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IDanakCheckAccessResponse {
|
||||||
|
statusCode: number;
|
||||||
|
success: boolean;
|
||||||
|
data?: {
|
||||||
|
hasAccess: boolean;
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IExternalInvoiceJob {
|
export interface IExternalInvoiceJob {
|
||||||
invoice: {
|
invoice: {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface IDanakCorpQuotaPurchaseRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IDanakCorpQuotaPurchaseResponse {
|
export interface IDanakCorpQuotaPurchaseResponse {
|
||||||
|
statusCode: number;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data?: {
|
data?: {
|
||||||
invoice: {
|
invoice: {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export class BusinessProvisioningProcessor extends WorkerProcessor {
|
|||||||
const { subscriptionId, serviceId, serviceName, businessName, slug, userId, fullName, phone, email } = job.data;
|
const { subscriptionId, serviceId, serviceName, businessName, slug, userId, fullName, phone, email } = job.data;
|
||||||
|
|
||||||
// Only act if the event is for this service
|
// 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}`);
|
this.logger.debug(`Skipping job for service: ${serviceName}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
import { EntityManager } from "@mikro-orm/postgresql";
|
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 { QuotaPurchaseService } from "./quota-purchase.service";
|
||||||
import { BusinessMessage } from "../../../common/enums/message.enum";
|
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 { PurchaseQuotaDto } from "../DTO/purchase-quota.dto";
|
||||||
import { UpdateBusinessSettingsDto } from "../DTO/update-business-settings.dto";
|
import { UpdateBusinessSettingsDto } from "../DTO/update-business-settings.dto";
|
||||||
import { BusinessStaff } from "../entities/business-staff.entity";
|
import { BusinessStaff } from "../entities/business-staff.entity";
|
||||||
|
import { Business } from "../entities/business.entity";
|
||||||
|
import { IDanakCheckAccessResponse } from "../interfaces/IBusiness";
|
||||||
import { BusinessRepository } from "../repositories/business.repository";
|
import { BusinessRepository } from "../repositories/business.repository";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BusinessesService {
|
export class BusinessesService {
|
||||||
|
private readonly logger = new Logger(BusinessesService.name);
|
||||||
constructor(
|
constructor(
|
||||||
private readonly businessRepository: BusinessRepository,
|
private readonly businessRepository: BusinessRepository,
|
||||||
|
@Inject(DANAKCORP_CONFIG) private readonly danakCorpConfig: IDanakCorpConfig,
|
||||||
|
private readonly httpService: HttpService,
|
||||||
private readonly em: EntityManager,
|
private readonly em: EntityManager,
|
||||||
private readonly quotaPurchaseService: QuotaPurchaseService,
|
private readonly quotaPurchaseService: QuotaPurchaseService,
|
||||||
) {}
|
) {}
|
||||||
@@ -23,6 +32,41 @@ export class BusinessesService {
|
|||||||
|
|
||||||
return business;
|
return business;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//************************************************** */
|
||||||
|
async hasAccessToService(business: Business, userId: string) {
|
||||||
|
try {
|
||||||
|
const { data } = await firstValueFrom(
|
||||||
|
this.httpService
|
||||||
|
.get<IDanakCheckAccessResponse>(`${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) {
|
async getBusinessBySlug(slug: string) {
|
||||||
@@ -48,7 +92,6 @@ export class BusinessesService {
|
|||||||
|
|
||||||
async getBusinessSettings(businessId: string) {
|
async getBusinessSettings(businessId: string) {
|
||||||
const business = await this.businessRepository.findOne({ id: businessId, deletedAt: null });
|
const business = await this.businessRepository.findOne({ id: businessId, deletedAt: null });
|
||||||
console.log("business", business);
|
|
||||||
if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND);
|
if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND);
|
||||||
|
|
||||||
return { business };
|
return { business };
|
||||||
|
|||||||
Reference in New Issue
Block a user