From ef57dc7107f337d0cffc157c5ed0fa2d19c5056c Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Sat, 16 Aug 2025 12:50:58 +0330 Subject: [PATCH] chore: change the login and regitster flow to single route --- src/common/enums/message.enum.ts | 2 + src/configs/sms.config.ts | 2 + src/configs/typeorm.config.ts | 2 +- src/modules/auth/DTO/complete-register.dto.ts | 94 ++++----- src/modules/auth/DTO/verify-otp.dto.ts | 11 +- src/modules/auth/auth.controller.ts | 14 -- src/modules/auth/providers/auth.service.ts | 178 +++++++----------- src/modules/notifications/handlers/index.ts | 1 + .../handlers/notification-handler.factory.ts | 19 +- .../handlers/user-password.handler.ts | 23 +++ .../interfaces/ISendNotificationData.ts | 4 + .../notifications/notifications.module.ts | 45 +++-- .../providers/notifications.service.ts | 12 ++ .../notifications/queue/notification.queue.ts | 5 + .../settings/enums/notif-settings.enum.ts | 3 + src/modules/users/DTO/create-admin.dto.ts | 33 +++- src/modules/users/DTO/create-real-user.dto.ts | 25 ++- src/modules/users/DTO/update-profile.dto.ts | 27 ++- src/modules/users/entities/user.entity.ts | 6 +- src/modules/users/providers/users.service.ts | 18 +- src/modules/utils/interfaces/IOtpKey.ts | 2 +- src/modules/utils/interfaces/ISms.ts | 3 +- src/modules/utils/providers/sms.service.ts | 30 +++ src/modules/utils/providers/string.utils.ts | 16 ++ 24 files changed, 340 insertions(+), 235 deletions(-) create mode 100644 src/modules/notifications/handlers/user-password.handler.ts diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 451dcb7..0a7848f 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -391,6 +391,8 @@ export const enum NotificationMessage { USERID_IS_STRING = "شناسه کاربر باید یک رشته متنی باشد", LOGIN = "لاگین", LOGIN_MESSAGE = "یک لاگین در تاریخ [loginDate] به حساب کاربری شما صورت گرفت", + USER_PASSWORD = "رمز عبور پیش‌فرض", + USER_PASSWORD_MESSAGE = "رمز عبور پیش‌فرض شما: [password]", NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED = "اعلان یافت نشد یا دسترسی غیرمجاز است", INVOICE_CREATION = "صدور صورت حساب", INVOICE_CREATION_MESSAGE = "یک صورت حساب به شماره [id] جدید برای شما صادر شده است", diff --git a/src/configs/sms.config.ts b/src/configs/sms.config.ts index 802ba0f..56ef74d 100755 --- a/src/configs/sms.config.ts +++ b/src/configs/sms.config.ts @@ -32,6 +32,7 @@ export function smsConfigs() { SMS_PATTERN_NEW_SUBSCRIPTION: configService.getOrThrow("SMS_PATTERN_NEW_SUBSCRIPTION"), SMS_PATTERN_NEW_TICKET_GLOBAL: configService.getOrThrow("SMS_PATTERN_NEW_TICKET_GLOBAL"), SMS_PATTERN_NEW_CRITICISM: configService.getOrThrow("SMS_PATTERN_NEW_CRITICISM"), + SMS_PATTERN_USER_PASSWORD: configService.getOrThrow("SMS_PATTERN_USER_PASSWORD"), }; }, }; @@ -65,4 +66,5 @@ export interface ISmsConfigs { SMS_PATTERN_NEW_SUBSCRIPTION: string; SMS_PATTERN_NEW_TICKET_GLOBAL: string; SMS_PATTERN_NEW_CRITICISM: string; + SMS_PATTERN_USER_PASSWORD: string; } diff --git a/src/configs/typeorm.config.ts b/src/configs/typeorm.config.ts index 8d8378f..9391ab2 100755 --- a/src/configs/typeorm.config.ts +++ b/src/configs/typeorm.config.ts @@ -13,7 +13,7 @@ export function databaseConfigs(): TypeOrmModuleAsyncOptions { username: configService.getOrThrow("DB_USER"), password: configService.getOrThrow("DB_PASS"), autoLoadEntities: true, - synchronize: configService.getOrThrow("NODE_ENV") == "production" ? false : true, + synchronize: configService.getOrThrow("NODE_ENV") == "production" ? false : false, logging: configService.getOrThrow("NODE_ENV") == "production" ? false : true, migrationsTableName: "typeorm_migrations", migrationsRun: false, diff --git a/src/modules/auth/DTO/complete-register.dto.ts b/src/modules/auth/DTO/complete-register.dto.ts index 2696690..ef99bb0 100755 --- a/src/modules/auth/DTO/complete-register.dto.ts +++ b/src/modules/auth/DTO/complete-register.dto.ts @@ -1,56 +1,56 @@ -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { IsMobilePhone, IsNotEmpty, IsNumberString, IsOptional, IsString, Length, MinLength } from "class-validator"; +// import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +// import { IsMobilePhone, IsNotEmpty, IsNumberString, IsOptional, IsString, Length } from "class-validator"; -// import { IsNationalCode } from "../../../common/decorators/is-national-code.decorator"; -import { AuthMessage, ReferralMessage } from "../../../common/enums/message.enum"; +// // import { IsNationalCode } from "../../../common/decorators/is-national-code.decorator"; +// import { AuthMessage, ReferralMessage } from "../../../common/enums/message.enum"; -export class CompleteRegistrationDto { - @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; +// export class CompleteRegistrationDto { +// @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; - @ApiProperty({ description: "OTP code received via SMS", example: "56893" }) - @IsNotEmpty({ message: AuthMessage.OTP_NOT_EMPTY }) - @IsNumberString(undefined, { message: AuthMessage.OTP_FORMAT_INVALID }) - @Length(5, 5, { message: AuthMessage.OTP_FORMAT_INVALID }) - code: string; +// @ApiProperty({ description: "OTP code received via SMS", example: "56893" }) +// @IsNotEmpty({ message: AuthMessage.OTP_NOT_EMPTY }) +// @IsNumberString(undefined, { message: AuthMessage.OTP_FORMAT_INVALID }) +// @Length(5, 5, { message: AuthMessage.OTP_FORMAT_INVALID }) +// code: string; - @IsNotEmpty({ message: AuthMessage.FIRST_NAME_NOT_EMPTY }) - @IsString({ message: AuthMessage.FIRST_NAME_NOT_EMPTY }) - @Length(2, 50, { message: AuthMessage.FIRST_NAME_SHOULD_BE_BETWEEN_2_AND_50 }) - @ApiProperty({ description: "First name", example: "mahyar" }) - firstName: string; +// // @IsNotEmpty({ message: AuthMessage.FIRST_NAME_NOT_EMPTY }) +// // @IsString({ message: AuthMessage.FIRST_NAME_NOT_EMPTY }) +// // @Length(2, 50, { message: AuthMessage.FIRST_NAME_SHOULD_BE_BETWEEN_2_AND_50 }) +// // @ApiProperty({ description: "First name", example: "mahyar" }) +// // firstName: string; - @IsNotEmpty({ message: AuthMessage.LAST_NAME_NOT_EMPTY }) - @IsString({ message: AuthMessage.LAST_NAME_NOT_EMPTY }) - @Length(2, 50, { message: AuthMessage.LAST_NAME_SHOULD_BE_BETWEEN_2_AND_50 }) - @ApiProperty({ description: "Last name", example: "jamshidi" }) - lastName: string; +// // @IsNotEmpty({ message: AuthMessage.LAST_NAME_NOT_EMPTY }) +// // @IsString({ message: AuthMessage.LAST_NAME_NOT_EMPTY }) +// // @Length(2, 50, { message: AuthMessage.LAST_NAME_SHOULD_BE_BETWEEN_2_AND_50 }) +// // @ApiProperty({ description: "Last name", example: "jamshidi" }) +// // lastName: string; - // @IsOptional() - // @IsNotEmpty({ message: AuthMessage.BIRTH_DATE_NOT_EMPTY }) - // @IsString({ message: AuthMessage.BIRTH_DATE_NOT_EMPTY }) - // @ApiProperty({ description: "Birth date", example: "1403/01/01" }) - // birthDate: string; +// // @IsOptional() +// // @IsNotEmpty({ message: AuthMessage.BIRTH_DATE_NOT_EMPTY }) +// // @IsString({ message: AuthMessage.BIRTH_DATE_NOT_EMPTY }) +// // @ApiProperty({ description: "Birth date", example: "1403/01/01" }) +// // birthDate: string; - // @IsOptional() - // @IsNotEmpty({ message: AuthMessage.NATIONAL_NOT_EMPTY }) - // @IsNumberString({ no_symbols: true }, { message: AuthMessage.NATIONAL_CODE_INCORRECT }) - // @Length(10, 10, { message: AuthMessage.NATIONAL_CODE_INCORRECT }) - // @IsNationalCode({ message: AuthMessage.NATIONAL_CODE_INVALID }) - // @ApiProperty({ description: "iranian format (10 char)", example: "4569852169" }) - // nationalCode: string; +// // @IsOptional() +// // @IsNotEmpty({ message: AuthMessage.NATIONAL_NOT_EMPTY }) +// // @IsNumberString({ no_symbols: true }, { message: AuthMessage.NATIONAL_CODE_INCORRECT }) +// // @Length(10, 10, { message: AuthMessage.NATIONAL_CODE_INCORRECT }) +// // @IsNationalCode({ message: AuthMessage.NATIONAL_CODE_INVALID }) +// // @ApiProperty({ description: "iranian format (10 char)", example: "4569852169" }) +// // nationalCode: string; - @IsNotEmpty({ message: AuthMessage.PASSWORD_NOT_EMPTY }) - @IsString({ message: AuthMessage.PASSWORD_FORMAT_INVALID }) - @ApiProperty({ description: "password", example: "12S345SS678" }) - @MinLength(8, { message: AuthMessage.PASSWORD_LENGTH }) - password: string; +// // @IsNotEmpty({ message: AuthMessage.PASSWORD_NOT_EMPTY }) +// // @IsString({ message: AuthMessage.PASSWORD_FORMAT_INVALID }) +// // @ApiProperty({ description: "password", example: "12S345SS678" }) +// // @MinLength(8, { message: AuthMessage.PASSWORD_LENGTH }) +// // password: string; - @IsOptional() - @IsString({ message: ReferralMessage.INVALID_REFERRAL_CODE }) - @ApiPropertyOptional({ description: "Referral code", example: "abc123" }) - referralCode?: string; -} +// @IsOptional() +// @IsString({ message: ReferralMessage.INVALID_REFERRAL_CODE }) +// @ApiPropertyOptional({ description: "Referral code", example: "abc123" }) +// referralCode?: string; +// } diff --git a/src/modules/auth/DTO/verify-otp.dto.ts b/src/modules/auth/DTO/verify-otp.dto.ts index b059f74..9b9d4ee 100755 --- a/src/modules/auth/DTO/verify-otp.dto.ts +++ b/src/modules/auth/DTO/verify-otp.dto.ts @@ -1,7 +1,7 @@ -import { ApiProperty, PickType } from "@nestjs/swagger"; -import { IsMobilePhone, IsNotEmpty, IsNumberString, Length } from "class-validator"; +import { ApiProperty, ApiPropertyOptional, PickType } from "@nestjs/swagger"; +import { IsMobilePhone, IsNotEmpty, IsNumberString, IsOptional, IsString, Length } from "class-validator"; -import { AuthMessage } from "../../../common/enums/message.enum"; +import { AuthMessage, ReferralMessage } from "../../../common/enums/message.enum"; export class VerifyOtpDto { @IsNotEmpty({ message: AuthMessage.PHONE_NOT_EMPTY }) @@ -15,6 +15,11 @@ export class VerifyOtpDto { @IsNumberString(undefined, { message: AuthMessage.OTP_FORMAT_INVALID }) @Length(5, 5, { message: AuthMessage.OTP_FORMAT_INVALID }) code: string; + + @IsOptional() + @IsString({ message: ReferralMessage.INVALID_REFERRAL_CODE }) + @ApiPropertyOptional({ description: "Referral code", example: "abc123" }) + referralCode?: string; } export class VerifyOtpWithUserId extends PickType(VerifyOtpDto, ["code"]) {} diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 65f008d..2162e55 100755 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -3,7 +3,6 @@ import { ApiOperation, ApiTags } from "@nestjs/swagger"; import { Throttle, ThrottlerGuard } from "@nestjs/throttler"; import { ChangePasswordDto } from "./DTO/change-password.dto"; -import { CompleteRegistrationDto } from "./DTO/complete-register.dto"; import { CheckUserExistDto, LoginPasswordDTO } from "./DTO/loginPassword.dto"; import { RefreshTokenDto } from "./DTO/refresh-token.dto"; import { RequestOtpDto } from "./DTO/request-otp.dto"; @@ -27,19 +26,6 @@ export class AuthController { private readonly ssoService: SSOService, ) {} - @ApiOperation({ summary: "Initiate registration" }) - @HttpCode(HttpStatus.OK) - @Post("register/initiate") - register(@Body() registerDto: RequestOtpDto) { - return this.authService.initiateRegistration(registerDto); - } - - @ApiOperation({ summary: "complete registration ==> step 2" }) - @Post("register/complete") - completeRegistration(@Body() completeRegistrationDto: CompleteRegistrationDto) { - return this.authService.completeRegistration(completeRegistrationDto); - } - @ApiOperation({ summary: "request to login with otp" }) @HttpCode(HttpStatus.OK) @Post("otp/send") diff --git a/src/modules/auth/providers/auth.service.ts b/src/modules/auth/providers/auth.service.ts index a702135..3739b07 100755 --- a/src/modules/auth/providers/auth.service.ts +++ b/src/modules/auth/providers/auth.service.ts @@ -1,4 +1,6 @@ -import { BadRequestException, Injectable } from "@nestjs/common"; +import { randomBytes } from "node:crypto"; + +import { BadRequestException, Injectable, Logger } from "@nestjs/common"; import { DataSource } from "typeorm"; import { TokensService } from "./tokens.service"; @@ -10,13 +12,14 @@ import { AdminsService } from "../../users/providers/admins.service"; import { UsersService } from "../../users/providers/users.service"; import { OTPService } from "../../utils/providers/otp.service"; import { PasswordService } from "../../utils/providers/password.service"; +import { randomizeCase } from "../../utils/providers/string.utils"; import { ChangePasswordDto } from "../DTO/change-password.dto"; -import { CompleteRegistrationDto } from "../DTO/complete-register.dto"; import { CheckUserExistDto, LoginPasswordDTO } from "../DTO/loginPassword.dto"; import { RequestOtpDto } from "../DTO/request-otp.dto"; import { VerifyOtpDto } from "../DTO/verify-otp.dto"; @Injectable() export class AuthService { + private readonly logger = new Logger(AuthService.name); constructor( private readonly usersService: UsersService, private readonly adminsService: AdminsService, @@ -27,78 +30,7 @@ export class AuthService { private readonly notificationQueue: NotificationQueue, private readonly referralsService: ReferralsService, ) {} - //****************** */ - //****************** */ - async initiateRegistration(requestOtpDto: RequestOtpDto) { - const { phone } = requestOtpDto; - const existUser = await this.usersService.findOneWithPhone(phone); - if (existUser) throw new BadRequestException(AuthMessage.PHONE_EXISTS); - const existCode = await this.otpService.checkExistOtp(phone, "REGISTER"); - if (existCode) { - return { - message: AuthMessage.OTP_ALREADY_SENT, - ttlSecond: existCode, - }; - } - const otpCode = await this.otpService.generateAndSetInCache(phone, "REGISTER"); - // - // await this.smsService.sendSmsVerifyCode(phone, otpCode); - await this.notificationQueue.addLoginOtpNotification({ phone, code: otpCode }); - - return { - message: AuthMessage.OTP_SENT, - }; - } - //****************** */ - //****************** */ - async completeRegistration(completeRegistrationDto: CompleteRegistrationDto) { - const { phone, code, referralCode } = completeRegistrationDto; - const queryRunner = this.dataSource.createQueryRunner(); - - try { - await queryRunner.connect(); - await queryRunner.startTransaction(); - // - const isValid = await this.otpService.verifyOtp(phone, code, "REGISTER"); - if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP); - - const existUser = await this.usersService.findOneWithPhone(phone); - if (existUser) throw new BadRequestException(AuthMessage.PHONE_EXISTS); - - const hashedPassword = await this.passwordService.hashPassword(completeRegistrationDto.password); - const user = await this.usersService.createUser(completeRegistrationDto, hashedPassword, queryRunner); - - if (referralCode) await this.referralsService.useReferralCode({ referralCode, userId: user.id }, queryRunner); - - const tokens = await this.tokensService.generateTokens(user, queryRunner); - - const superAdmins = await this.adminsService.getSuperAdmins(queryRunner); - - for (const admin of superAdmins) { - await this.notificationQueue.addNewCustomerNotification(admin.id, { - userPhone: admin.phone, - userEmail: admin.email, - fullName: `${user.firstName} ${user.lastName}`, - mobile: user.phone, - }); - } - - await this.otpService.delOtpFormCache(phone, "REGISTER"); - - await queryRunner.commitTransaction(); - - return { - message: AuthMessage.USER_REGISTER_SUCCESS, - ...tokens, - }; - } catch (error) { - await queryRunner.rollbackTransaction(); - throw error; - } finally { - await queryRunner.release(); - } - } //****************** */ //****************** */ async loginWithPassword(loginDto: LoginPasswordDTO) { @@ -150,18 +82,10 @@ export class AuthService { //****************** */ //****************** */ - async requestLoginOtp(requestOtpDto: RequestOtpDto, isAdmin: boolean = false) { + async requestLoginOtp(requestOtpDto: RequestOtpDto) { const { phone } = requestOtpDto; - const user = await this.usersService.findOneWithPhone(phone); - if (!user) throw new BadRequestException(AuthMessage.PHONE_NOT_FOUND); - //check the if the method call is from admin or not - - const isUserAdmin = this.checkUserIsAdmin(user.roles); - - if (isAdmin && !isUserAdmin) throw new BadRequestException(AuthMessage.NOT_ADMIN); - - const existCode = await this.otpService.checkExistOtp(phone, "LOGIN"); + const existCode = await this.otpService.checkExistOtp(phone, "LOGIN/REGISTER"); if (existCode) { return { message: AuthMessage.OTP_ALREADY_SENT, @@ -169,9 +93,9 @@ export class AuthService { }; } - const otpCode = await this.otpService.generateAndSetInCache(phone, "LOGIN"); + const otpCode = await this.otpService.generateAndSetInCache(phone, "LOGIN/REGISTER"); // - // await this.smsService.sendSmsVerifyCode(phone, otpCode); + this.logger.log(`otp code: ${otpCode} for phone: ${phone}`); await this.notificationQueue.addLoginOtpNotification({ phone, code: otpCode }); return { @@ -183,20 +107,67 @@ export class AuthService { //****************** */ async verifyLoginOtp(verifyOtpDto: VerifyOtpDto) { - const { code, phone } = verifyOtpDto; + const { code, phone, referralCode } = verifyOtpDto; - const user = await this.checkUserLoginCredentialWithPhone(phone, code); + const queryRunner = this.dataSource.createQueryRunner(); - if (!user.roles.some((role) => !role.isAdmin)) throw new BadRequestException(AuthMessage.ADMIN_CAN_NOT_LOGIN); - // if (user.roles.some((role) => role.isAdmin)) throw new BadRequestException(AuthMessage.ADMIN_CAN_NOT_LOGIN); + try { + await queryRunner.connect(); + await queryRunner.startTransaction(); - const tokens = await this.tokensService.generateTokens(user); + const isValid = await this.otpService.verifyOtp(phone, code, "LOGIN/REGISTER"); + if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP); - await this.notificationQueue.addLoginNotification(user.id, { userPhone: user.phone, userEmail: user.email }); - return { - message: AuthMessage.LOGIN_SUCCESS, - ...tokens, - }; + let user = await this.usersService.findOneWithPhone(phone); + let isNewUser = false; + + if (!user) { + const defaultUserPassword = randomizeCase(randomBytes(6).toString("hex")); + isNewUser = true; + + const hashedPassword = await this.passwordService.hashPassword(defaultUserPassword); + user = await this.usersService.createUser(verifyOtpDto, hashedPassword, queryRunner); + + if (referralCode) await this.referralsService.useReferralCode({ referralCode, userId: user.id }, queryRunner); + + const superAdmins = await this.adminsService.getSuperAdmins(queryRunner); + + for (const admin of superAdmins) { + await this.notificationQueue.addNewCustomerNotification(admin.id, { + userPhone: admin.phone, + userEmail: admin.email, + fullName: `${user.firstName} ${user.lastName}`, + mobile: user.phone, + }); + } + + await this.notificationQueue.addUserPasswordNotification(user.id, { + userPhone: user.phone, + userEmail: user.email, + password: defaultUserPassword, + }); + } + + if (!user.roles.some((role) => !role.isAdmin)) throw new BadRequestException(AuthMessage.ADMIN_CAN_NOT_LOGIN); + + await this.notificationQueue.addLoginNotification(user.id, { userPhone: user.phone, userEmail: user.email }); + + const tokens = await this.tokensService.generateTokens(user, queryRunner); + + await this.otpService.delOtpFormCache(phone, "LOGIN/REGISTER"); + + await queryRunner.commitTransaction(); + + return { + message: isNewUser ? AuthMessage.USER_REGISTER_SUCCESS : AuthMessage.LOGIN_SUCCESS, + ...tokens, + }; + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } } //****************** */ @@ -204,7 +175,9 @@ export class AuthService { async adminVerifyLoginOtp(verifyOtpDto: VerifyOtpDto) { const { code, phone } = verifyOtpDto; - const user = await this.checkUserLoginCredentialWithPhone(phone, code); + const user = await this.checkUserAuthCredentialWithPhone(phone, code); + + if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); const isUserAdmin = this.checkUserIsAdmin(user.roles); @@ -260,23 +233,18 @@ export class AuthService { return user; } - //****************** */ - //****************** */ - private async checkUserLoginCredentialWithPhone(phone: string, otpCode: string) { - const isValid = await this.otpService.verifyOtp(phone, otpCode, "LOGIN"); + private async checkUserAuthCredentialWithPhone(phone: string, otpCode: string) { + const isValid = await this.otpService.verifyOtp(phone, otpCode, "LOGIN/REGISTER"); if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP); - await this.otpService.delOtpFormCache(phone, "LOGIN"); + await this.otpService.delOtpFormCache(phone, "LOGIN/REGISTER"); const user = await this.usersService.findOneWithPhone(phone); - if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); return user; } - // private checkUserPerm(user: User, perm: PermissionEnum) { - // return user.roles.some((role) => role?.permissions?.some((p) => p.name === perm)); - // } + //****************** */ //****************** */ private checkUserIsAdmin(roles: Role[]) { diff --git a/src/modules/notifications/handlers/index.ts b/src/modules/notifications/handlers/index.ts index 021a359..fc82de8 100644 --- a/src/modules/notifications/handlers/index.ts +++ b/src/modules/notifications/handlers/index.ts @@ -7,6 +7,7 @@ export { NotificationHandlerFactory } from "./notification-handler.factory"; // Special handlers (non-transactional) export { OtpHandler } from "./otp.handler"; export { UserLoginHandler } from "./user-login.handler"; +export { UserPasswordHandler } from "./user-password.handler"; // Wallet handlers export { WalletChargeHandler, WalletDeductionHandler } from "./wallet.handler"; diff --git a/src/modules/notifications/handlers/notification-handler.factory.ts b/src/modules/notifications/handlers/notification-handler.factory.ts index 6833e45..dbf6f5f 100644 --- a/src/modules/notifications/handlers/notification-handler.factory.ts +++ b/src/modules/notifications/handlers/notification-handler.factory.ts @@ -16,6 +16,7 @@ import { import { PaymentCancellationHandler, PaymentReminderHandler } from "./payment.handler"; import { BlockServiceHandler } from "./service.handler"; import { AnswerTicketHandler, AssignTicketHandler, CreateTicketHandler, NewTicketHandler } from "./ticket.handler"; +import { UserPasswordHandler } from "./user-password.handler"; import { WalletChargeHandler, WalletDeductionHandler } from "./wallet.handler"; import { NotifType } from "../../settings/enums/notif-settings.enum"; @@ -25,11 +26,9 @@ export class NotificationHandlerFactory { private readonly handlers: Map = new Map(); constructor( - // Wallet handlers private readonly walletChargeHandler: WalletChargeHandler, private readonly walletDeductionHandler: WalletDeductionHandler, - // Invoice handlers private readonly createInvoiceHandler: CreateInvoiceHandler, private readonly billInvoiceReminderHandler: BillInvoiceReminderHandler, private readonly billInvoiceHandler: BillInvoiceHandler, @@ -37,7 +36,6 @@ export class NotificationHandlerFactory { private readonly invoiceOverdueHandler: InvoiceOverdueHandler, private readonly recurringInvoiceHandler: RecurringInvoiceHandler, - // Admin handlers private readonly blogCommentHandler: BlogCommentHandler, private readonly serviceReviewHandler: ServiceReviewHandler, private readonly newCustomerHandler: NewCustomerHandler, @@ -45,30 +43,26 @@ export class NotificationHandlerFactory { private readonly newCriticismHandler: NewCriticismHandler, private readonly newTicketHandler: NewTicketHandler, - // User handlers private readonly announcementHandler: AnnouncementHandler, - // Ticket handlers private readonly answerTicketHandler: AnswerTicketHandler, private readonly createTicketHandler: CreateTicketHandler, private readonly assignTicketHandler: AssignTicketHandler, - // Service handlers private readonly blockServiceHandler: BlockServiceHandler, - // Payment handlers private readonly paymentReminderHandler: PaymentReminderHandler, private readonly paymentCancellationHandler: PaymentCancellationHandler, + + private readonly userPasswordHandler: UserPasswordHandler, ) { this.registerHandlers(); } private registerHandlers(): void { - // Wallet notifications this.handlers.set(NotifType.WALLET_CHARGE, this.walletChargeHandler); this.handlers.set(NotifType.WALLET_DEDUCTION, this.walletDeductionHandler); - // Invoice notifications this.handlers.set(NotifType.CREATE_INVOICE, this.createInvoiceHandler); this.handlers.set(NotifType.BILL_INVOICE_REMINDER, this.billInvoiceReminderHandler); this.handlers.set(NotifType.BILL_INVOICE, this.billInvoiceHandler); @@ -76,7 +70,6 @@ export class NotificationHandlerFactory { this.handlers.set(NotifType.INVOICE_OVERDUE, this.invoiceOverdueHandler); this.handlers.set(NotifType.RECURRING_INVOICE, this.recurringInvoiceHandler); - // Admin notifications this.handlers.set(NotifType.NEW_BLOG_COMMENT, this.blogCommentHandler); this.handlers.set(NotifType.NEW_SERVICE_REVIEW, this.serviceReviewHandler); this.handlers.set(NotifType.NEW_CUSTOMER, this.newCustomerHandler); @@ -84,21 +77,19 @@ export class NotificationHandlerFactory { this.handlers.set(NotifType.NEW_CRITICISM, this.newCriticismHandler); this.handlers.set(NotifType.NEW_TICKET, this.newTicketHandler); - // User notifications this.handlers.set(NotifType.ANNOUNCEMENT, this.announcementHandler); - // Ticket notifications this.handlers.set(NotifType.ANSWER_TICKET, this.answerTicketHandler); this.handlers.set(NotifType.CREATE_TICKET, this.createTicketHandler); this.handlers.set(NotifType.ASSIGN_TICKET, this.assignTicketHandler); - // Service notifications this.handlers.set(NotifType.BLOCK_SERVICE, this.blockServiceHandler); - // Payment notifications this.handlers.set(NotifType.PAYMENT_REMINDER, this.paymentReminderHandler); this.handlers.set(NotifType.PAYMENT_CANCELLATION, this.paymentCancellationHandler); + this.handlers.set(NotifType.USER_PASSWORD, this.userPasswordHandler); + this.logger.log(`Registered ${this.handlers.size} notification handlers`); } diff --git a/src/modules/notifications/handlers/user-password.handler.ts b/src/modules/notifications/handlers/user-password.handler.ts new file mode 100644 index 0000000..b0a9459 --- /dev/null +++ b/src/modules/notifications/handlers/user-password.handler.ts @@ -0,0 +1,23 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { QueryRunner } from "typeorm"; + +import { BaseNotificationHandler } from "./base-notification.handler"; +import { IUserPasswordNotificationData } from "../interfaces/ISendNotificationData"; +import { NotificationsService } from "../providers/notifications.service"; + +@Injectable() +export class UserPasswordHandler extends BaseNotificationHandler { + protected readonly logger = new Logger(UserPasswordHandler.name); + + constructor(private readonly notificationsService: NotificationsService) { + super(); + } + + protected async handle(recipientId: string, data: IUserPasswordNotificationData, queryRunner: QueryRunner): Promise { + await this.notificationsService.createUserPasswordNotification(recipientId, data, queryRunner); + } + + protected getHandlerName(): string { + return "UserPassword"; + } +} diff --git a/src/modules/notifications/interfaces/ISendNotificationData.ts b/src/modules/notifications/interfaces/ISendNotificationData.ts index 26ce2ec..27b6ac5 100755 --- a/src/modules/notifications/interfaces/ISendNotificationData.ts +++ b/src/modules/notifications/interfaces/ISendNotificationData.ts @@ -82,3 +82,7 @@ export interface INewCriticismNotificationData extends IBaseNotificationData { title: string; fullName: string; } + +export interface IUserPasswordNotificationData extends IBaseNotificationData { + password: string; +} diff --git a/src/modules/notifications/notifications.module.ts b/src/modules/notifications/notifications.module.ts index 660d424..fa9b6a1 100755 --- a/src/modules/notifications/notifications.module.ts +++ b/src/modules/notifications/notifications.module.ts @@ -5,34 +5,28 @@ import { TypeOrmModule } from "@nestjs/typeorm"; import { NOTIFICATION } from "./constants"; import { Notification } from "./entities/notification.entity"; +import { BlogCommentHandler } from "./handlers/blog-comment.handler"; +import { NotificationController } from "./notifications.controller"; +import { LoggerModule } from "../logger/logger.module"; import { - AnnouncementHandler, - AnswerTicketHandler, - ApproveInvoiceHandler, - AssignTicketHandler, - BillInvoiceHandler, - BillInvoiceReminderHandler, - BlockServiceHandler, - BlogCommentHandler, - CreateInvoiceHandler, - CreateTicketHandler, - InvoiceOverdueHandler, NewCriticismHandler, NewCustomerHandler, NewSubscriptionHandler, - NewTicketHandler, - NotificationHandlerFactory, - OtpHandler, - PaymentCancellationHandler, - PaymentReminderHandler, - RecurringInvoiceHandler, ServiceReviewHandler, - UserLoginHandler, - WalletChargeHandler, - WalletDeductionHandler, -} from "./handlers"; -import { NotificationController } from "./notifications.controller"; -import { LoggerModule } from "../logger/logger.module"; +} from "./handlers/admin-notifications.handler"; +import { AnnouncementHandler } from "./handlers/announcement.handler"; +import { + ApproveInvoiceHandler, + BillInvoiceHandler, + BillInvoiceReminderHandler, + CreateInvoiceHandler, + InvoiceOverdueHandler, + RecurringInvoiceHandler, +} from "./handlers/invoice.handler"; +import { PaymentCancellationHandler, PaymentReminderHandler } from "./handlers/payment.handler"; +import { BlockServiceHandler } from "./handlers/service.handler"; +import { AnswerTicketHandler, AssignTicketHandler, CreateTicketHandler, NewTicketHandler } from "./handlers/ticket.handler"; +import { UserPasswordHandler } from "./handlers/user-password.handler"; import { NotificationsService } from "./providers/notifications.service"; import { NotificationProcessor } from "./queue/notification.processor"; import { NotificationQueue } from "./queue/notification.queue"; @@ -40,6 +34,10 @@ import { NotificationRepository } from "./repositories/notifications.repository" import { NotificationSetting } from "../settings/entities/notification-setting.entity"; import { SettingModule } from "../settings/settings.module"; import { UtilsModule } from "../utils/utils.module"; +import { NotificationHandlerFactory } from "./handlers/notification-handler.factory"; +import { OtpHandler } from "./handlers/otp.handler"; +import { UserLoginHandler } from "./handlers/user-login.handler"; +import { WalletChargeHandler, WalletDeductionHandler } from "./handlers/wallet.handler"; @Module({ imports: [ @@ -92,6 +90,7 @@ import { UtilsModule } from "../utils/utils.module"; BlockServiceHandler, PaymentReminderHandler, PaymentCancellationHandler, + UserPasswordHandler, ], controllers: [NotificationController], exports: [NotificationRepository, NotificationsService, NotificationQueue], diff --git a/src/modules/notifications/providers/notifications.service.ts b/src/modules/notifications/providers/notifications.service.ts index 82bf2dc..dbcb0aa 100755 --- a/src/modules/notifications/providers/notifications.service.ts +++ b/src/modules/notifications/providers/notifications.service.ts @@ -23,6 +23,7 @@ import { IServiceReviewNotificationData, ISubscriptionNotificationData, ITicketNotificationData, + IUserPasswordNotificationData, IWalletNotificationData, } from "../interfaces/ISendNotificationData"; import { NotificationRepository } from "../repositories/notifications.repository"; @@ -551,4 +552,15 @@ export class NotificationsService { queryRunner, ); } + //************************************************* */ + + async createUserPasswordNotification(recipientId: string, data: IUserPasswordNotificationData, queryRunner: QueryRunner) { + const message = NotificationMessage.USER_PASSWORD_MESSAGE.replace("[password]", data.password); + + await this.smsService.sendUserPasswordSms(data.userPhone, data.password); + return this.createNotification( + { title: NotificationMessage.USER_PASSWORD, type: NotifType.USER_PASSWORD, message, recipientId }, + queryRunner, + ); + } } diff --git a/src/modules/notifications/queue/notification.queue.ts b/src/modules/notifications/queue/notification.queue.ts index a12196e..86aef76 100644 --- a/src/modules/notifications/queue/notification.queue.ts +++ b/src/modules/notifications/queue/notification.queue.ts @@ -17,6 +17,7 @@ import { IServiceReviewNotificationData, ISubscriptionNotificationData, ITicketNotificationData, + IUserPasswordNotificationData, IWalletNotificationData, } from "../interfaces/ISendNotificationData"; @@ -145,4 +146,8 @@ export class NotificationQueue { async addPaymentCancellationNotification(recipientId: string, data: IPaymentNotificationData) { await this.addNotificationJob(NotifType.PAYMENT_CANCELLATION, recipientId, data); } + + async addUserPasswordNotification(recipientId: string, data: IUserPasswordNotificationData) { + await this.addNotificationJob(NotifType.USER_PASSWORD, recipientId, data); + } } diff --git a/src/modules/settings/enums/notif-settings.enum.ts b/src/modules/settings/enums/notif-settings.enum.ts index 6ed9856..c62e020 100755 --- a/src/modules/settings/enums/notif-settings.enum.ts +++ b/src/modules/settings/enums/notif-settings.enum.ts @@ -10,6 +10,7 @@ export enum NotifCategory { export enum NotifType { // Account category USER_LOGIN = "USER_LOGIN", + USER_PASSWORD = "USER_PASSWORD", ANNOUNCEMENT = "ANNOUNCEMENT", // Finance category @@ -50,6 +51,8 @@ export const NotifDescriptions: Record { + async createUser(registerDto: VerifyOtpDto, hashedPassword: string, queryRunner: QueryRunner): Promise { const role = await queryRunner.manager.findOneBy(Role, { name: RoleEnum.USER }); if (!role) throw new BadRequestException(UserMessage.ROLE_NOT_FOUND); @@ -247,7 +246,10 @@ export class UsersService { // const existUser = await queryRunner.manager.findOneBy(User, { nationalCode: registerDto.nationalCode }); // if (existUser) throw new BadRequestException(UserMessage.NATIONAL_CODE_EXIST); - const userName = slugify(`${registerDto.firstName} ${Date.now().toString().slice(-5)}`, { lower: true, trim: true }); + const userName = slugify(`u-${registerDto.phone}`, { lower: true, trim: true }); + + const defaultFirstName = "کاربر"; + const defaultLastName = userName.slice(0, 5); const { referralCode, ...userData } = registerDto; const user = queryRunner.manager.create(User, { @@ -255,6 +257,8 @@ export class UsersService { userName, password: hashedPassword, roles: [role], + firstName: defaultFirstName, + lastName: defaultLastName, }); await queryRunner.manager.save(user); @@ -383,10 +387,10 @@ export class UsersService { /************************************************************ */ private async checkUserLoginCredentialWithPhone(phone: string, otpCode: string, userId: string) { - const isValid = await this.otpService.verifyOtp(phone, otpCode, "LOGIN"); + const isValid = await this.otpService.verifyOtp(phone, otpCode, "LOGIN/REGISTER"); if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP); - await this.otpService.delOtpFormCache(phone, "LOGIN"); + await this.otpService.delOtpFormCache(phone, "LOGIN/REGISTER"); const user = await this.userRepository.findOneBy({ id: userId }); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); diff --git a/src/modules/utils/interfaces/IOtpKey.ts b/src/modules/utils/interfaces/IOtpKey.ts index 8389763..10fe7a0 100755 --- a/src/modules/utils/interfaces/IOtpKey.ts +++ b/src/modules/utils/interfaces/IOtpKey.ts @@ -1 +1 @@ -export type OtpCacheKeyType = "LOGIN" | "REGISTER" | "FORGOT_PASSWORD" | "INVOICE_VERIFY"; +export type OtpCacheKeyType = "LOGIN/REGISTER" | "FORGOT_PASSWORD" | "INVOICE_VERIFY"; diff --git a/src/modules/utils/interfaces/ISms.ts b/src/modules/utils/interfaces/ISms.ts index 7633046..ab2d3af 100755 --- a/src/modules/utils/interfaces/ISms.ts +++ b/src/modules/utils/interfaces/ISms.ts @@ -48,4 +48,5 @@ export type TemplateParams = | "message" | "blogTitle" | "fullName" - | "mobile"; + | "mobile" + | "password"; diff --git a/src/modules/utils/providers/sms.service.ts b/src/modules/utils/providers/sms.service.ts index eea4c92..9322dd0 100755 --- a/src/modules/utils/providers/sms.service.ts +++ b/src/modules/utils/providers/sms.service.ts @@ -789,5 +789,35 @@ export class SmsService { } } + async sendUserPasswordSms(mobile: string, password: string) { + const smsData: ISmsVerifyBody = { + Parameters: [{ name: "password", value: password }], + Mobile: mobile, + TemplateId: this.smsConfigs.SMS_PATTERN_USER_PASSWORD, + }; + + try { + const { data } = await firstValueFrom( + this.httpService + .post( + `${this.smsConfigs.API_URL}/send/verify`, + { ...smsData }, + { + headers: { "X-API-KEY": this.smsConfigs.API_KEY }, + }, + ) + .pipe( + catchError((err: AxiosError) => { + this.logger.error("error in sending user password sms", err); + throw new InternalServerErrorException("error in sending user password sms"); + }), + ), + ); + return data; + } catch (error) { + this.logger.error("error in sending sms", error); + } + } + //************************************************* */ } diff --git a/src/modules/utils/providers/string.utils.ts b/src/modules/utils/providers/string.utils.ts index 8e0221e..9785101 100644 --- a/src/modules/utils/providers/string.utils.ts +++ b/src/modules/utils/providers/string.utils.ts @@ -2,3 +2,19 @@ export function truncateIfLong(str: string, maxLength = 30): string { if (typeof str !== "string") return str; return str.length > maxLength ? str.slice(0, maxLength) + "..." : str; } + +export function randomizeCase(str: string): string { + if (typeof str !== "string") return str; + + return str + .split("") + .map((char) => { + // Only process alphabetic characters + if (/[a-zA-Z]/.test(char)) { + // 50% chance to make it uppercase, 50% lowercase + return Math.random() < 0.5 ? char.toLowerCase() : char.toUpperCase(); + } + return char; // Keep non-alphabetic characters as they are + }) + .join(""); +}