From bb42b393c472c78ecb6653d62bf7bb86b6423dc3 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Mon, 13 Apr 2026 11:57:13 +0330 Subject: [PATCH] add : reseller --- src/common/decorators/auth-guard.decorator.ts | 3 +- src/common/decorators/reseller.decorator.ts | 4 ++ src/modules/auth/auth.controller.ts | 8 ++-- src/modules/auth/guards/reseller.guard.ts | 26 +++++++++++ src/modules/auth/interfaces/IToken-payload.ts | 1 + src/modules/auth/providers/auth.service.ts | 9 ++-- src/modules/auth/providers/sso.service.ts | 2 + src/modules/auth/providers/tokens.service.ts | 15 ++++++- .../reseller/dto/create-reseller-agent.dto.ts | 9 ++++ src/modules/reseller/reseller.controller.ts | 41 ++++++++++++++--- src/modules/reseller/reseller.service.ts | 45 ++++++++++++++++++- .../providers/subscriptions.service.ts | 2 +- src/modules/users/providers/users.service.ts | 8 ++-- 13 files changed, 152 insertions(+), 21 deletions(-) create mode 100755 src/common/decorators/reseller.decorator.ts create mode 100755 src/modules/auth/guards/reseller.guard.ts create mode 100755 src/modules/reseller/dto/create-reseller-agent.dto.ts diff --git a/src/common/decorators/auth-guard.decorator.ts b/src/common/decorators/auth-guard.decorator.ts index d87fb3a..2d7a030 100755 --- a/src/common/decorators/auth-guard.decorator.ts +++ b/src/common/decorators/auth-guard.decorator.ts @@ -4,8 +4,9 @@ import { ApiBearerAuth } from "@nestjs/swagger"; import { AdminRouteGuard } from "../../modules/auth/guards/admin.guard"; import { JwtAuthGuard } from "../../modules/auth/guards/auth.guard"; import { PermissionsGuard } from "../../modules/auth/guards/permission.guard"; +import { ResellerRouteGuard } from "../../modules/auth/guards/reseller.guard"; // import { RoleGuard } from "../../modules/auth/guards/role.guard"; export function AuthGuards() { - return applyDecorators(UseGuards(JwtAuthGuard, PermissionsGuard, AdminRouteGuard), ApiBearerAuth("authorization")); + return applyDecorators(UseGuards(JwtAuthGuard, PermissionsGuard, AdminRouteGuard,ResellerRouteGuard), ApiBearerAuth("authorization")); } diff --git a/src/common/decorators/reseller.decorator.ts b/src/common/decorators/reseller.decorator.ts new file mode 100755 index 0000000..089073b --- /dev/null +++ b/src/common/decorators/reseller.decorator.ts @@ -0,0 +1,4 @@ +export const RESELLER_ROUTE = "shouldReseller"; +import { SetMetadata } from "@nestjs/common"; + +export const ResellerRoute = (isResellerRoute: boolean = true) => SetMetadata(RESELLER_ROUTE, isResellerRoute); diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 8c6a7f8..633df31 100755 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -34,11 +34,11 @@ export class AuthController { } - @ApiOperation({ summary: "request to login with otp" }) + @ApiOperation({ summary: "request to login with otp as reseller" }) @HttpCode(HttpStatus.OK) @Post("otp/send/reseller") sendOtpReseller(@Body() requestOtpDto: RequestOtpDto) { - return this.authService.requestLoginOtpAgentAndReseller(requestOtpDto); + return this.authService.requestLoginOtpReseller(requestOtpDto); } @ApiOperation({ summary: "verify otp for login" }) @@ -79,9 +79,9 @@ export class AuthController { @ApiOperation({ summary: "verify otp for login" }) @HttpCode(HttpStatus.OK) - @Post("otp/verify/agent") + @Post("otp/verify/reseller") resellerVerifyOtp(@Body() verifyOtpDto: VerifyOtpDto) { - return this.authService.requestLoginOtpAgentAndReseller(verifyOtpDto); + return this.authService.resellerVerifyLoginOtp(verifyOtpDto); } @AuthGuards() diff --git a/src/modules/auth/guards/reseller.guard.ts b/src/modules/auth/guards/reseller.guard.ts new file mode 100755 index 0000000..68dbb31 --- /dev/null +++ b/src/modules/auth/guards/reseller.guard.ts @@ -0,0 +1,26 @@ +import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from "@nestjs/common"; +import { FastifyRequest } from "fastify"; + + import { AuthMessage } from "../../../common/enums/message.enum"; +import { Reflector } from "@nestjs/core"; +import { RESELLER_ROUTE } from "../../../common/decorators/reseller.decorator"; + +@Injectable() +export class ResellerRouteGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + + canActivate(context: ExecutionContext): boolean { + const requiredReseller = this.reflector.getAllAndOverride(RESELLER_ROUTE, [context.getHandler(), context.getClass()]); + if (!requiredReseller) return true; + + const req = context.switchToHttp().getRequest(); + const user = req.user; + + if (!user) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS); + + if (!user.isReseller) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS); + + return true; + } +} diff --git a/src/modules/auth/interfaces/IToken-payload.ts b/src/modules/auth/interfaces/IToken-payload.ts index 5b86c07..6a173c8 100755 --- a/src/modules/auth/interfaces/IToken-payload.ts +++ b/src/modules/auth/interfaces/IToken-payload.ts @@ -4,6 +4,7 @@ import { PermissionEnum } from "../../users/enums/permission.enum"; export interface ITokenPayload { id: string; isAdmin: boolean; + isReseller: boolean; permissions: PermissionEnum[]; audience?: string; // Client application identifier for SSO } diff --git a/src/modules/auth/providers/auth.service.ts b/src/modules/auth/providers/auth.service.ts index 702e5ee..7f99c01 100755 --- a/src/modules/auth/providers/auth.service.ts +++ b/src/modules/auth/providers/auth.service.ts @@ -112,11 +112,11 @@ export class AuthService { //****************** */ //****************** */ - async requestLoginOtpAgentAndReseller(requestOtpDto: RequestOtpDto) { + async requestLoginOtpReseller(requestOtpDto: RequestOtpDto) { const { phone } = requestOtpDto; // check if agent with this phone exist or not - const { reseller, agent } = await this.checkAgentExistWithPhone(phone) + await this.checkAgentExistWithPhone(phone) const existCode = await this.otpService.checkExistOtp(phone, "LOGIN_RESELLER_AGENT"); if (existCode) { @@ -133,8 +133,7 @@ export class AuthService { return { message: AuthMessage.OTP_SENT + otpCode, - reseller, - agent + }; } @@ -244,7 +243,7 @@ export class AuthService { if (user.deletedAt) throw new BadRequestException(AuthMessage.ACCESS_DENIED); - const tokens = await this.tokensService.generateTokens(user); + const tokens = await this.tokensService.generateTokensForReseller(user); return { message: AuthMessage.LOGIN_SUCCESS, diff --git a/src/modules/auth/providers/sso.service.ts b/src/modules/auth/providers/sso.service.ts index 906160a..1d0bba1 100644 --- a/src/modules/auth/providers/sso.service.ts +++ b/src/modules/auth/providers/sso.service.ts @@ -45,6 +45,7 @@ export class SSOService { // Generate token with client-specific audience const payload: ITokenPayload = { id: user.id, + isReseller:false, isAdmin: user.roles.some((r) => r.isAdmin), permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])), audience: clientId, @@ -105,6 +106,7 @@ export class SSOService { const payload: ITokenPayload = { id: user.id, isAdmin: user.roles.some((r) => r.isAdmin), + isReseller:false, permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])), audience: clientId, }; diff --git a/src/modules/auth/providers/tokens.service.ts b/src/modules/auth/providers/tokens.service.ts index b937888..f20e9d0 100755 --- a/src/modules/auth/providers/tokens.service.ts +++ b/src/modules/auth/providers/tokens.service.ts @@ -19,7 +19,7 @@ export class TokensService { private readonly jwtService: JwtService, private readonly refreshTokensRepository: RefreshTokensRepository, private readonly dataSource: DataSource, - ) {} + ) { } // ----------- generate token ----------------- async generateTokens(user: User, queryRunner?: QueryRunner) { @@ -27,6 +27,19 @@ export class TokensService { { id: user.id, isAdmin: user.roles.some((r) => r.isAdmin), + isReseller: false, + permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])), + }, + queryRunner, + ); + } + // ----------------------------- + async generateTokensForReseller(user: User, queryRunner?: QueryRunner) { + return this.generateAccessAndRefreshToken( + { + id: user.id, + isAdmin: user.roles.some((r) => r.isAdmin), + isReseller: true, permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])), }, queryRunner, diff --git a/src/modules/reseller/dto/create-reseller-agent.dto.ts b/src/modules/reseller/dto/create-reseller-agent.dto.ts new file mode 100755 index 0000000..7b6e5db --- /dev/null +++ b/src/modules/reseller/dto/create-reseller-agent.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsString } from "class-validator"; + +export class CreateResellerAgentDto { + @IsNotEmpty() + @IsString() + @ApiProperty() + userId: string; +} diff --git a/src/modules/reseller/reseller.controller.ts b/src/modules/reseller/reseller.controller.ts index e32660c..2a5fe1f 100644 --- a/src/modules/reseller/reseller.controller.ts +++ b/src/modules/reseller/reseller.controller.ts @@ -1,29 +1,60 @@ -import { Controller, Get, Post, Body } from '@nestjs/common'; +import { Controller, Get, Post, Body, Delete } from '@nestjs/common'; import { ResellerService } from './reseller.service'; import { ApiOperation } from '@nestjs/swagger'; import { PermissionsDec } from '../../common/decorators/permission.decorator'; import { PermissionEnum } from '../users/enums/permission.enum'; import { CreateResellerDto } from './dto/create-reseller.dto'; - +import { AuthGuards } from '../../common/decorators/auth-guard.decorator'; +import { AdminRoute } from '../../common/decorators/admin.decorator'; +import { ResellerRoute } from '../../common/decorators/reseller.decorator'; +import { UserDec } from '../../common/decorators/user.decorator'; +import { CreateResellerAgentDto } from './dto/create-reseller-agent.dto'; + @Controller('reseller') +@AuthGuards() export class ResellerController { constructor( private readonly resellerService: ResellerService, - ) { } @ApiOperation({ summary: "create Reseller ==> admin route" }) + @AdminRoute() @PermissionsDec(PermissionEnum.RESELLER) - @Post("reseller") + @Post() createReseller(@Body() dto: CreateResellerDto) { return this.resellerService.create(dto) } @ApiOperation({ summary: "get Resellers ==> admin route" }) + @AdminRoute() @PermissionsDec(PermissionEnum.RESELLER) - @Get("reseller") + @Get() getResellers() { return this.resellerService.findAll() } + + // =========== Reseller-->Agent ============= + + @ApiOperation({ summary: "get Reseller agents ==> reseller route" }) + @ResellerRoute() + @Get('agents') + getResellerAgents(@UserDec("id") userId: string) { + return this.resellerService.finResellerAgents(userId) + } + + @ApiOperation({ summary: "Create Reseller agent ==> reseller route" }) + @ResellerRoute() + @Post('agents') + createResellerAgent(@UserDec("id") userId: string, @Body() dto: CreateResellerAgentDto) { + return this.resellerService.createResellerAgent(userId, dto.userId) + } + + @ApiOperation({ summary: "Remove Reseller agent ==> reseller route" }) + @ResellerRoute() + @Delete('agents') + deleteResellerAgent(@UserDec("id") userId: string, @Body() dto: CreateResellerAgentDto) { + return this.resellerService.removeResellerAgent(userId, dto.userId) + } + } diff --git a/src/modules/reseller/reseller.service.ts b/src/modules/reseller/reseller.service.ts index 3ee4f66..fd779ef 100644 --- a/src/modules/reseller/reseller.service.ts +++ b/src/modules/reseller/reseller.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { UsersService } from '../users/providers/users.service'; import { CreateResellerDto } from './dto/create-reseller.dto'; import { ResellerRepository } from './repositories/reseller.repository'; @@ -37,4 +37,47 @@ export class ResellerService { } }) } + + async finResellerAgents(userId: string) { + const reseller = await this.FindOneOrFailByUserId(userId) + + return reseller.agents + } + + async FindOneOrFailByUserId(userId: string) { + const reseller = await this.resellerRepository.findOne({ + where: { owner: { id: userId } }, + relations: { agents: true } + }) + + if (!reseller) { + throw new NotFoundException() + } + + return reseller + } + + async createResellerAgent(userId: string, userTobeAgent: string) { + const reseller = await this.FindOneOrFailByUserId(userId) + + const { user } = await this.usersService.findOneById(userTobeAgent) + + user.reseller = reseller + + await this.usersService.save(user) + + return user + } + + async removeResellerAgent(userId: string, agentToBeDeleted: string) { + await this.FindOneOrFailByUserId(userId) + + const { user } = await this.usersService.findOneById(agentToBeDeleted) + + user.reseller = undefined + + await this.usersService.save(user) + + return user + } } diff --git a/src/modules/subscriptions/providers/subscriptions.service.ts b/src/modules/subscriptions/providers/subscriptions.service.ts index 99167d4..55251b6 100755 --- a/src/modules/subscriptions/providers/subscriptions.service.ts +++ b/src/modules/subscriptions/providers/subscriptions.service.ts @@ -709,4 +709,4 @@ export class SubscriptionsService { } return usersubscription } -} +} \ No newline at end of file diff --git a/src/modules/users/providers/users.service.ts b/src/modules/users/providers/users.service.ts index bc46ae8..c55d86b 100755 --- a/src/modules/users/providers/users.service.ts +++ b/src/modules/users/providers/users.service.ts @@ -30,7 +30,7 @@ import { LegalUserRepository } from "../repositories/legal-user.repository"; import { RealUserRepository } from "../repositories/real-user.repository"; import { UserGroupRepository } from "../repositories/user-group.repository"; import { UserRepository } from "../repositories/users.repository"; - + @Injectable() export class UsersService { private readonly logger = new Logger(UsersService.name); @@ -48,7 +48,7 @@ export class UsersService { private readonly smsService: SmsService, private readonly cacheService: CacheService, private readonly referralsService: ReferralsService, - ) { } + ) { } /************************************************************ */ @@ -415,6 +415,8 @@ export class UsersService { } /************************************************************ */ - + async save(user: User) { + return this.userRepository.save(user) + } }