From ba9aa004fec26f741a7b76f5786ab55bc604ee5d Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Mon, 16 Mar 2026 14:48:51 +0330 Subject: [PATCH] add : business list for super admin --- src/common/decorators/auth-guard.decorator.ts | 3 +- .../decorators/super-admin.decorator.ts | 4 + .../auth/guards/superAdminAuth.guard.ts | 78 +++++++++++++++++++ src/modules/auth/services/auth.service.ts | 2 +- .../businesses/DTO/find-businesses.dto.ts | 35 +++++++++ .../businesses/businesses.controller.ts | 14 +++- .../repositories/business.repository.ts | 37 ++++++++- .../businesses/services/businesses.service.ts | 7 +- 8 files changed, 172 insertions(+), 8 deletions(-) create mode 100755 src/common/decorators/super-admin.decorator.ts create mode 100644 src/modules/auth/guards/superAdminAuth.guard.ts create mode 100644 src/modules/businesses/DTO/find-businesses.dto.ts diff --git a/src/common/decorators/auth-guard.decorator.ts b/src/common/decorators/auth-guard.decorator.ts index 7d398fa..6ea0a87 100755 --- a/src/common/decorators/auth-guard.decorator.ts +++ b/src/common/decorators/auth-guard.decorator.ts @@ -3,10 +3,11 @@ import { ApiBearerAuth, ApiHeader } from "@nestjs/swagger"; import { AdminRouteGuard } from "../../modules/auth/guards/admin.guard"; import { JwtAuthGuard } from "../../modules/auth/guards/auth.guard"; +import { SuperAdminAuthGuard } from "../../modules/auth/guards/superAdminAuth.guard"; export function AuthGuards() { return applyDecorators( - UseGuards(JwtAuthGuard, AdminRouteGuard), + UseGuards(JwtAuthGuard, AdminRouteGuard, SuperAdminAuthGuard), ApiBearerAuth("authorization"), ApiHeader({ name: "x-business-id", description: "Business ID" }), ); diff --git a/src/common/decorators/super-admin.decorator.ts b/src/common/decorators/super-admin.decorator.ts new file mode 100755 index 0000000..57a3561 --- /dev/null +++ b/src/common/decorators/super-admin.decorator.ts @@ -0,0 +1,4 @@ +export const SUPER_ADMIN_ROUTE = "shouldSuperAdmin"; +import { SetMetadata } from "@nestjs/common"; + +export const SuperAdminRoute = (isSuperAdminRoute: boolean = true) => SetMetadata(SUPER_ADMIN_ROUTE, isSuperAdminRoute); diff --git a/src/modules/auth/guards/superAdminAuth.guard.ts b/src/modules/auth/guards/superAdminAuth.guard.ts new file mode 100644 index 0000000..48a98ee --- /dev/null +++ b/src/modules/auth/guards/superAdminAuth.guard.ts @@ -0,0 +1,78 @@ +import { CanActivate, ExecutionContext, Injectable, UnauthorizedException, Inject, Logger } from '@nestjs/common'; +import { Request } from 'express'; +import { ConfigService } from '@nestjs/config'; +import { Reflector } from '@nestjs/core'; +import { SuperAdminRoute } from '../../../common/decorators/super-admin.decorator'; + +@Injectable() +export class SuperAdminAuthGuard implements CanActivate { + private readonly logger = new Logger(SuperAdminAuthGuard.name); + + constructor( + @Inject(ConfigService) + private readonly configService: ConfigService, + private reflector:Reflector + ) {} + + canActivate(context: ExecutionContext): boolean { + + const requiredSuperAdmin = this.reflector.getAllAndOverride(SuperAdminRoute, [context.getHandler(), context.getClass()]); + if (!requiredSuperAdmin) return true; + + + const request = context.switchToHttp().getRequest(); + + + try { + + const credentials = this.extractBasicAuthCredentials(request); + if (!credentials) { + throw new UnauthorizedException('Basic authentication required'); + } + + const { username, password } = credentials; + + const expectedUsername = this.configService.get('SUPER_ADMIN_USERNAME') ?? 'danak@dsc.com'; + const expectedPassword = this.configService.get('SUPER_ADMIN_PASSWORD') ?? 'DsCdAnAk?@ABC'; + + if (username !== expectedUsername || password !== expectedPassword) { + this.logger.warn(`Invalid super admin credentials attempt for username: ${username}`); + throw new UnauthorizedException('Invalid credentials'); + } + + return true; + } catch (err) { + if (err instanceof UnauthorizedException) { + throw err; + } + this.logger.error('error in SuperAdminAuthGuard', err); + throw new UnauthorizedException('Authentication failed'); + } + } + + private extractBasicAuthCredentials(request: Request): { username: string; password: string } | null { + const authHeader = request.headers.authorization; + if (!authHeader) { + return null; + } + + const [type, encoded] = authHeader.split(' '); + if (type?.toLowerCase() !== 'basic' || !encoded) { + return null; + } + + try { + const decoded = Buffer.from(encoded, 'base64').toString('utf-8'); + const [username, password] = decoded.split(':'); + + if (!username || !password) { + return null; + } + + return { username, password }; + } catch (error) { + this.logger.error('Failed to decode Basic Auth credentials', error); + return null; + } + } +} diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index 0b4f611..fa3ab64 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -33,7 +33,7 @@ export class AuthService { const user = await this.checkUserLoginCredentialWithEmail(email, password); - if (!user.isActive) throw new BadRequestException(AuthMessage.USER_ACCOUNT_INACTIVE); + if (!user.isActive) throw new BadRequestException(AuthMessage.USER_ACCOUNT_INACTIVE); if (user.is2FAEnabled) { if (!twoFactorToken) { diff --git a/src/modules/businesses/DTO/find-businesses.dto.ts b/src/modules/businesses/DTO/find-businesses.dto.ts new file mode 100644 index 0000000..9b3a577 --- /dev/null +++ b/src/modules/businesses/DTO/find-businesses.dto.ts @@ -0,0 +1,35 @@ +import { IsOptional, IsNumber, IsString, IsIn, IsBoolean } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +export class FindBusinessesDto { + @IsOptional() + @IsNumber() + @Type(() => Number) + @ApiPropertyOptional({ example: 1 }) + page?: number; + + @IsOptional() + @IsNumber() + @Type(() => Number) + @ApiPropertyOptional({ example: 10 }) + limit?: number; + + + @IsOptional() + @IsString() + @ApiPropertyOptional({ example: 'createdAt' }) + orderBy?: string; + + @IsOptional() + @IsIn(['asc', 'desc']) + @ApiPropertyOptional({ example: 'desc' }) + order?: 'asc' | 'desc'; + + + @IsOptional() + @IsBoolean() + @Type(() => Boolean) + @ApiPropertyOptional({ example: true }) + isActive?: boolean; +} diff --git a/src/modules/businesses/businesses.controller.ts b/src/modules/businesses/businesses.controller.ts index d7bae5c..31e0581 100644 --- a/src/modules/businesses/businesses.controller.ts +++ b/src/modules/businesses/businesses.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Param, Patch, Post, UseInterceptors } from "@nestjs/common"; +import { Body, Controller, Get, Param, Patch, Post, Query, UseInterceptors } from "@nestjs/common"; import { ApiOperation, ApiResponse } from "@nestjs/swagger"; import { BusinessQuotaResponseDto } from "./DTO/business-quota-response.dto"; @@ -10,10 +10,12 @@ import { AdminRoute } from "../../common/decorators/admin.decorator"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { BusinessDec } from "../../common/decorators/business.decorator"; import { BusinessInterceptor } from "../../core/interceptors/business.interceptor"; +import { SuperAdminRoute } from "../../common/decorators/super-admin.decorator"; +import { FindBusinessesDto } from "./DTO/find-businesses.dto"; @Controller("business") export class BusinessesController { - constructor(private readonly businessesService: BusinessesService) {} + constructor(private readonly businessesService: BusinessesService) { } @Get("slug/:slug") @ApiOperation({ summary: "Get business by slug" }) @@ -58,4 +60,12 @@ export class BusinessesController { purchaseQuota(@BusinessDec("id") businessId: string, @Body() purchaseDto: PurchaseQuotaDto) { return this.businessesService.purchaseQuota(businessId, purchaseDto); } + + @Get("list") + @AuthGuards() + @SuperAdminRoute() + @ApiOperation({ summary: "Get Paginated list of businesses" }) + getPaginatedList(@Query() query: FindBusinessesDto) { + return this.businessesService.findBusinesses(query) + } } diff --git a/src/modules/businesses/repositories/business.repository.ts b/src/modules/businesses/repositories/business.repository.ts index 8afcdc0..f560238 100644 --- a/src/modules/businesses/repositories/business.repository.ts +++ b/src/modules/businesses/repositories/business.repository.ts @@ -1,5 +1,36 @@ -import { EntityRepository } from "@mikro-orm/postgresql"; - +import { EntityRepository, FilterQuery } from "@mikro-orm/postgresql"; import { Business } from "../entities/business.entity"; +import { FindBusinessesDto } from "../DTO/find-businesses.dto"; -export class BusinessRepository extends EntityRepository {} +export class BusinessRepository extends EntityRepository { + + async findAllPaginated(opts: FindBusinessesDto = {}) { + + const { page = 1, limit = 10, orderBy = 'createdAt', order = 'desc' } = opts; + + const offset = (page - 1) * limit; + + const where: FilterQuery = {}; + + const [data, total] = await this.findAndCount(where, { + limit, + offset, + orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' }, + populate: [], + }); + + const totalPages = Math.ceil(total / limit); + + return { + data, + meta: { + total, + page, + limit, + totalPages, + }, + }; + + + } +} diff --git a/src/modules/businesses/services/businesses.service.ts b/src/modules/businesses/services/businesses.service.ts index f1b3db5..fc675ae 100644 --- a/src/modules/businesses/services/businesses.service.ts +++ b/src/modules/businesses/services/businesses.service.ts @@ -14,6 +14,7 @@ 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 { FindBusinessesDto } from "../DTO/find-businesses.dto"; @Injectable() export class BusinessesService { @@ -24,7 +25,7 @@ export class BusinessesService { private readonly httpService: HttpService, private readonly em: EntityManager, private readonly quotaPurchaseService: QuotaPurchaseService, - ) {} + ) { } async getBusinessByDanakSubscriptionId(danakSubscriptionId: string) { const business = await this.businessRepository.findOne({ danakSubscriptionId, deletedAt: null }); @@ -162,4 +163,8 @@ export class BusinessesService { return this.quotaPurchaseService.purchaseQuota(business, purchaseDto); } + //********************************************* */ + async findBusinesses(query: FindBusinessesDto) { + return this.businessRepository.findAllPaginated(query) + } }