chore: change the login and regitster flow to single route
This commit is contained in:
@@ -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] جدید برای شما صادر شده است",
|
||||
|
||||
@@ -32,6 +32,7 @@ export function smsConfigs() {
|
||||
SMS_PATTERN_NEW_SUBSCRIPTION: configService.getOrThrow<string>("SMS_PATTERN_NEW_SUBSCRIPTION"),
|
||||
SMS_PATTERN_NEW_TICKET_GLOBAL: configService.getOrThrow<string>("SMS_PATTERN_NEW_TICKET_GLOBAL"),
|
||||
SMS_PATTERN_NEW_CRITICISM: configService.getOrThrow<string>("SMS_PATTERN_NEW_CRITICISM"),
|
||||
SMS_PATTERN_USER_PASSWORD: configService.getOrThrow<string>("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;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export function databaseConfigs(): TypeOrmModuleAsyncOptions {
|
||||
username: configService.getOrThrow<string>("DB_USER"),
|
||||
password: configService.getOrThrow<string>("DB_PASS"),
|
||||
autoLoadEntities: true,
|
||||
synchronize: configService.getOrThrow<string>("NODE_ENV") == "production" ? false : true,
|
||||
synchronize: configService.getOrThrow<string>("NODE_ENV") == "production" ? false : false,
|
||||
logging: configService.getOrThrow<string>("NODE_ENV") == "production" ? false : true,
|
||||
migrationsTableName: "typeorm_migrations",
|
||||
migrationsRun: false,
|
||||
|
||||
@@ -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;
|
||||
// }
|
||||
|
||||
@@ -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"]) {}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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[]) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<NotifType, BaseNotificationHandler> = 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`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<IUserPasswordNotificationData> {
|
||||
protected readonly logger = new Logger(UserPasswordHandler.name);
|
||||
|
||||
constructor(private readonly notificationsService: NotificationsService) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected async handle(recipientId: string, data: IUserPasswordNotificationData, queryRunner: QueryRunner): Promise<void> {
|
||||
await this.notificationsService.createUserPasswordNotification(recipientId, data, queryRunner);
|
||||
}
|
||||
|
||||
protected getHandlerName(): string {
|
||||
return "UserPassword";
|
||||
}
|
||||
}
|
||||
@@ -82,3 +82,7 @@ export interface INewCriticismNotificationData extends IBaseNotificationData {
|
||||
title: string;
|
||||
fullName: string;
|
||||
}
|
||||
|
||||
export interface IUserPasswordNotificationData extends IBaseNotificationData {
|
||||
password: string;
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<NotifType, { fa: string; category: NotifC
|
||||
// Account category
|
||||
[NotifType.USER_LOGIN]: { fa: "ورود به ناحیه کاربری", category: NotifCategory.ACCOUNT },
|
||||
[NotifType.ANNOUNCEMENT]: { fa: "اعلانات و اطلاعیه ها", category: NotifCategory.ACCOUNT },
|
||||
//DO_NOT_CREATE_THIS_NOTIFICATION_IN_DATABASE
|
||||
[NotifType.USER_PASSWORD]: { fa: "ارسال رمز عبور پیشفرض", category: NotifCategory.ACCOUNT },
|
||||
|
||||
// Finance category
|
||||
[NotifType.PAYMENT_REMINDER]: { fa: "یادآوری پرداخت", category: NotifCategory.FINANCE },
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { ApiProperty, ApiPropertyOptional, PickType } from "@nestjs/swagger";
|
||||
import { IsEmail, IsNotEmpty, IsOptional, IsString, IsUUID, IsUrl, MinLength } from "class-validator";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsEmail, IsMobilePhone, IsNotEmpty, IsOptional, IsString, IsUUID, IsUrl, Length, MinLength } from "class-validator";
|
||||
|
||||
import { AdminMessage, AuthMessage } from "../../../common/enums/message.enum";
|
||||
import { CompleteRegistrationDto } from "../../auth/DTO/complete-register.dto";
|
||||
|
||||
export class CreateAdminDto extends PickType(CompleteRegistrationDto, ["firstName", "lastName", "phone", "password"] as const) {
|
||||
export class CreateAdminDto {
|
||||
@IsNotEmpty({ message: AuthMessage.PASSWORD_NOT_EMPTY })
|
||||
@IsString({ message: AuthMessage.PASSWORD_FORMAT_INVALID })
|
||||
@ApiProperty({ description: "repeat password", example: "12345678" })
|
||||
@@ -16,14 +15,38 @@ export class CreateAdminDto extends PickType(CompleteRegistrationDto, ["firstNam
|
||||
@ApiProperty({ description: "Email", example: "ma@gmail.com" })
|
||||
email: 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;
|
||||
|
||||
@IsNotEmpty({ message: AdminMessage.ROLE_REQUIRED })
|
||||
@IsUUID("4", { message: AdminMessage.ROLE_ID_SHOULD_BE_UUID })
|
||||
@ApiProperty({ description: "Role id", example: "123e4567-e89b-12d3-a456-426614174000" })
|
||||
roleId: 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.PASSWORD_NOT_EMPTY })
|
||||
@IsString({ message: AuthMessage.PASSWORD_FORMAT_INVALID })
|
||||
@ApiProperty({ description: "password", example: "12S345SS678" })
|
||||
@MinLength(8, { message: AuthMessage.PASSWORD_LENGTH })
|
||||
password: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNotEmpty({ message: AdminMessage.PROFILE_PIC_REQUIRED })
|
||||
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: AdminMessage.PROFILE_PIC_URL })
|
||||
@ApiPropertyOptional({ description: "Profile picture", example: "https://www.google.com" })
|
||||
profilePic: string;
|
||||
profilePic?: string;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,35 @@
|
||||
import { ApiProperty, PickType } from "@nestjs/swagger";
|
||||
import { IsEnum, IsInt, IsNotEmpty, IsNumberString, IsString, Length } from "class-validator";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsEnum, IsInt, IsMobilePhone, IsNotEmpty, IsNumberString, IsString, Length } from "class-validator";
|
||||
|
||||
import { IsNationalCode } from "../../../common/decorators/is-national-code.decorator";
|
||||
import { AuthMessage, FinancialMessage } from "../../../common/enums/message.enum";
|
||||
import { CompleteRegistrationDto } from "../../auth/DTO/complete-register.dto";
|
||||
import { GenderEnum } from "../enums/gender-type.enum";
|
||||
import { NationalityEnum } from "../enums/nationality.enum";
|
||||
|
||||
export class CreateRealUserDto extends PickType(CompleteRegistrationDto, ["firstName", "lastName", "phone"]) {
|
||||
export class CreateRealUserDto {
|
||||
@IsNotEmpty({ message: FinancialMessage.GENDER_TYPE_REQUIRED })
|
||||
@IsEnum(GenderEnum)
|
||||
@ApiProperty({ enum: GenderEnum, description: "Gender type", example: GenderEnum.MALE })
|
||||
gender: GenderEnum;
|
||||
|
||||
@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;
|
||||
|
||||
@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: FinancialMessage.FATHER_NAME_REQUIRED })
|
||||
@IsString({ message: FinancialMessage.FATHER_NAME_INVALID })
|
||||
@ApiProperty({ description: "Father name" })
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
import { ApiProperty, ApiPropertyOptional, PartialType, PickType } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsNumberString, IsOptional, IsString, IsUrl, Length } from "class-validator";
|
||||
|
||||
import { IsNationalCode } from "../../../common/decorators/is-national-code.decorator";
|
||||
import { AuthMessage, UserMessage } from "../../../common/enums/message.enum";
|
||||
import { CompleteRegistrationDto } from "../../auth/DTO/complete-register.dto";
|
||||
|
||||
export class UpdateProfileDto extends PartialType(PickType(CompleteRegistrationDto, ["firstName", "lastName"] as const)) {
|
||||
export class UpdateProfileDto {
|
||||
@IsOptional()
|
||||
@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;
|
||||
|
||||
@IsOptional()
|
||||
@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: UserMessage.USERNAME_NOT_EMPTY })
|
||||
@Length(3, 50, { message: UserMessage.USERNAME_SHOULD_BE_BETWEEN_3_AND_50 })
|
||||
@@ -17,7 +30,7 @@ export class UpdateProfileDto extends PartialType(PickType(CompleteRegistrationD
|
||||
@IsNotEmpty({ message: AuthMessage.BIRTH_DATE_NOT_EMPTY })
|
||||
@IsString({ message: AuthMessage.BIRTH_DATE_NOT_EMPTY })
|
||||
@ApiProperty({ description: "Birth date", example: "1403/01/01" })
|
||||
birthDate: string;
|
||||
birthDate?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNotEmpty({ message: AuthMessage.NATIONAL_NOT_EMPTY })
|
||||
@@ -25,7 +38,7 @@ export class UpdateProfileDto extends PartialType(PickType(CompleteRegistrationD
|
||||
@Length(10, 10, { message: AuthMessage.NATIONAL_CODE_INCORRECT })
|
||||
@IsNationalCode({ message: AuthMessage.NATIONAL_CODE_INVALID })
|
||||
@ApiProperty({ description: "iranian format (10 char)", example: "4569852169" })
|
||||
nationalCode: string;
|
||||
nationalCode?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "Profile picture", example: "https://www.google.com" })
|
||||
@IsOptional()
|
||||
@@ -47,8 +60,8 @@ export class UpdateProfileDto extends PartialType(PickType(CompleteRegistrationD
|
||||
@Length(10, 10, { message: UserMessage.POSTAL_CODE_LENGTH })
|
||||
postalCode?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "City id" })
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ description: "City id" })
|
||||
@IsNotEmpty({ message: UserMessage.CITY_REQUIRED })
|
||||
cityId: string;
|
||||
cityId?: string;
|
||||
}
|
||||
|
||||
@@ -50,13 +50,13 @@ export class User extends BaseEntity {
|
||||
lastName: string;
|
||||
|
||||
@Column({ type: "varchar", length: 12, nullable: true })
|
||||
birthDate: string;
|
||||
birthDate?: string;
|
||||
|
||||
@Column({ type: "varchar", length: 100, unique: true, nullable: true })
|
||||
nationalCode: string | null;
|
||||
nationalCode?: string;
|
||||
|
||||
@Column({ type: "varchar", length: 100, nullable: true })
|
||||
profilePic: string;
|
||||
profilePic?: string;
|
||||
|
||||
@Column({ type: "boolean", default: false })
|
||||
emailVerified: boolean;
|
||||
|
||||
@@ -8,7 +8,6 @@ import { In, Not, QueryRunner } from "typeorm";
|
||||
|
||||
import { AuthMessage, CommonMessage, UserMessage } from "../../../common/enums/message.enum";
|
||||
import { AddressService } from "../../address/providers/address.service";
|
||||
import { CompleteRegistrationDto } from "../../auth/DTO/complete-register.dto";
|
||||
import { RequestOtpDto } from "../../auth/DTO/request-otp.dto";
|
||||
import { VerifyOtpDto } from "../../auth/DTO/verify-otp.dto";
|
||||
import { ReferralsService } from "../../referrals/providers/referrals.service";
|
||||
@@ -201,7 +200,7 @@ export class UsersService {
|
||||
const existPhone = await this.userRepository.findOneBy({ phone });
|
||||
if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST);
|
||||
|
||||
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,
|
||||
@@ -209,7 +208,7 @@ export class UsersService {
|
||||
};
|
||||
}
|
||||
|
||||
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.debug(`OTP sent to ${phone}: ${otpCode}`);
|
||||
@@ -237,7 +236,7 @@ export class UsersService {
|
||||
|
||||
/************************************************************ */
|
||||
|
||||
async createUser(registerDto: CompleteRegistrationDto, hashedPassword: string, queryRunner: QueryRunner): Promise<User> {
|
||||
async createUser(registerDto: VerifyOtpDto, hashedPassword: string, queryRunner: QueryRunner): Promise<User> {
|
||||
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);
|
||||
|
||||
@@ -1 +1 @@
|
||||
export type OtpCacheKeyType = "LOGIN" | "REGISTER" | "FORGOT_PASSWORD" | "INVOICE_VERIFY";
|
||||
export type OtpCacheKeyType = "LOGIN/REGISTER" | "FORGOT_PASSWORD" | "INVOICE_VERIFY";
|
||||
|
||||
@@ -48,4 +48,5 @@ export type TemplateParams =
|
||||
| "message"
|
||||
| "blogTitle"
|
||||
| "fullName"
|
||||
| "mobile";
|
||||
| "mobile"
|
||||
| "password";
|
||||
|
||||
@@ -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<ISmsVerifyResponse>(
|
||||
`${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);
|
||||
}
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
}
|
||||
|
||||
@@ -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("");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user