add : reseller

This commit is contained in:
2026-04-12 15:04:34 +03:30
parent 63f83f5a68
commit 84ed654739
14 changed files with 253 additions and 7 deletions
@@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddREseller1775992337764 implements MigrationInterface {
name = 'AddREseller1775992337764'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "reseller" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" character varying(100) NOT NULL, "ownerId" uuid, CONSTRAINT "UQ_4993ba05f9437d2c32ea8adde42" UNIQUE ("name"), CONSTRAINT "REL_e2c8deee8c9000aefd91c17486" UNIQUE ("ownerId"), CONSTRAINT "PK_fbb203c7b490e89a011e62f2faf" PRIMARY KEY ("id"))`);
await queryRunner.query(`ALTER TABLE "user" ADD "resellerId" uuid`);
await queryRunner.query(`ALTER TABLE "reseller" ADD CONSTRAINT "FK_e2c8deee8c9000aefd91c174860" FOREIGN KEY ("ownerId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_f39518997d27b06d20bd1b08af0" FOREIGN KEY ("resellerId") REFERENCES "reseller"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_f39518997d27b06d20bd1b08af0"`);
await queryRunner.query(`ALTER TABLE "reseller" DROP CONSTRAINT "FK_e2c8deee8c9000aefd91c174860"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "resellerId"`);
await queryRunner.query(`DROP TABLE "reseller"`);
}
}
+1
View File
@@ -62,6 +62,7 @@ export const enum AuthMessage {
CURRENT_PASSWORD_INVALID = "رمز عبور فعلی نامعتبر است",
TOKEN_EXPIRED_OR_INVALID = "توکن منقضی شده یا نامعتبر است",
ACCESS_DENIED = "دسترسی شما محدود شده است",
NOT_AGENT="شما مجوز دسترسی به بخش نمایندگان را ندارید"
}
export const enum UserMessage {
+15
View File
@@ -33,6 +33,14 @@ export class AuthController {
return this.authService.requestLoginOtp(requestOtpDto);
}
@ApiOperation({ summary: "request to login with otp" })
@HttpCode(HttpStatus.OK)
@Post("otp/send/reseller")
sendOtpReseller(@Body() requestOtpDto: RequestOtpDto) {
return this.authService.requestLoginOtpAgentAndReseller(requestOtpDto);
}
@ApiOperation({ summary: "verify otp for login" })
@HttpCode(HttpStatus.OK)
@Post("otp/verify")
@@ -69,6 +77,13 @@ export class AuthController {
return this.authService.adminVerifyLoginOtp(verifyOtpDto);
}
@ApiOperation({ summary: "verify otp for login" })
@HttpCode(HttpStatus.OK)
@Post("otp/verify/agent")
resellerVerifyOtp(@Body() verifyOtpDto: VerifyOtpDto) {
return this.authService.requestLoginOtpAgentAndReseller(verifyOtpDto);
}
@AuthGuards()
@ApiOperation({ summary: "Update user password" })
@HttpCode(HttpStatus.OK)
+82 -1
View File
@@ -20,6 +20,7 @@ import { VerifyOtpDto } from "../DTO/verify-otp.dto";
import { ForgetPasswordDto } from "../DTO/forget-password.dto";
import { VerifyForgotPasswordOtpDto } from "../DTO/verify-forgot-otp.dto";
import { EmailService } from "../../utils/providers/email.service";
import { User } from "../../users/entities/user.entity";
@Injectable()
export class AuthService {
@@ -111,6 +112,35 @@ export class AuthService {
//****************** */
//****************** */
async requestLoginOtpAgentAndReseller(requestOtpDto: RequestOtpDto) {
const { phone } = requestOtpDto;
// check if agent with this phone exist or not
const { reseller, agent } = await this.checkAgentExistWithPhone(phone)
const existCode = await this.otpService.checkExistOtp(phone, "LOGIN_RESELLER_AGENT");
if (existCode) {
return {
message: AuthMessage.OTP_ALREADY_SENT,
ttlSecond: existCode,
};
}
const otpCode = await this.otpService.generateAndSetInCache(phone, "LOGIN_RESELLER_AGENT");
//
this.logger.log(`otp code: ${otpCode} for phone: ${phone}`);
await this.notificationQueue.addLoginOtpNotification({ phone, code: otpCode });
return {
message: AuthMessage.OTP_SENT + otpCode,
reseller,
agent
};
}
//****************** */
//****************** */
async verifyLoginOtp(verifyOtpDto: VerifyOtpDto) {
const { code, phone, referralCode } = verifyOtpDto;
@@ -198,6 +228,31 @@ export class AuthService {
}
//****************** */
async resellerVerifyLoginOtp(verifyOtpDto: VerifyOtpDto) {
const { code, phone } = verifyOtpDto;
await this.checkResellerOtpWithPhone(phone, code)
const { agent, reseller } = await this.checkAgentExistWithPhone(phone);
const user: User | null = agent ?? reseller
if (!user) {
throw new BadRequestException(AuthMessage.USER_NOT_FOUND)
}
if (user.deletedAt) throw new BadRequestException(AuthMessage.ACCESS_DENIED);
const tokens = await this.tokensService.generateTokens(user);
return {
message: AuthMessage.LOGIN_SUCCESS,
...tokens,
};
}
//****************** */
async changePassword(userId: string, updateDto: ChangePasswordDto) {
const { user } = await this.usersService.findOneById(userId);
@@ -249,6 +304,27 @@ export class AuthService {
return user;
}
private async checkResellerOtpWithPhone(phone: string, otpCode: string) {
const isValid = await this.otpService.verifyOtp(phone, otpCode, "LOGIN_RESELLER_AGENT");
if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP);
return true;
}
//****************** */
//****************** */
private async checkAgentExistWithPhone(phone: string) {
const { reseller } = await this.usersService.getResellerByUserPhone(phone);
const { agent } = await this.usersService.getAgentByUserPhone(phone);
if (!reseller && !agent) {
throw new BadRequestException(AuthMessage.USER_NOT_FOUND)
}
return { reseller, agent }
}
//****************** */
//****************** */
async requestForgotPasswordOtp(dto: ForgetPasswordDto) {
@@ -270,7 +346,7 @@ export class AuthService {
}
const otpCode = await this.otpService.generateAndSetInCache(email, "FORGOT_PASSWORD");
await this.emailService.sendOtpEmail({ userEmail: email, otp: otpCode });
this.logger.log(`otp code: ${otpCode} for email: ${email}`);
@@ -306,4 +382,9 @@ export class AuthService {
private checkUserIsAdmin(roles: Role[]) {
return roles.some((role) => role.isAdmin);
}
//****************** */
//****************** */
// private checkUserIsAgent(roles: Role[]) {
// return roles.some((role) => role.isAgent);
// }
}
+19
View File
@@ -0,0 +1,19 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsMobilePhone, IsNotEmpty, IsString, Length } from "class-validator";
import { AuthMessage } from "../../../common/enums/message.enum";
export class CreateResellerDto {
@IsNotEmpty()
@IsString()
@Length(2, 50,)
@ApiProperty({ description: "name", example: "mahyar" })
name: string;
@IsNotEmpty({ message: AuthMessage.PHONE_NOT_EMPTY })
@IsMobilePhone("fa-IR", {}, { message: AuthMessage.INVALID_PHONE_FORMAT })
@Length(11, 11, { message: AuthMessage.PHONE_SHOULD_BE_11_DIGIT })
@ApiProperty({ description: "phone number", default: "09922320740" })
phone: string;
}
+19
View File
@@ -0,0 +1,19 @@
import { Column, Entity, JoinColumn, OneToMany, OneToOne } from "typeorm";
import { User } from "./user.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
@Entity()
export class Reseller extends BaseEntity {
@Column({ type: "varchar", length: 100, unique: true })
name: string;
//---------------------------------------
@OneToOne(() => User,user=>user.ownedReseller)
@JoinColumn()
owner!: User;
@OneToMany(() => User, user => user.reseller)
agents: User[];
}
@@ -15,6 +15,9 @@ export class Role extends BaseEntity {
@Column({ type: "boolean", default: false })
isAdmin: boolean;
// @Column({ type: "boolean", default: false })
// isAgent: boolean;
@ManyToMany(() => Permission, (permission) => permission.roles)
@JoinTable({ name: "role_permission_relation" })
permissions: Permission[];
+11 -1
View File
@@ -1,5 +1,5 @@
import { Exclude } from "class-transformer";
import { Column, DeleteDateColumn, Entity, JoinTable, ManyToMany, OneToMany, OneToOne } from "typeorm";
import { Column, DeleteDateColumn, Entity, JoinTable, ManyToMany, ManyToOne, OneToMany, OneToOne } from "typeorm";
import { LegalUser } from "./legal-user.entity";
import { RealUser } from "./real-user.entity";
@@ -30,6 +30,7 @@ import { TicketMessage } from "../../tickets/entities/ticket-message.entity";
import { Ticket } from "../../tickets/entities/ticket.entity";
import { Wallet } from "../../wallets/entities/wallet.entity";
import { FinancialType } from "../enums/financial-type.enum";
import { Reseller } from "./reseller.entity";
@Entity()
export class User extends BaseEntity {
@@ -161,4 +162,13 @@ export class User extends BaseEntity {
@OneToMany(() => ReferralReward, (reward) => reward.user)
referralRewards: ReferralReward[];
//---------------------------------------
@ManyToOne(() => Reseller, (reseller) => reseller.agents, { nullable: true })
reseller?: Reseller
@OneToOne(() => Reseller, reseller => reseller.owner, { nullable: true })
ownedReseller?: Reseller
}
+2 -1
View File
@@ -23,5 +23,6 @@ export enum PermissionEnum {
DMENU = "dmenu",
DKALA = "dkala",
DPAGE = "dpage",
DMAIL = 'dmail'
DMAIL = 'dmail',
RESELLER="reseller"
}
+46 -1
View File
@@ -30,6 +30,8 @@ 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";
import { ResellerRepository } from "../repositories/reseller.repository";
import { CreateResellerDto } from "../DTO/create-reseller.dto";
@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);
@@ -47,7 +49,8 @@ export class UsersService {
private readonly smsService: SmsService,
private readonly cacheService: CacheService,
private readonly referralsService: ReferralsService,
) {}
private readonly resellerRepository: ResellerRepository,
) { }
/************************************************************ */
@@ -370,6 +373,29 @@ export class UsersService {
/************************************************************ */
async getResellerByUserPhone(phone: string) {
const reseller = await this.userRepository.findOne({ where: { ownedReseller: { owner: { phone } } } });
return { reseller };
}
/************************************************************ */
async getAgentByUserPhone(phone: string) {
const agent = await this.userRepository.findOne({ where: { reseller: { agents: { phone } } } });
return { agent };
}
/************************************************************ */
async getResellersList() {
this.resellerRepository.find({
relations: {
owner: true
}
})
}
/************************************************************ */
private async createEmailHashLink(email: string) {
const secret = this.configService.getOrThrow<string>("EMAIL_SECRET");
const nonce = randomBytes(16).toString("hex");
@@ -399,4 +425,23 @@ export class UsersService {
}
/************************************************************ */
async createReseller(dto: CreateResellerDto) {
const { name, phone } = dto
const user = await this.findOneWithPhone(phone)
if (!user) {
throw new BadRequestException(AuthMessage.USER_NOT_FOUND)
}
const reseller = this.resellerRepository.create({
name,
owner: user
})
await this.resellerRepository.save(reseller)
return reseller
}
}
+12
View File
@@ -0,0 +1,12 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Reseller } from "../entities/reseller.entity";
@Injectable()
export class ResellerRepository extends Repository<Reseller> {
constructor(@InjectRepository(Reseller) resellerRepository: Repository<Reseller>) {
super(resellerRepository.target, resellerRepository.manager, resellerRepository.queryRunner);
}
}
+18 -1
View File
@@ -29,6 +29,7 @@ import { IUserIpAndHeaders, UserDec, UserIpAndHeaders } from "../../common/decor
import { ParamDto } from "../../common/DTO/param.dto";
import { RequestOtpDto } from "../auth/DTO/request-otp.dto";
import { VerifyOtpDto } from "../auth/DTO/verify-otp.dto";
import { CreateResellerDto } from "./DTO/create-reseller.dto";
@AuthGuards()
@Controller("users")
@@ -37,7 +38,7 @@ export class UsersController {
private readonly usersService: UsersService,
private readonly adminsService: AdminsService,
private readonly customersService: CustomersService,
) {}
) { }
//----------------------- user -----------------------
@@ -228,4 +229,20 @@ export class UsersController {
createRole(@Body() createDto: CreateRoleDto) {
return this.adminsService.createRole(createDto);
}
// ==================== Reseller =========
@ApiOperation({ summary: "create Reseller ==> admin route" })
@PermissionsDec(PermissionEnum.RESELLER)
@Post("reseller")
createReseller(@Body() dto: CreateResellerDto) {
return this.usersService.createReseller(dto)
}
@ApiOperation({ summary: "get Resellers ==> admin route" })
@PermissionsDec(PermissionEnum.RESELLER)
@Get("reseller")
getResellers( ) {
return this.usersService.getResellersList()
}
}
+4 -1
View File
@@ -28,10 +28,12 @@ import { AdminsService } from "./providers/admins.service";
import { CustomersService } from "./providers/customers.service";
import { RefreshTokensRepository } from "./repositories/refresh-token.repository";
import { ReferralsModule } from "../referrals/referrals.module";
import { ResellerRepository } from "./repositories/reseller.repository";
import { Reseller } from "./entities/reseller.entity";
@Module({
imports: [
TypeOrmModule.forFeature([User, Role, UserGroup, UserSetting, RealUser, LegalUser, Permission, RefreshToken]),
TypeOrmModule.forFeature([User, Role, UserGroup, UserSetting, RealUser, LegalUser, Permission, RefreshToken,Reseller]),
WalletsModule,
UtilsModule,
AddressModule,
@@ -52,6 +54,7 @@ import { ReferralsModule } from "../referrals/referrals.module";
RealUserRepository,
LegalUserRepository,
AddressService,
ResellerRepository
],
controllers: [UsersController],
exports: [UsersService, UserRepository, CustomersService, AdminsService, RefreshTokensRepository],
+1 -1
View File
@@ -1 +1 @@
export type OtpCacheKeyType = "LOGIN/REGISTER" | "FORGOT_PASSWORD" | "INVOICE_VERIFY";
export type OtpCacheKeyType = "LOGIN/REGISTER" | "FORGOT_PASSWORD" | "INVOICE_VERIFY"|"LOGIN_RESELLER_AGENT";