chore: first
This commit is contained in:
Executable
+24
@@ -0,0 +1,24 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsString, MinLength } from "class-validator";
|
||||
|
||||
import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class ChangePasswordDto {
|
||||
@IsNotEmpty({ message: AuthMessage.PASSWORD_NOT_EMPTY })
|
||||
@IsString({ message: AuthMessage.PASSWORD_FORMAT_INVALID })
|
||||
@ApiProperty({ description: "old 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: "new password", example: "12S345SS678" })
|
||||
@MinLength(8, { message: AuthMessage.PASSWORD_LENGTH })
|
||||
newPassword: string;
|
||||
|
||||
@IsNotEmpty({ message: AuthMessage.PASSWORD_NOT_EMPTY })
|
||||
@IsString({ message: AuthMessage.PASSWORD_FORMAT_INVALID })
|
||||
@ApiProperty({ description: "re new password", example: "12S345SS678" })
|
||||
@MinLength(8, { message: AuthMessage.PASSWORD_LENGTH })
|
||||
repeatPassword: string;
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsNumberString, IsString, Length, MinLength } from "class-validator";
|
||||
import { IsMobilePhone } from "class-validator";
|
||||
|
||||
import { IsNationalCode } from "../../../common/decorators/is-national-code.decorator";
|
||||
import { AuthMessage } 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;
|
||||
|
||||
@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.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.BIRTH_DATE_NOT_EMPTY })
|
||||
@IsString({ message: AuthMessage.BIRTH_DATE_NOT_EMPTY })
|
||||
@ApiProperty({ description: "Birth date", example: "1403/01/01" })
|
||||
birthDate: string;
|
||||
|
||||
@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;
|
||||
}
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
import { ApiProperty, PickType } from "@nestjs/swagger";
|
||||
import { IsEmail, IsNotEmpty, IsString } from "class-validator";
|
||||
|
||||
import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class LoginPasswordDTO {
|
||||
@IsNotEmpty({ message: AuthMessage.EMAIL_NOT_EMPTY })
|
||||
@IsEmail(undefined, { message: AuthMessage.INVALID_EMAIL_FORMAT })
|
||||
@ApiProperty({ description: "email of user", default: "admin-dsc@gmail.com" })
|
||||
email: string;
|
||||
|
||||
@IsNotEmpty({ message: AuthMessage.PasswordNotEmpty })
|
||||
@IsString({ message: AuthMessage.INVALID_PASS_FORMAT })
|
||||
@ApiProperty({ type: "string", description: "password user", example: "admin123" })
|
||||
password: string;
|
||||
}
|
||||
|
||||
export class CheckUserExistDto extends PickType(LoginPasswordDTO, ["email"] as const) {}
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsString } from "class-validator";
|
||||
|
||||
export class RefreshTokenDto {
|
||||
@ApiProperty({ description: "Refresh token" })
|
||||
@IsString({ message: "Refresh token must be a string" })
|
||||
@IsNotEmpty({ message: "Refresh token is required" })
|
||||
refreshToken: string;
|
||||
}
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsMobilePhone, IsNotEmpty, Length } from "class-validator";
|
||||
|
||||
import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class RequestOtpDto {
|
||||
@IsNotEmpty({ message: AuthMessage.PHONE_NOT_EMPTY })
|
||||
@Length(11, 11, { message: AuthMessage.PHONE_SHOULD_BE_11_DIGIT })
|
||||
@IsMobilePhone("fa-IR", {}, { message: AuthMessage.INVALID_PHONE_FORMAT })
|
||||
@ApiProperty({ description: "phone number", default: "09922320740" })
|
||||
phone: string;
|
||||
}
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
import { ApiProperty, PickType } from "@nestjs/swagger";
|
||||
import { IsMobilePhone, IsNotEmpty, IsNumberString, Length } from "class-validator";
|
||||
|
||||
import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class VerifyOtpDto {
|
||||
@IsNotEmpty({ message: AuthMessage.PHONE_NOT_EMPTY })
|
||||
@Length(11, 11, { message: AuthMessage.PHONE_SHOULD_BE_11_DIGIT })
|
||||
@IsMobilePhone("fa-IR", {}, { message: AuthMessage.INVALID_PHONE_FORMAT })
|
||||
@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;
|
||||
}
|
||||
|
||||
export class VerifyOtpWithUserId extends PickType(VerifyOtpDto, ["code"]) {}
|
||||
Executable
+98
@@ -0,0 +1,98 @@
|
||||
import { Body, Controller, HttpCode, HttpStatus, Patch, Post, UseGuards } from "@nestjs/common";
|
||||
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";
|
||||
import { VerifyOtpDto } from "./DTO/verify-otp.dto";
|
||||
import { AuthService } from "./providers/auth.service";
|
||||
import { AUTH_THROTTLE_LIMIT, AUTH_THROTTLE_TTL, AUTH__REFRESH_THROTTLE_LIMIT, AUTH__REFRESH_THROTTLE_TTL } from "../../common/constants";
|
||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||
import { UserDec } from "../../common/decorators/user.decorator";
|
||||
|
||||
@ApiTags("Auth")
|
||||
@Controller("auth")
|
||||
@Throttle({ default: { limit: AUTH_THROTTLE_LIMIT, ttl: AUTH_THROTTLE_TTL } })
|
||||
@UseGuards(ThrottlerGuard)
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@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")
|
||||
sendOtp(@Body() requestOtpDto: RequestOtpDto) {
|
||||
return this.authService.requestLoginOtp(requestOtpDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "verify otp for login" })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("otp/verify")
|
||||
verifyOtp(@Body() verifyOtpDto: VerifyOtpDto) {
|
||||
return this.authService.verifyLoginOtp(verifyOtpDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "check if user email exist" })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("check")
|
||||
checkUserExist(@Body() checkUserExistDto: CheckUserExistDto) {
|
||||
return this.authService.checkUserExist(checkUserExistDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "login with password" })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("login/password")
|
||||
loginWithPassword(@Body() loginPasswordDto: LoginPasswordDTO) {
|
||||
return this.authService.loginWithPassword(loginPasswordDto);
|
||||
}
|
||||
|
||||
@AuthGuards()
|
||||
@ApiOperation({ summary: "Update user password" })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Patch("change-password")
|
||||
changePassword(@UserDec("id") userId: string, @Body() changePasswordDto: ChangePasswordDto) {
|
||||
return this.authService.changePassword(userId, changePasswordDto);
|
||||
}
|
||||
|
||||
@Throttle({ default: { limit: AUTH__REFRESH_THROTTLE_LIMIT, ttl: AUTH__REFRESH_THROTTLE_TTL } })
|
||||
@ApiOperation({ summary: "refresh the user access token / refresh token" })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("refresh")
|
||||
refresh(@Body() refreshTokenDto: RefreshTokenDto) {
|
||||
return this.authService.refreshToken(refreshTokenDto.refreshToken);
|
||||
}
|
||||
|
||||
@AuthGuards()
|
||||
@ApiOperation({ summary: "logout the user" })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("logout")
|
||||
logout(@UserDec("id") userId: string) {
|
||||
return this.authService.logout(userId);
|
||||
}
|
||||
//***************************** */
|
||||
|
||||
// @Post("forgot-password")
|
||||
// async forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto) {
|
||||
// return this.authService.forgotPassword(forgotPasswordDto);
|
||||
// }
|
||||
|
||||
// @Post("reset-password")
|
||||
// async resetPassword(@Body() resetPasswordDto: ResetPasswordDto) {
|
||||
// return this.authService.resetPassword(resetPasswordDto);
|
||||
// }
|
||||
}
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { JwtModule } from "@nestjs/jwt";
|
||||
import { PassportModule } from "@nestjs/passport";
|
||||
|
||||
import { AuthController } from "./auth.controller";
|
||||
import { jwtConfig } from "../../configs/jwt.config";
|
||||
import { UtilsModule } from "../utils/utils.module";
|
||||
import { AuthService } from "./providers/auth.service";
|
||||
import { TokensService } from "./providers/tokens.service";
|
||||
import { JwtStrategy } from "./strategies/jwt.strategy";
|
||||
import { NotificationModule } from "../notifications/notifications.module";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
|
||||
@Module({
|
||||
imports: [UtilsModule, UsersModule, PassportModule, JwtModule.registerAsync(jwtConfig()), NotificationModule],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, TokensService, JwtStrategy],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { FastifyRequest } from "fastify";
|
||||
|
||||
import { ADMIN_ROUTE } from "../../../common/decorators/admin.decorator";
|
||||
import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
@Injectable()
|
||||
export class AdminRouteGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredAdmin = this.reflector.getAllAndOverride<boolean>(ADMIN_ROUTE, [context.getHandler(), context.getClass()]);
|
||||
if (!requiredAdmin) return true;
|
||||
|
||||
const req = context.switchToHttp().getRequest<FastifyRequest>();
|
||||
const user = req.user;
|
||||
|
||||
if (!user) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS);
|
||||
|
||||
if (!user.role) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
import { ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { JWT_STRATEGY_NAME } from "../../../common/constants";
|
||||
import { SKIP_AUTH_KEY } from "../../../common/decorators/skip-auth.decorator";
|
||||
import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard(JWT_STRATEGY_NAME) {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
|
||||
const skipAuth = this.reflector.getAllAndOverride<boolean>(SKIP_AUTH_KEY, [context.getHandler(), context.getClass()]);
|
||||
|
||||
if (skipAuth) return true;
|
||||
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
handleRequest(err: unknown, user: any, _info: unknown) {
|
||||
if (err || !user) throw new UnauthorizedException(AuthMessage.TOKEN_EXPIRED_OR_INVALID);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
// import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from "@nestjs/common";
|
||||
// import { Reflector } from "@nestjs/core";
|
||||
// import { FastifyRequest } from "fastify";
|
||||
|
||||
// import { PERMISSION_KEY } from "../../../common/decorators/permission.decorator";
|
||||
// import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
// import { PermissionEnum } from "../../users/enums/permission.enum";
|
||||
|
||||
// @Injectable()
|
||||
// export class PermissionsGuard implements CanActivate {
|
||||
// constructor(private reflector: Reflector) {}
|
||||
|
||||
// canActivate(context: ExecutionContext) {
|
||||
// const requiredPermissions = this.reflector.getAllAndOverride<PermissionEnum[]>(PERMISSION_KEY, [
|
||||
// context.getHandler(),
|
||||
// context.getClass(),
|
||||
// ]);
|
||||
|
||||
// if (!requiredPermissions) return true;
|
||||
|
||||
// const request = context.switchToHttp().getRequest<FastifyRequest>();
|
||||
// const user = request.user;
|
||||
|
||||
// if (!user) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS);
|
||||
|
||||
// const hasPermission = requiredPermissions.every((perm) => user.permissions.includes(perm));
|
||||
|
||||
// if (!hasPermission) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS);
|
||||
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
// import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from "@nestjs/common";
|
||||
// import { Reflector } from "@nestjs/core";
|
||||
// import { FastifyRequest } from "fastify";
|
||||
|
||||
// import { ROLES_KEY } from "../../../common/decorators/roles.decorator";
|
||||
// import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
// import { RoleEnum } from "../../users/enums/role.enum";
|
||||
|
||||
// @Injectable()
|
||||
// export class RoleGuard implements CanActivate {
|
||||
// constructor(private reflector: Reflector) {}
|
||||
|
||||
// canActivate(context: ExecutionContext): boolean {
|
||||
// const requiredRole = this.reflector.getAllAndOverride<RoleEnum[]>(ROLES_KEY, [context.getHandler(), context.getClass()]);
|
||||
// if (!requiredRole) return true;
|
||||
|
||||
// const req = context.switchToHttp().getRequest<FastifyRequest>();
|
||||
// const user = req.user;
|
||||
|
||||
// if (!user) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS);
|
||||
|
||||
// const hasRequiredRole = requiredRole.some((role) => user.roles.includes(role));
|
||||
// if (!hasRequiredRole) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS);
|
||||
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { RoleEnum } from "../../users/enums/role.enum";
|
||||
|
||||
export interface ITokenPayload {
|
||||
id: string;
|
||||
role: RoleEnum;
|
||||
}
|
||||
Executable
+227
@@ -0,0 +1,227 @@
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
|
||||
import { TokensService } from "./tokens.service";
|
||||
import { AuthMessage, UserMessage } from "../../../common/enums/message.enum";
|
||||
import { NotificationQueue } from "../../notifications/queue/notification.queue";
|
||||
import { Role } from "../../users/entities/role.entity";
|
||||
import { RoleEnum } from "../../users/enums/role.enum";
|
||||
import { UsersService } from "../../users/services/users.service";
|
||||
import { OTPService } from "../../utils/providers/otp.service";
|
||||
import { PasswordService } from "../../utils/providers/password.service";
|
||||
import { SmsService } from "../../utils/providers/sms.service";
|
||||
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 {
|
||||
constructor(
|
||||
private readonly usersService: UsersService,
|
||||
private readonly passwordService: PasswordService,
|
||||
private readonly otpService: OTPService,
|
||||
private readonly tokensService: TokensService,
|
||||
private readonly notificationQueue: NotificationQueue,
|
||||
private readonly smsService: SmsService,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
//****************** */
|
||||
//****************** */
|
||||
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.notificationQueue.addOtpNotification(phone, { userPhone: phone, otp: otpCode });
|
||||
await this.smsService.sendSmsVerifyCode(phone, otpCode);
|
||||
|
||||
return {
|
||||
message: AuthMessage.OTP_SENT,
|
||||
};
|
||||
}
|
||||
//****************** */
|
||||
//****************** */
|
||||
async completeRegistration(completeRegistrationDto: CompleteRegistrationDto) {
|
||||
const { phone, code } = completeRegistrationDto;
|
||||
|
||||
const entityManager = this.em.fork();
|
||||
|
||||
try {
|
||||
await entityManager.begin();
|
||||
//
|
||||
const isValid = await this.otpService.verifyOtp(phone, code, "REGISTER");
|
||||
|
||||
if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP);
|
||||
|
||||
await this.otpService.delOtpFormCache(phone, "REGISTER");
|
||||
const hashedPassword = await this.passwordService.hashPassword(completeRegistrationDto.password);
|
||||
|
||||
const user = await this.usersService.createUser(completeRegistrationDto, hashedPassword, entityManager);
|
||||
|
||||
const tokens = await this.tokensService.generateTokens(user, entityManager);
|
||||
|
||||
await entityManager.commit();
|
||||
|
||||
return {
|
||||
message: AuthMessage.USER_REGISTER_SUCCESS,
|
||||
...tokens,
|
||||
};
|
||||
} catch (error) {
|
||||
await entityManager.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
//****************** */
|
||||
//****************** */
|
||||
async loginWithPassword(loginDto: LoginPasswordDTO) {
|
||||
const { email, password } = loginDto;
|
||||
|
||||
const user = await this.checkUserLoginCredentialWithEmail(email, password);
|
||||
|
||||
if (this.checkUserIsAdmin(user.role)) throw new BadRequestException(AuthMessage.ADMIN_CAN_NOT_LOGIN);
|
||||
|
||||
const tokens = await this.tokensService.generateTokens(user);
|
||||
|
||||
await this.notificationQueue.addLoginNotification(user.id, { userPhone: user.phone });
|
||||
|
||||
return {
|
||||
message: AuthMessage.PASSWORD_LOGIN_SUCCESS,
|
||||
...tokens,
|
||||
};
|
||||
}
|
||||
|
||||
//****************** */
|
||||
//****************** */
|
||||
|
||||
async checkUserExist(checkUserExistDto: CheckUserExistDto) {
|
||||
const user = await this.usersService.findOneWithEmail(checkUserExistDto.email);
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
return { message: UserMessage.USER_EXISTS };
|
||||
}
|
||||
|
||||
//****************** */
|
||||
//****************** */
|
||||
|
||||
async requestLoginOtp(requestOtpDto: RequestOtpDto, isAdmin: boolean = false) {
|
||||
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.role);
|
||||
|
||||
if (isAdmin && !isUserAdmin) throw new BadRequestException(AuthMessage.NOT_ADMIN);
|
||||
|
||||
const existCode = await this.otpService.checkExistOtp(phone, "LOGIN");
|
||||
if (existCode) {
|
||||
return {
|
||||
message: AuthMessage.OTP_ALREADY_SENT,
|
||||
ttlSecond: existCode,
|
||||
};
|
||||
}
|
||||
|
||||
const otpCode = await this.otpService.generateAndSetInCache(phone, "LOGIN");
|
||||
//
|
||||
await this.smsService.sendSmsVerifyCode(phone, otpCode);
|
||||
|
||||
return {
|
||||
message: AuthMessage.OTP_SENT,
|
||||
};
|
||||
}
|
||||
|
||||
//****************** */
|
||||
//****************** */
|
||||
|
||||
async verifyLoginOtp(verifyOtpDto: VerifyOtpDto) {
|
||||
const { code, phone } = verifyOtpDto;
|
||||
|
||||
const user = await this.checkUserLoginCredentialWithPhone(phone, code);
|
||||
|
||||
if (this.checkUserIsAdmin(user.role)) throw new BadRequestException(AuthMessage.ADMIN_CAN_NOT_LOGIN);
|
||||
|
||||
const tokens = await this.tokensService.generateTokens(user);
|
||||
|
||||
await this.notificationQueue.addLoginNotification(user.id, { userPhone: user.phone, userEmail: user.email });
|
||||
return {
|
||||
message: AuthMessage.LOGIN_SUCCESS,
|
||||
...tokens,
|
||||
};
|
||||
}
|
||||
|
||||
//****************** */
|
||||
|
||||
async changePassword(userId: string, updateDto: ChangePasswordDto) {
|
||||
const { user } = await this.usersService.findOneById(userId);
|
||||
|
||||
const passCompare = await this.passwordService.comparePassword(updateDto.password, user.password);
|
||||
if (!passCompare) throw new BadRequestException(AuthMessage.CURRENT_PASSWORD_INVALID);
|
||||
|
||||
if (updateDto.newPassword !== updateDto.repeatPassword) throw new BadRequestException(AuthMessage.INVALID_REPEAT_PASSWORD);
|
||||
|
||||
const hashedPassword = await this.passwordService.hashPassword(updateDto.newPassword);
|
||||
await this.usersService.updateUserPassword(user, hashedPassword);
|
||||
|
||||
return {
|
||||
message: AuthMessage.PASSWORD_CHANGED_SUCCESSFULLY,
|
||||
};
|
||||
}
|
||||
//****************** */
|
||||
|
||||
async refreshToken(oldRefreshToken: string) {
|
||||
return this.tokensService.refreshToken(oldRefreshToken);
|
||||
}
|
||||
//****************** */
|
||||
|
||||
async logout(userId: string) {
|
||||
await this.tokensService.invalidateRefreshToken(userId);
|
||||
return {
|
||||
message: AuthMessage.LOGOUT_SUCCESS,
|
||||
};
|
||||
}
|
||||
|
||||
//****************** */
|
||||
|
||||
private async checkUserLoginCredentialWithEmail(email: string, password: string) {
|
||||
const user = await this.usersService.findOneWithEmail(email);
|
||||
if (!user) throw new BadRequestException(AuthMessage.INVALID_PASSWORD);
|
||||
|
||||
const passCompare = await this.passwordService.comparePassword(password, user.password);
|
||||
if (!passCompare) throw new BadRequestException(AuthMessage.INVALID_PASSWORD);
|
||||
|
||||
return user;
|
||||
}
|
||||
//****************** */
|
||||
//****************** */
|
||||
|
||||
private async checkUserLoginCredentialWithPhone(phone: string, otpCode: string) {
|
||||
const isValid = await this.otpService.verifyOtp(phone, otpCode, "LOGIN");
|
||||
if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP);
|
||||
|
||||
await this.otpService.delOtpFormCache(phone, "LOGIN");
|
||||
|
||||
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(role: Role) {
|
||||
return role.name === RoleEnum.ADMIN;
|
||||
}
|
||||
}
|
||||
Executable
+143
@@ -0,0 +1,143 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { BadRequestException, Injectable, Logger, UnauthorizedException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { AuthMessage, UserMessage } from "../../../common/enums/message.enum";
|
||||
import { RefreshToken } from "../../users/entities/refresh-token.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { ITokenPayload } from "../interfaces/IToken-payload";
|
||||
|
||||
@Injectable()
|
||||
export class TokensService {
|
||||
private readonly logger = new Logger(TokensService.name);
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
|
||||
async generateTokens(user: User, em?: EntityManager) {
|
||||
return this.generateAccessAndRefreshToken(
|
||||
{
|
||||
id: user.id,
|
||||
role: user.role.name,
|
||||
},
|
||||
em,
|
||||
);
|
||||
}
|
||||
|
||||
private async generateAccessAndRefreshToken(payload: ITokenPayload, em?: EntityManager) {
|
||||
const accessExpire = this.configService.getOrThrow<number>("ACCESS_TOKEN_EXPIRE");
|
||||
const refreshExpire = this.configService.getOrThrow<number>("REFRESH_TOKEN_EXPIRE");
|
||||
|
||||
const accessToken = await this.jwtService.signAsync(payload, { expiresIn: `${accessExpire}m` });
|
||||
const refreshToken = await this.generateRefreshToken();
|
||||
|
||||
await this.storeRefreshToken(payload.id, refreshToken, em);
|
||||
|
||||
return {
|
||||
accessToken: { token: accessToken, expire: dayjs().add(accessExpire, "minute").valueOf() },
|
||||
refreshToken: { token: refreshToken, expire: dayjs().add(refreshExpire, "day").valueOf() },
|
||||
};
|
||||
}
|
||||
|
||||
async storeRefreshToken(userId: string, refreshToken: string, em?: EntityManager) {
|
||||
const entityManager = em || this.em.fork();
|
||||
let transactionStarted = false;
|
||||
|
||||
try {
|
||||
if (!entityManager.isInTransaction()) {
|
||||
await entityManager.begin();
|
||||
transactionStarted = true;
|
||||
}
|
||||
|
||||
const refreshExpire = this.configService.getOrThrow<number>("REFRESH_TOKEN_EXPIRE");
|
||||
const expiresAt = dayjs().add(refreshExpire, "day").toDate();
|
||||
|
||||
const user = await entityManager.findOne(User, { id: userId });
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
|
||||
const token = entityManager.create(RefreshToken, {
|
||||
token: refreshToken,
|
||||
user,
|
||||
expiresAt,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
await entityManager.persistAndFlush(token);
|
||||
|
||||
if (transactionStarted) await entityManager.commit();
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
if (transactionStarted) await entityManager.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async refreshToken(oldRefreshToken: string) {
|
||||
const entityManager = this.em.fork();
|
||||
let transactionStarted = false;
|
||||
|
||||
try {
|
||||
await entityManager.begin();
|
||||
transactionStarted = true;
|
||||
|
||||
const token = await entityManager.findOne(
|
||||
RefreshToken,
|
||||
{ token: oldRefreshToken },
|
||||
{
|
||||
populate: ["user", "user.role"],
|
||||
},
|
||||
);
|
||||
|
||||
if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
|
||||
|
||||
if (dayjs(token.expiresAt).isBefore(dayjs())) {
|
||||
await entityManager.removeAndFlush(token);
|
||||
throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
|
||||
}
|
||||
|
||||
await entityManager.removeAndFlush(token);
|
||||
const tokens = await this.generateTokens(token.user, entityManager);
|
||||
|
||||
await entityManager.commit();
|
||||
return tokens;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
if (transactionStarted) {
|
||||
await entityManager.rollback();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async invalidateRefreshToken(userId: string) {
|
||||
const entityManager = this.em.fork();
|
||||
let transactionStarted = false;
|
||||
|
||||
try {
|
||||
await entityManager.begin();
|
||||
transactionStarted = true;
|
||||
|
||||
await entityManager.nativeDelete(RefreshToken, { user: { id: userId } });
|
||||
this.logger.log(`Successfully deleted all refresh tokens for user ${userId}`);
|
||||
|
||||
await entityManager.commit();
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
if (transactionStarted) {
|
||||
await entityManager.rollback();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async generateRefreshToken() {
|
||||
return randomBytes(32).toString("hex");
|
||||
}
|
||||
}
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { ExtractJwt, Strategy } from "passport-jwt";
|
||||
|
||||
import { JWT_STRATEGY_NAME } from "../../../common/constants";
|
||||
import { ITokenPayload } from "../interfaces/IToken-payload";
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy, JWT_STRATEGY_NAME) {
|
||||
constructor(configService: ConfigService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: configService.getOrThrow<string>("JWT_SECRET"),
|
||||
issuer: configService.getOrThrow<string>("JWT_ISSUER"),
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: ITokenPayload) {
|
||||
return { id: payload.id, role: payload.role };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { IsEnum, IsNotEmpty, IsString, IsUUID } from "class-validator";
|
||||
|
||||
import { NotificationMessage } from "../../../common/enums/message.enum";
|
||||
import { NotifType } from "../enums/notif-settings.enum";
|
||||
|
||||
export class CreateNotificationDto {
|
||||
@IsNotEmpty({ message: NotificationMessage.TITLE_IS_REQUIRED })
|
||||
@IsString({ message: NotificationMessage.TITLE_STRING })
|
||||
title: string;
|
||||
|
||||
@IsNotEmpty({ message: NotificationMessage.MESSAGE_IS_REQUIRED })
|
||||
@IsString({ message: NotificationMessage.MESSAGE_STRING })
|
||||
message: string;
|
||||
|
||||
@IsNotEmpty({ message: NotificationMessage.USERID_IS_REQUIRED })
|
||||
@IsString({ message: NotificationMessage.USERID_IS_STRING })
|
||||
@IsUUID("7", { message: NotificationMessage.USERID_IS_UUID })
|
||||
recipientId: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsEnum(NotifType)
|
||||
type: NotifType;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsIn, IsOptional } from "class-validator";
|
||||
|
||||
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
||||
|
||||
export class SearchNotificationQueryDto extends PaginationDto {
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsIn([0, 1])
|
||||
@ApiPropertyOptional({ description: "Notification status", example: 1 })
|
||||
isRead?: number;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export const NOTIFICATION = Object.freeze({
|
||||
QUEUE_NAME: "notification",
|
||||
SEND_NOTIFICATION_JOB_NAME: "sendNotification",
|
||||
|
||||
SEND_NOTIFICATION_JOB_DELAY: 1000, // delay after 1 second
|
||||
SEND_NOTIFICATION_JOB_PRIORITY: 1, // high priority
|
||||
SEND_NOTIFICATION_JOB_ATTEMPTS: 3, // retry 3 times
|
||||
SEND_NOTIFICATION_JOB_BACKOFF: 5 * 1000, // retry after 5 seconds
|
||||
SEND_NOTIFICATION_JOB_TIMEOUT: 10000, // timeout after 10 seconds
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Entity, EntityRepositoryType, Enum, ManyToOne, Opt, Property } from "@mikro-orm/core";
|
||||
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { NotifType } from "../enums/notif-settings.enum";
|
||||
import { NotificationRepository } from "../repositories/notifications.repository";
|
||||
|
||||
@Entity({ repository: () => NotificationRepository })
|
||||
export class Notification extends BaseEntity {
|
||||
@Property({ type: "varchar", length: 250, nullable: false })
|
||||
title!: string;
|
||||
|
||||
@Property({ type: "text", nullable: false })
|
||||
message!: string;
|
||||
|
||||
@Enum({ items: () => NotifType, nativeEnumName: "notif_type", nullable: false })
|
||||
type: NotifType;
|
||||
|
||||
@Property({ type: "boolean", default: false })
|
||||
isRead: boolean & Opt;
|
||||
|
||||
@ManyToOne(() => User, { deleteRule: "cascade" })
|
||||
recipient!: User;
|
||||
|
||||
[EntityRepositoryType]?: NotificationRepository;
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
export enum NotifType {
|
||||
// Account category
|
||||
OTP = "OTP",
|
||||
USER_LOGIN = "USER_LOGIN",
|
||||
ANNOUNCEMENT = "ANNOUNCEMENT",
|
||||
|
||||
// Finance category
|
||||
WALLET_CHARGE = "WALLET_CHARGE",
|
||||
WALLET_DEDUCTION = "WALLET_DEDUCTION",
|
||||
|
||||
//Invoice
|
||||
BILL_INVOICE_REMINDER = "BILL_INVOICE_REMINDER",
|
||||
BILL_INVOICE = "BILL_INVOICE",
|
||||
APPROVE_INVOICE = "APPROVE_INVOICE",
|
||||
CREATE_INVOICE = "CREATE_INVOICE",
|
||||
INVOICE_OVERDUE = "INVOICE_OVERDUE",
|
||||
|
||||
// Service category
|
||||
CREATE_SERVICE = "CREATE_SERVICE",
|
||||
UNBLOCK_SERVICE = "UNBLOCK_SERVICE",
|
||||
BLOCK_SERVICE = "BLOCK_SERVICE",
|
||||
|
||||
// Support category
|
||||
ANSWER_TICKET = "ANSWER_TICKET",
|
||||
CREATE_TICKET = "CREATE_TICKET",
|
||||
//
|
||||
ASSIGN_TICKET = "ASSIGN_TICKET",
|
||||
RECURRING_INVOICE = "RECURRING_INVOICE",
|
||||
PAYMENT_REMINDER = "PAYMENT_REMINDER",
|
||||
PAYMENT_CANCELLATION = "PAYMENT_CANCELLATION",
|
||||
|
||||
// // Admin category
|
||||
|
||||
NEW_CUSTOMER = "NEW_CUSTOMER",
|
||||
NEW_TICKET = "NEW_TICKET",
|
||||
NEW_CRITICISM = "NEW_CRITICISM",
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
export interface IBaseNotificationData {
|
||||
userPhone: string;
|
||||
userEmail?: string;
|
||||
}
|
||||
|
||||
export interface IOtpNotificationData extends IBaseNotificationData {
|
||||
otp: string;
|
||||
}
|
||||
|
||||
export interface IInvoiceNotificationData extends IBaseNotificationData {
|
||||
price: number;
|
||||
items: string;
|
||||
dueDate: Date;
|
||||
createDate: Date;
|
||||
invoiceId: string;
|
||||
paidAt?: Date;
|
||||
lateFee?: number;
|
||||
}
|
||||
|
||||
export interface ITicketNotificationData extends IBaseNotificationData {
|
||||
ticketId: string;
|
||||
subject: string;
|
||||
date: Date;
|
||||
user?: string;
|
||||
}
|
||||
|
||||
export interface IGenericNotificationData extends IBaseNotificationData {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface IAnnouncementNotificationData extends IBaseNotificationData {
|
||||
title: string;
|
||||
description: string;
|
||||
date: Date;
|
||||
}
|
||||
|
||||
export interface IPaymentNotificationData extends IBaseNotificationData {
|
||||
amount: number;
|
||||
paymentId: string;
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { Controller, Get, Param, Patch, Query } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
|
||||
import { SearchNotificationQueryDto } from "./DTO/search-notification-query.dto";
|
||||
import { NotificationsService } from "./providers/notifications.service";
|
||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||
import { Pagination } from "../../common/decorators/pagination.decorator";
|
||||
import { UserDec } from "../../common/decorators/user.decorator";
|
||||
import { ParamDto } from "../../common/DTO/param.dto";
|
||||
|
||||
@Controller("notifications")
|
||||
@ApiTags("Notifications")
|
||||
@AuthGuards()
|
||||
export class NotificationController {
|
||||
constructor(private readonly notificationService: NotificationsService) {}
|
||||
|
||||
//********************* */
|
||||
|
||||
@ApiOperation({ summary: "all notifications by user" })
|
||||
@Pagination()
|
||||
@Get()
|
||||
getAllNotifications(@UserDec("id") userId: string, @Query() queryDto: SearchNotificationQueryDto) {
|
||||
return this.notificationService.getAllNotifications(queryDto, userId);
|
||||
}
|
||||
|
||||
//********************* */
|
||||
|
||||
@ApiOperation({ summary: "mark user notification as read" })
|
||||
@Patch(":id/read")
|
||||
markAsRead(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
|
||||
return this.notificationService.markAsRead(paramDto.id, userId);
|
||||
}
|
||||
|
||||
//********************* */
|
||||
|
||||
@ApiOperation({ summary: "mark user all notification as read" })
|
||||
@Patch("read-all")
|
||||
markAllAsRead(@UserDec("id") userId: string) {
|
||||
return this.notificationService.markAllAsRead(userId);
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { MikroOrmModule } from "@mikro-orm/nestjs";
|
||||
import { BullModule } from "@nestjs/bullmq";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
|
||||
import { NOTIFICATION } from "./constants";
|
||||
import { UtilsModule } from "../utils/utils.module";
|
||||
import { Notification } from "./entities/notification.entity";
|
||||
import { NotificationController } from "./notifications.controller";
|
||||
import { NotificationsService } from "./providers/notifications.service";
|
||||
import { NotificationProcessor } from "./queue/notification.processor";
|
||||
import { NotificationQueue } from "./queue/notification.queue";
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule,
|
||||
MikroOrmModule.forFeature([Notification]),
|
||||
BullModule.registerQueue({
|
||||
name: NOTIFICATION.QUEUE_NAME,
|
||||
defaultJobOptions: {
|
||||
attempts: NOTIFICATION.SEND_NOTIFICATION_JOB_ATTEMPTS,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: NOTIFICATION.SEND_NOTIFICATION_JOB_BACKOFF,
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: false,
|
||||
delay: NOTIFICATION.SEND_NOTIFICATION_JOB_DELAY,
|
||||
},
|
||||
}),
|
||||
UtilsModule,
|
||||
],
|
||||
providers: [NotificationsService, NotificationQueue, NotificationProcessor],
|
||||
controllers: [NotificationController],
|
||||
exports: [NotificationsService, NotificationQueue],
|
||||
})
|
||||
export class NotificationModule {}
|
||||
@@ -0,0 +1,299 @@
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { CommonMessage, NotificationMessage } from "../../../common/enums/message.enum";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { EmailService } from "../../utils/providers/email.service";
|
||||
import { dateFormat, numberFormat } from "../../utils/providers/international.utils";
|
||||
import { SmsService } from "../../utils/providers/sms.service";
|
||||
import { CreateNotificationDto } from "../DTO/create-notification.dto";
|
||||
import { SearchNotificationQueryDto } from "../DTO/search-notification-query.dto";
|
||||
import { Notification } from "../entities/notification.entity";
|
||||
import { NotifType } from "../enums/notif-settings.enum";
|
||||
import {
|
||||
IAnnouncementNotificationData,
|
||||
IBaseNotificationData,
|
||||
IInvoiceNotificationData,
|
||||
IOtpNotificationData,
|
||||
IPaymentNotificationData,
|
||||
ITicketNotificationData,
|
||||
} from "../interfaces/ISendNotificationData";
|
||||
import { NotificationRepository } from "../repositories/notifications.repository";
|
||||
|
||||
@Injectable()
|
||||
export class NotificationsService {
|
||||
private readonly logger = new Logger(NotificationsService.name);
|
||||
constructor(
|
||||
private readonly notificationRepository: NotificationRepository,
|
||||
private readonly smsService: SmsService,
|
||||
private readonly emailService: EmailService,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
|
||||
//************************ */
|
||||
|
||||
async createNotification(createNotificationDto: CreateNotificationDto, em?: EntityManager) {
|
||||
if (!em) {
|
||||
const localEm = this.em.fork();
|
||||
|
||||
try {
|
||||
await localEm.begin();
|
||||
//
|
||||
const notification = localEm.create(Notification, {
|
||||
...createNotificationDto,
|
||||
recipient: localEm.getReference(User, createNotificationDto.recipientId),
|
||||
});
|
||||
await localEm.persistAndFlush(notification);
|
||||
|
||||
await localEm.commit();
|
||||
|
||||
return notification;
|
||||
//
|
||||
} catch (error) {
|
||||
this.logger.error("error in createNotification", error);
|
||||
await localEm.rollback();
|
||||
throw error;
|
||||
//
|
||||
}
|
||||
//
|
||||
} else {
|
||||
//
|
||||
const notification = em.create(Notification, {
|
||||
...createNotificationDto,
|
||||
recipient: em.getReference(User, createNotificationDto.recipientId),
|
||||
});
|
||||
//
|
||||
await em.persistAndFlush(notification);
|
||||
return notification;
|
||||
}
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async markAsRead(notificationId: string, userId: string) {
|
||||
const notification = await this.notificationRepository.findOne({ id: notificationId, recipient: { id: userId } });
|
||||
|
||||
if (!notification) throw new NotFoundException(NotificationMessage.NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED);
|
||||
|
||||
notification.isRead = true;
|
||||
await this.em.flush();
|
||||
return notification;
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async getAllNotifications(queryDto: SearchNotificationQueryDto, recipientId: string) {
|
||||
return await this.notificationRepository.getAllNotifications(queryDto, recipientId);
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async getNotificationById(id: string) {
|
||||
const notification = await this.notificationRepository.findOne({ id });
|
||||
if (!notification) throw new BadRequestException(NotificationMessage.NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED);
|
||||
return { notification };
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async countUserNotifications(userId: string) {
|
||||
const notificationCount = await this.notificationRepository.count({ recipient: { id: userId }, isRead: false });
|
||||
return notificationCount;
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async markAllAsRead(userId: string) {
|
||||
await this.em.nativeUpdate(Notification, { recipient: { id: userId } }, { isRead: true });
|
||||
|
||||
return {
|
||||
message: CommonMessage.UPDATE_SUCCESS,
|
||||
};
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async createOtpNotification(data: IOtpNotificationData) {
|
||||
//sent notification based on the userSettings
|
||||
return await this.smsService.sendSmsVerifyCode(data.userPhone, data.otp);
|
||||
}
|
||||
|
||||
async createLoginNotification(recipientId: string, data: IBaseNotificationData) {
|
||||
const loginDate = new Date().toLocaleString("fa-IR");
|
||||
const message = NotificationMessage.LOGIN_MESSAGE.replace("[loginDate]", loginDate);
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendLoginSms(data.userPhone, loginDate);
|
||||
// if (data.userEmail) await this.emailService.sendLoginEmail(data);
|
||||
|
||||
return this.createNotification({ title: NotificationMessage.LOGIN, type: NotifType.USER_LOGIN, message, recipientId });
|
||||
}
|
||||
|
||||
//************************ */
|
||||
async createInvoiceCreationNotification(recipientId: string, data: IInvoiceNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.INVOICE_CREATION_MESSAGE.replace("[id]", data.invoiceId);
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendInvoiceCreationSms(
|
||||
data.userPhone,
|
||||
data.invoiceId,
|
||||
data.price,
|
||||
data.items,
|
||||
dateFormat(data.createDate),
|
||||
dateFormat(data.dueDate),
|
||||
);
|
||||
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
||||
|
||||
return this.createNotification(
|
||||
{ title: NotificationMessage.INVOICE_CREATION, type: NotifType.CREATE_INVOICE, message, recipientId },
|
||||
em,
|
||||
);
|
||||
}
|
||||
//************************ */
|
||||
|
||||
async createAnnouncementNotification(recipientId: string, data: IAnnouncementNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.ANNOUNCEMENT_MESSAGE;
|
||||
//sent notification based on the userSettings
|
||||
|
||||
await this.smsService.sendAnnouncementSms(data.userPhone, data.title, data.description, dateFormat(data.date));
|
||||
if (data.userEmail) await this.emailService.sendAnnouncementEmail(data);
|
||||
|
||||
return this.createNotification({ title: NotificationMessage.ANNOUNCEMENT, type: NotifType.ANNOUNCEMENT, message, recipientId }, em);
|
||||
}
|
||||
|
||||
// //************************ */
|
||||
|
||||
async createAnswerTicketNotification(recipientId: string, data: ITicketNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.ANSWER_TICKET_MESSAGE.replace("[ticketId]", data.ticketId);
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendAnswerTicketSms(data.userPhone, data.ticketId, data.subject);
|
||||
if (data.userEmail) await this.emailService.sendTicketEmail(data);
|
||||
|
||||
return this.createNotification({ title: NotificationMessage.ANSWER_TICKET, type: NotifType.ANSWER_TICKET, message, recipientId }, em);
|
||||
}
|
||||
|
||||
// //************************ */
|
||||
|
||||
async createTicketNotification(recipientId: string, data: ITicketNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.CREATE_TICKET_MESSAGE.replace("[ticketId]", data.ticketId);
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendCreateTicketSms(data.userPhone, data.ticketId, data.subject, dateFormat(data.date));
|
||||
if (data.userEmail) await this.emailService.sendTicketEmail(data);
|
||||
|
||||
return this.createNotification({ title: NotificationMessage.CREATE_TICKET, type: NotifType.CREATE_TICKET, message, recipientId }, em);
|
||||
}
|
||||
//************************ */
|
||||
async createAssignTicketNotificationForAdmin(recipientId: string, data: ITicketNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.ASSIGN_TICKET_MESSAGE.replace("[ticketId]", data.ticketId);
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendTicketAssignedToAdminSms(data.userPhone, data.ticketId, data.subject, data.user!);
|
||||
if (data.userEmail) await this.emailService.sendTicketEmail(data);
|
||||
|
||||
return this.createNotification({ title: NotificationMessage.ASSIGN_TICKET, type: NotifType.ASSIGN_TICKET, message, recipientId }, em);
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async createBillInvoiceReminderNotification(recipientId: string, data: IInvoiceNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.BILL_INVOICE_REMINDER_MESSAGE.replace("[invoiceNumber]", data.invoiceId);
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendInvoicePaymentReminderSms(data.userPhone, data.invoiceId, data.price, dateFormat(data.dueDate));
|
||||
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
||||
|
||||
return this.createNotification(
|
||||
{ title: NotificationMessage.BILL_INVOICE_REMINDER, type: NotifType.BILL_INVOICE_REMINDER, message, recipientId },
|
||||
em,
|
||||
);
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async createBillInvoiceNotification(recipientId: string, data: IInvoiceNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.BILL_INVOICE_MESSAGE.replace("[invoiceNumber]", data.invoiceId);
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendInvoicePaidSms(data.userPhone, data.invoiceId, data.price, dateFormat(data.paidAt!));
|
||||
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
||||
|
||||
return this.createNotification({ title: NotificationMessage.BILL_INVOICE, type: NotifType.BILL_INVOICE, message, recipientId }, em);
|
||||
}
|
||||
//************************ */
|
||||
|
||||
async createApprovedInvoiceNotification(recipientId: string, data: IInvoiceNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.APPROVED_INVOICE_MESSAGE.replace("[invoiceNumber]", data.invoiceId);
|
||||
//sent notification based on the userSettings
|
||||
|
||||
await this.smsService.sendInvoiceApprovedSms(data.userPhone, data.invoiceId, data.price, dateFormat(data.dueDate));
|
||||
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
||||
|
||||
return this.createNotification({ title: NotificationMessage.APPROVED_INVOICE, type: NotifType.BILL_INVOICE, message, recipientId }, em);
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async createInvoiceOverdueNotification(recipientId: string, data: IInvoiceNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.INVOICE_OVERDUE_MESSAGE.replace("[invoiceNumber]", data.invoiceId).replace(
|
||||
"[lateFee]",
|
||||
numberFormat(data.lateFee || 0),
|
||||
);
|
||||
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendInvoiceOverdueSms(data.userPhone, data.invoiceId, data.price, data.lateFee || 0, dateFormat(data.dueDate));
|
||||
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
||||
|
||||
return this.createNotification(
|
||||
{ title: NotificationMessage.INVOICE_OVERDUE, type: NotifType.INVOICE_OVERDUE, message, recipientId },
|
||||
em,
|
||||
);
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async createRecurringInvoiceNotification(recipientId: string, data: IInvoiceNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.RECURRING_INVOICE_MESSAGE.replace("[invoiceNumber]", data.invoiceId);
|
||||
//sent notification based on the userSettings
|
||||
|
||||
await this.smsService.sendRecurringInvoiceDraftSms(data.userPhone, data.invoiceId, data.price);
|
||||
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
||||
return this.createNotification(
|
||||
{ title: NotificationMessage.RECURRING_INVOICE, type: NotifType.RECURRING_INVOICE, message, recipientId },
|
||||
em,
|
||||
);
|
||||
}
|
||||
|
||||
// //************************ */
|
||||
|
||||
async createPaymentReminderNotification(recipientId: string, data: IPaymentNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.PAYMENT_REMINDER_MESSAGE.replace("[amount]", numberFormat(data.amount));
|
||||
|
||||
await this.smsService.sendPaymentReminderSms(data.userPhone, numberFormat(data.amount));
|
||||
if (data.userEmail) await this.emailService.sendPaymentReminderEmail(data);
|
||||
|
||||
return this.createNotification(
|
||||
{
|
||||
title: NotificationMessage.PAYMENT_REMINDER,
|
||||
type: NotifType.PAYMENT_REMINDER,
|
||||
message,
|
||||
recipientId,
|
||||
},
|
||||
em,
|
||||
);
|
||||
}
|
||||
// //************************ */
|
||||
|
||||
async createPaymentCancellationNotification(recipientId: string, data: IPaymentNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.PAYMENT_CANCELLATION_MESSAGE.replace("[amount]", numberFormat(data.amount));
|
||||
|
||||
await this.smsService.sendPaymentCancellationSms(data.userPhone, numberFormat(data.amount));
|
||||
if (data.userEmail) await this.emailService.sendPaymentCancellationEmail(data);
|
||||
|
||||
return this.createNotification(
|
||||
{
|
||||
title: NotificationMessage.PAYMENT_CANCELLATION,
|
||||
type: NotifType.PAYMENT_CANCELLATION,
|
||||
message,
|
||||
recipientId,
|
||||
},
|
||||
em,
|
||||
);
|
||||
}
|
||||
|
||||
//--------------------------------------
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { Processor } from "@nestjs/bullmq";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { Job } from "bullmq";
|
||||
|
||||
import { WorkerProcessor } from "../../../common/queues/worker.processor";
|
||||
import { NOTIFICATION } from "../constants";
|
||||
import { NotifType } from "../enums/notif-settings.enum";
|
||||
import {
|
||||
IAnnouncementNotificationData,
|
||||
IInvoiceNotificationData,
|
||||
IOtpNotificationData,
|
||||
IPaymentNotificationData,
|
||||
ITicketNotificationData,
|
||||
} from "../interfaces/ISendNotificationData";
|
||||
import { NotificationsService } from "../providers/notifications.service";
|
||||
|
||||
type NotificationJobData = {
|
||||
type: NotifType;
|
||||
recipientId: string;
|
||||
data:
|
||||
| ITicketNotificationData
|
||||
| IInvoiceNotificationData
|
||||
| IAnnouncementNotificationData
|
||||
| IPaymentNotificationData
|
||||
| IOtpNotificationData;
|
||||
};
|
||||
|
||||
@Processor(NOTIFICATION.QUEUE_NAME, { concurrency: 5 })
|
||||
export class NotificationProcessor extends WorkerProcessor {
|
||||
protected readonly logger = new Logger(NotificationProcessor.name);
|
||||
|
||||
constructor(
|
||||
private readonly notificationsService: NotificationsService,
|
||||
private readonly em: EntityManager,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async process(job: Job<NotificationJobData>, token?: string) {
|
||||
this.logger.log(`Processing notification job: ${job.id} ${token ? `with token: ${token}` : ""}`);
|
||||
|
||||
const entityManager = this.em.fork();
|
||||
|
||||
try {
|
||||
await entityManager.begin();
|
||||
|
||||
const { type, recipientId, data } = job.data;
|
||||
|
||||
switch (type) {
|
||||
// User Notifications
|
||||
case NotifType.OTP:
|
||||
await this.notificationsService.createOtpNotification(data as IOtpNotificationData);
|
||||
break;
|
||||
|
||||
case NotifType.USER_LOGIN:
|
||||
await this.notificationsService.createLoginNotification(recipientId, data);
|
||||
break;
|
||||
case NotifType.ANNOUNCEMENT:
|
||||
await this.notificationsService.createAnnouncementNotification(recipientId, data as IAnnouncementNotificationData, entityManager);
|
||||
break;
|
||||
|
||||
// Ticket Notifications
|
||||
case NotifType.ANSWER_TICKET:
|
||||
await this.notificationsService.createAnswerTicketNotification(recipientId, data as ITicketNotificationData, entityManager);
|
||||
break;
|
||||
case NotifType.CREATE_TICKET:
|
||||
await this.notificationsService.createTicketNotification(recipientId, data as ITicketNotificationData, entityManager);
|
||||
break;
|
||||
case NotifType.ASSIGN_TICKET:
|
||||
await this.notificationsService.createAssignTicketNotificationForAdmin(
|
||||
recipientId,
|
||||
data as ITicketNotificationData,
|
||||
entityManager,
|
||||
);
|
||||
break;
|
||||
|
||||
// Invoice Notifications
|
||||
case NotifType.CREATE_INVOICE:
|
||||
await this.notificationsService.createInvoiceCreationNotification(recipientId, data as IInvoiceNotificationData, entityManager);
|
||||
break;
|
||||
case NotifType.BILL_INVOICE_REMINDER:
|
||||
await this.notificationsService.createBillInvoiceReminderNotification(
|
||||
recipientId,
|
||||
data as IInvoiceNotificationData,
|
||||
entityManager,
|
||||
);
|
||||
break;
|
||||
case NotifType.BILL_INVOICE:
|
||||
await this.notificationsService.createBillInvoiceNotification(recipientId, data as IInvoiceNotificationData, entityManager);
|
||||
break;
|
||||
case NotifType.APPROVE_INVOICE:
|
||||
await this.notificationsService.createApprovedInvoiceNotification(recipientId, data as IInvoiceNotificationData, entityManager);
|
||||
break;
|
||||
case NotifType.INVOICE_OVERDUE:
|
||||
await this.notificationsService.createInvoiceOverdueNotification(recipientId, data as IInvoiceNotificationData, entityManager);
|
||||
break;
|
||||
case NotifType.RECURRING_INVOICE:
|
||||
await this.notificationsService.createRecurringInvoiceNotification(recipientId, data as IInvoiceNotificationData, entityManager);
|
||||
break;
|
||||
|
||||
// Payment Notifications
|
||||
case NotifType.PAYMENT_REMINDER:
|
||||
await this.notificationsService.createPaymentReminderNotification(recipientId, data as IPaymentNotificationData, entityManager);
|
||||
break;
|
||||
case NotifType.PAYMENT_CANCELLATION:
|
||||
await this.notificationsService.createPaymentCancellationNotification(
|
||||
recipientId,
|
||||
data as IPaymentNotificationData,
|
||||
entityManager,
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.warn(`Unknown notification type: ${type}`);
|
||||
}
|
||||
|
||||
await entityManager.commit();
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to process notification: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
);
|
||||
await entityManager.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { InjectQueue } from "@nestjs/bullmq";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Queue } from "bullmq";
|
||||
|
||||
import { NOTIFICATION } from "../constants";
|
||||
import { NotifType } from "../enums/notif-settings.enum";
|
||||
import {
|
||||
IAnnouncementNotificationData,
|
||||
IBaseNotificationData,
|
||||
IInvoiceNotificationData,
|
||||
IOtpNotificationData,
|
||||
IPaymentNotificationData,
|
||||
ITicketNotificationData,
|
||||
} from "../interfaces/ISendNotificationData";
|
||||
|
||||
@Injectable()
|
||||
export class NotificationQueue {
|
||||
constructor(@InjectQueue(NOTIFICATION.QUEUE_NAME) private readonly notificationsQueue: Queue) {}
|
||||
|
||||
async addNotificationJob(type: NotifType, recipientId: string, data: IBaseNotificationData) {
|
||||
await this.notificationsQueue.add(
|
||||
NOTIFICATION.SEND_NOTIFICATION_JOB_NAME,
|
||||
{
|
||||
type,
|
||||
recipientId,
|
||||
data,
|
||||
},
|
||||
{
|
||||
delay: NOTIFICATION.SEND_NOTIFICATION_JOB_DELAY,
|
||||
attempts: NOTIFICATION.SEND_NOTIFICATION_JOB_ATTEMPTS,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: NOTIFICATION.SEND_NOTIFICATION_JOB_BACKOFF,
|
||||
},
|
||||
priority: NOTIFICATION.SEND_NOTIFICATION_JOB_PRIORITY,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async addOtpNotification(recipientId: string, data: IOtpNotificationData) {
|
||||
await this.addNotificationJob(NotifType.OTP, recipientId, data);
|
||||
}
|
||||
|
||||
// User notification methods
|
||||
async addLoginNotification(recipientId: string, data: IBaseNotificationData) {
|
||||
await this.addNotificationJob(NotifType.USER_LOGIN, recipientId, data);
|
||||
}
|
||||
|
||||
async addInvoiceCreationNotification(recipientId: string, data: IInvoiceNotificationData) {
|
||||
await this.addNotificationJob(NotifType.CREATE_INVOICE, recipientId, data);
|
||||
}
|
||||
|
||||
async addAnnouncementNotification(recipientId: string, data: IAnnouncementNotificationData) {
|
||||
await this.addNotificationJob(NotifType.ANNOUNCEMENT, recipientId, data);
|
||||
}
|
||||
|
||||
async addAnswerTicketNotification(recipientId: string, data: ITicketNotificationData) {
|
||||
await this.addNotificationJob(NotifType.ANSWER_TICKET, recipientId, data);
|
||||
}
|
||||
|
||||
async addTicketNotification(recipientId: string, data: ITicketNotificationData) {
|
||||
await this.addNotificationJob(NotifType.CREATE_TICKET, recipientId, data);
|
||||
}
|
||||
|
||||
async addAssignTicketNotificationForAdmin(recipientId: string, data: ITicketNotificationData) {
|
||||
await this.addNotificationJob(NotifType.ASSIGN_TICKET, recipientId, data);
|
||||
}
|
||||
|
||||
async addBillInvoiceReminderNotification(recipientId: string, data: IInvoiceNotificationData) {
|
||||
await this.addNotificationJob(NotifType.BILL_INVOICE_REMINDER, recipientId, data);
|
||||
}
|
||||
|
||||
async addBillInvoiceNotification(recipientId: string, data: IInvoiceNotificationData) {
|
||||
await this.addNotificationJob(NotifType.BILL_INVOICE, recipientId, data);
|
||||
}
|
||||
|
||||
async addApprovedInvoiceNotification(recipientId: string, data: IInvoiceNotificationData) {
|
||||
await this.addNotificationJob(NotifType.APPROVE_INVOICE, recipientId, data);
|
||||
}
|
||||
|
||||
async addInvoiceOverdueNotification(recipientId: string, data: IInvoiceNotificationData) {
|
||||
await this.addNotificationJob(NotifType.INVOICE_OVERDUE, recipientId, data);
|
||||
}
|
||||
|
||||
async addRecurringInvoiceNotification(recipientId: string, data: IInvoiceNotificationData) {
|
||||
await this.addNotificationJob(NotifType.RECURRING_INVOICE, recipientId, data);
|
||||
}
|
||||
|
||||
async addPaymentReminderNotification(recipientId: string, data: IPaymentNotificationData) {
|
||||
await this.addNotificationJob(NotifType.PAYMENT_REMINDER, recipientId, data);
|
||||
}
|
||||
|
||||
async addPaymentCancellationNotification(recipientId: string, data: IPaymentNotificationData) {
|
||||
await this.addNotificationJob(NotifType.PAYMENT_CANCELLATION, recipientId, data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { EntityRepository } from "@mikro-orm/postgresql";
|
||||
|
||||
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
||||
import { SearchNotificationQueryDto } from "../DTO/search-notification-query.dto";
|
||||
import { Notification } from "../entities/notification.entity";
|
||||
|
||||
export class NotificationRepository extends EntityRepository<Notification> {
|
||||
async getAllNotifications(queryDto: SearchNotificationQueryDto, recipientId: string) {
|
||||
const { skip, limit } = PaginationUtils(queryDto);
|
||||
|
||||
const qb = this.qb("notification").where({ recipientId });
|
||||
|
||||
if (queryDto.isRead !== undefined) {
|
||||
qb.andWhere({ isRead: queryDto.isRead === 1 });
|
||||
}
|
||||
|
||||
return qb.orderBy({ createdAt: "DESC" }).offset(skip).limit(limit).getResultAndCount();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Entity, ManyToOne, Property } from "@mikro-orm/core";
|
||||
|
||||
import { User } from "./user.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
|
||||
@Entity()
|
||||
export class RefreshToken extends BaseEntity {
|
||||
@Property({ type: "varchar", length: 255 })
|
||||
token!: string;
|
||||
|
||||
@ManyToOne(() => User, { deleteRule: "restrict" })
|
||||
user!: User;
|
||||
|
||||
@Property({ type: "timestamptz", nullable: true })
|
||||
expiresAt?: Date;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Collection, Entity, Enum, OneToMany } from "@mikro-orm/core";
|
||||
|
||||
import { User } from "./user.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { RoleEnum } from "../enums/role.enum";
|
||||
|
||||
@Entity()
|
||||
export class Role extends BaseEntity {
|
||||
@Enum({ items: () => RoleEnum, nativeEnumName: "role_enum", default: RoleEnum.USER })
|
||||
name!: RoleEnum;
|
||||
|
||||
@OneToMany(() => User, (user) => user.role)
|
||||
users = new Collection<User>(this);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Collection, Entity, EntityRepositoryType, ManyToOne, OneToMany, Opt, Property } from "@mikro-orm/core";
|
||||
|
||||
import { RefreshToken } from "./refresh-token.entity";
|
||||
import { Role } from "./role.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { UserRepository } from "../repositories/user.repository";
|
||||
|
||||
@Entity({ repository: () => UserRepository })
|
||||
export class User extends BaseEntity {
|
||||
@Property({ type: "varchar", length: 150, nullable: true })
|
||||
email?: string;
|
||||
|
||||
@Property({ type: "varchar", length: 11, unique: true, nullable: false })
|
||||
phone!: string;
|
||||
|
||||
@Property({ type: "varchar", length: 50, unique: true, nullable: true })
|
||||
userName!: string;
|
||||
|
||||
@Property({ type: "varchar", length: 150 })
|
||||
password!: string;
|
||||
|
||||
@Property({ type: "varchar", length: 150 })
|
||||
firstName!: string;
|
||||
|
||||
@Property({ type: "varchar", length: 200 })
|
||||
lastName!: string;
|
||||
|
||||
@Property({ type: "varchar", length: 12, nullable: true })
|
||||
birthDate?: string;
|
||||
|
||||
@Property({ type: "varchar", length: 100, unique: true, nullable: true })
|
||||
nationalCode?: string;
|
||||
|
||||
@Property({ type: "varchar", length: 100, nullable: true })
|
||||
profilePic?: string;
|
||||
|
||||
@Property({ type: "boolean", default: false })
|
||||
emailVerified!: boolean & Opt;
|
||||
|
||||
//-----------------------------------
|
||||
|
||||
@ManyToOne(() => Role, { deleteRule: "restrict" })
|
||||
role!: Role;
|
||||
|
||||
//-----------------------------------
|
||||
|
||||
@OneToMany(() => RefreshToken, (token) => token.user)
|
||||
refreshTokens = new Collection<RefreshToken>(this);
|
||||
|
||||
[EntityRepositoryType]?: UserRepository;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum RoleEnum {
|
||||
ADMIN = "ADMIN",
|
||||
USER = "USER",
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { EntityRepository } from "@mikro-orm/core";
|
||||
|
||||
import { User } from "../entities/user.entity";
|
||||
|
||||
export class UserRepository extends EntityRepository<User> {}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import slugify from "slugify";
|
||||
|
||||
import { UserMessage } from "../../../common/enums/message.enum";
|
||||
import { CompleteRegistrationDto } from "../../auth/DTO/complete-register.dto";
|
||||
import { Role } from "../entities/role.entity";
|
||||
import { User } from "../entities/user.entity";
|
||||
import { RoleEnum } from "../enums/role.enum";
|
||||
import { UserRepository } from "../repositories/user.repository";
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
|
||||
/*******************************/
|
||||
|
||||
async findOneWithEmail(email: string) {
|
||||
const user = await this.userRepository.findOne({ email }, { populate: ["role"] });
|
||||
return user;
|
||||
}
|
||||
/*******************************/
|
||||
|
||||
async findOneWithPhone(phone: string) {
|
||||
const user = await this.userRepository.findOne({ phone }, { populate: ["role"] });
|
||||
return user;
|
||||
}
|
||||
/*******************************/
|
||||
|
||||
async findOneById(id: string) {
|
||||
const user = await this.userRepository.findOne({ id }, { populate: ["role"] });
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
return { user };
|
||||
}
|
||||
/*******************************/
|
||||
|
||||
async getMe(userId: string) {
|
||||
const user = await this.userRepository.findOne({ id: userId }, { populate: ["role"] });
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
return { user };
|
||||
}
|
||||
|
||||
/*******************************/
|
||||
|
||||
async createUser(registerDto: CompleteRegistrationDto, hashedPassword: string, em: EntityManager) {
|
||||
const role = await em.findOne(Role, { name: RoleEnum.USER });
|
||||
if (!role) throw new BadRequestException(UserMessage.ROLE_NOT_FOUND);
|
||||
|
||||
const existPhone = await em.findOne(User, { phone: registerDto.phone });
|
||||
if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST);
|
||||
|
||||
const existUser = await em.findOne(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 user = em.create(User, {
|
||||
...registerDto,
|
||||
userName,
|
||||
password: hashedPassword,
|
||||
role,
|
||||
});
|
||||
|
||||
await em.persistAndFlush(user);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/*******************************/
|
||||
|
||||
async updateUserPassword(user: User, newPassword: string) {
|
||||
user.password = newPassword;
|
||||
await this.em.flush();
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
import { ApiOperation } from "@nestjs/swagger";
|
||||
|
||||
import { UsersService } from "./services/users.service";
|
||||
import { UserDec } from "../../common/decorators/user.decorator";
|
||||
|
||||
@Controller("users")
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@ApiOperation({ summary: "Get user profile" })
|
||||
@Get("me")
|
||||
getMe(@UserDec("id") userId: string) {
|
||||
return this.usersService.getMe(userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { MikroOrmModule } from "@mikro-orm/nestjs";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { User } from "./entities/user.entity";
|
||||
import { UsersService } from "./services/users.service";
|
||||
import { UsersController } from "./users.controller";
|
||||
|
||||
@Module({
|
||||
imports: [MikroOrmModule.forFeature([User])],
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
export const SMS_CONFIG = "SMS_CONFIG";
|
||||
export const S3_CONFIG = "S3_CONFIG";
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
export interface IFile {
|
||||
fieldname: string;
|
||||
originalname: string;
|
||||
encoding: string;
|
||||
mimetype: string;
|
||||
buffer: Buffer;
|
||||
size: number;
|
||||
}
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
export type OtpCacheKeyType = "LOGIN" | "REGISTER" | "FORGOT_PASSWORD" | "INVOICE_VERIFY";
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
export interface IParameterArray {
|
||||
name: TemplateParams;
|
||||
value: string;
|
||||
}
|
||||
export interface ISmsResponse {
|
||||
status: number;
|
||||
message: string;
|
||||
}
|
||||
//----------------------------------------------
|
||||
|
||||
export interface ISmsGetLineResponse extends ISmsResponse {
|
||||
data: string[];
|
||||
}
|
||||
|
||||
export interface ISmsVerifyResponse extends ISmsResponse {
|
||||
data: { MessageId: number; Cost: number };
|
||||
}
|
||||
|
||||
//----------------------------------------------
|
||||
export interface ISmsVerifyBody {
|
||||
Parameters: IParameterArray[];
|
||||
Mobile: string;
|
||||
TemplateId: string;
|
||||
}
|
||||
|
||||
export type TemplateParams =
|
||||
| "VERIFICATIONCODE"
|
||||
| "code"
|
||||
| "invoiceId"
|
||||
| "price"
|
||||
| "items"
|
||||
| "loginDate"
|
||||
| "invoiceDate"
|
||||
| "title"
|
||||
| "description"
|
||||
| "ticketId"
|
||||
| "serviceName"
|
||||
| "date"
|
||||
| "amount"
|
||||
| "newBalance"
|
||||
| "reason"
|
||||
| "dueDate"
|
||||
| "lateFee"
|
||||
| "paidDate"
|
||||
| "user"
|
||||
| "subject"
|
||||
| "planName"
|
||||
| "message"
|
||||
| "blogTitle"
|
||||
| "fullName"
|
||||
| "mobile";
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
import { CACHE_MANAGER, Cache } from "@nestjs/cache-manager";
|
||||
import { Inject, Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class CacheService {
|
||||
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
|
||||
|
||||
async get<T>(key: string) {
|
||||
return await this.cacheManager.get<T>(key);
|
||||
}
|
||||
|
||||
async set(key: string, value: string, ttl: number = 120 * 1000) {
|
||||
await this.cacheManager.set(key, value, ttl);
|
||||
}
|
||||
|
||||
async del(key: string) {
|
||||
await this.cacheManager.del(key);
|
||||
}
|
||||
|
||||
async getTtl(key: string) {
|
||||
return await this.cacheManager.ttl(key);
|
||||
}
|
||||
}
|
||||
Executable
+156
@@ -0,0 +1,156 @@
|
||||
import { Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
|
||||
import { MailerService } from "@nestjs-modules/mailer";
|
||||
import { SentMessageInfo } from "nodemailer";
|
||||
|
||||
import { dateFormat, numberFormat } from "./international.utils";
|
||||
import { EmailMessage } from "../../../common/enums/message.enum";
|
||||
import {
|
||||
IAnnouncementNotificationData,
|
||||
IInvoiceNotificationData,
|
||||
IPaymentNotificationData,
|
||||
ITicketNotificationData,
|
||||
} from "../../notifications/interfaces/ISendNotificationData";
|
||||
import { IBaseNotificationData } from "../../notifications/interfaces/ISendNotificationData";
|
||||
|
||||
@Injectable()
|
||||
export class EmailService {
|
||||
private readonly logger = new Logger(EmailService.name);
|
||||
constructor(private readonly mailerService: MailerService) {}
|
||||
|
||||
async sendVerificationEmail(to: string, link: string, fullName: string): Promise<SentMessageInfo> {
|
||||
try {
|
||||
const emailInfo = await this.mailerService.sendMail({
|
||||
to: to,
|
||||
|
||||
subject: EmailMessage.EMAIL_VERIFICATION,
|
||||
template: "email-verify",
|
||||
context: {
|
||||
title: EmailMessage.EMAIL_VERIFICATION,
|
||||
link,
|
||||
fullName,
|
||||
},
|
||||
});
|
||||
return emailInfo;
|
||||
} catch (error) {
|
||||
this.logger.error("Email sending failed:", error);
|
||||
throw new InternalServerErrorException(EmailMessage.EMAIL_SENDING_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
async sendLoginEmail(data: IBaseNotificationData) {
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: data.userEmail,
|
||||
subject: EmailMessage.LOGIN,
|
||||
template: "login",
|
||||
context: {
|
||||
date: dateFormat(new Date()),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending login email", error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendInvoiceEmail(data: IInvoiceNotificationData) {
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: data.userEmail,
|
||||
subject: EmailMessage.INVOICE,
|
||||
template: "invoice",
|
||||
context: {
|
||||
price: numberFormat(data.price),
|
||||
createDate: dateFormat(data.createDate),
|
||||
dueDate: dateFormat(data.dueDate),
|
||||
invoiceId: data.invoiceId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending invoice email", error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendTicketEmail(data: ITicketNotificationData) {
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: data.userEmail,
|
||||
subject: EmailMessage.TICKET,
|
||||
template: "ticket",
|
||||
context: {
|
||||
ticketId: data.ticketId,
|
||||
subject: data.subject,
|
||||
date: data.date ? dateFormat(data.date) : dateFormat(new Date()),
|
||||
user: data.user,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending ticket email", error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendAnnouncementEmail(data: IAnnouncementNotificationData) {
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: data.userEmail,
|
||||
subject: EmailMessage.ANNOUNCEMENT,
|
||||
template: "announcement",
|
||||
context: {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
date: dateFormat(data.date),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending announcement email", error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendPaymentReminderEmail(data: IPaymentNotificationData) {
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: data.userEmail,
|
||||
subject: EmailMessage.PAYMENT_REMINDER,
|
||||
template: "payment-reminder",
|
||||
context: {
|
||||
amount: numberFormat(data.amount),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending payment reminder email", error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendPaymentCancellationEmail(data: IPaymentNotificationData) {
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: data.userEmail,
|
||||
subject: EmailMessage.PAYMENT_CANCELLATION,
|
||||
template: "payment-cancellation",
|
||||
context: {
|
||||
amount: numberFormat(data.amount),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending payment cancellation email", error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendAdminNotificationEmail(data: { userEmail: string; title: string; message: string }) {
|
||||
const emailData = {
|
||||
to: data.userEmail,
|
||||
subject: data.title,
|
||||
template: "admin-notification",
|
||||
context: {
|
||||
title: data.title,
|
||||
message: data.message,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await this.mailerService.sendMail(emailData);
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending admin notification email", error);
|
||||
// throw new InternalServerErrorException("error in sending admin notification email");
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
export function numberFormat(number: number, locale: string = "fa-IR") {
|
||||
return Intl.NumberFormat(locale).format(number);
|
||||
}
|
||||
|
||||
export function dateFormat(date: Date, locale: string = "fa-IR") {
|
||||
return new Intl.DateTimeFormat(locale).format(date);
|
||||
}
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
import { randomInt } from "node:crypto";
|
||||
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { CacheService } from "./cache.service";
|
||||
import { OtpCacheKeyType } from "../interfaces/IOtpKey";
|
||||
|
||||
@Injectable()
|
||||
export class OTPService {
|
||||
constructor(private cacheService: CacheService) {}
|
||||
//
|
||||
private generateOTP(): string {
|
||||
return randomInt(10000, 99999).toString();
|
||||
}
|
||||
//
|
||||
async generateAndSetInCache(identifier: string, cacheKey: OtpCacheKeyType) {
|
||||
const otp = this.generateOTP();
|
||||
const key = `${identifier}:${cacheKey}:OTP`;
|
||||
await this.cacheService.set(key, otp, 180 * 1000);
|
||||
return otp;
|
||||
}
|
||||
//
|
||||
async verifyOtp(identifier: string, otpCode: string, cacheKey: OtpCacheKeyType) {
|
||||
const key = `${identifier}:${cacheKey}:OTP`;
|
||||
const codeInCache = await this.cacheService.get<string | null>(key);
|
||||
return codeInCache && otpCode === codeInCache;
|
||||
}
|
||||
//
|
||||
async delOtpFormCache(identifier: string, cacheKey: OtpCacheKeyType) {
|
||||
const key = `${identifier}:${cacheKey}:OTP`;
|
||||
await this.cacheService.del(key);
|
||||
}
|
||||
|
||||
async checkExistOtp(identifier: string, cacheKey: OtpCacheKeyType) {
|
||||
const key = `${identifier}:${cacheKey}:OTP`;
|
||||
const codeInCache = await this.cacheService.get<string | null>(key);
|
||||
|
||||
if (!codeInCache) return null;
|
||||
|
||||
const ttlInMsSeconds = await this.cacheService.getTtl(key);
|
||||
|
||||
return Math.floor((ttlInMsSeconds! - Date.now()) / 1000);
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
||||
|
||||
export function PaginationUtils(paginationData: PaginationDto) {
|
||||
const { limit = 10, page = 1 } = paginationData;
|
||||
|
||||
// const pageN = Number.parseInt(page) || 1;
|
||||
// const limitN = Number.parseInt(limit) || 10;
|
||||
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
return {
|
||||
// page: page,
|
||||
// limit: limit > 50 ? 50 : limit,
|
||||
limit: limit,
|
||||
skip: skip,
|
||||
};
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { compare, genSalt, hash } from "bcrypt";
|
||||
@Injectable()
|
||||
export class PasswordService {
|
||||
//** */
|
||||
async hashPassword(rawPassword: string) {
|
||||
const salt = await genSalt(10);
|
||||
return hash(rawPassword, salt);
|
||||
}
|
||||
//** */
|
||||
async comparePassword(rawPassword: string, hashedPassword: string) {
|
||||
return compare(rawPassword, hashedPassword);
|
||||
}
|
||||
}
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { extname } from "node:path";
|
||||
|
||||
import { PutObjectCommand, PutObjectCommandInput, S3Client } from "@aws-sdk/client-s3";
|
||||
import { Inject, Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
|
||||
|
||||
import { IS3Configs } from "../../../configs/s3.config";
|
||||
import { S3_CONFIG } from "../constants";
|
||||
import { IFile } from "../interfaces/IFile";
|
||||
|
||||
@Injectable()
|
||||
export class S3Service {
|
||||
private readonly logger = new Logger(S3Service.name);
|
||||
private client: S3Client;
|
||||
|
||||
//
|
||||
constructor(@Inject(S3_CONFIG) private s3Configs: IS3Configs) {
|
||||
this.client = new S3Client({
|
||||
endpoint: this.s3Configs.endpoint,
|
||||
region: this.s3Configs.region,
|
||||
credentials: {
|
||||
accessKeyId: this.s3Configs.accessKeyId,
|
||||
secretAccessKey: this.s3Configs.secretAccessKey,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async upload(file: IFile) {
|
||||
const extName = extname(file.originalname);
|
||||
const params: PutObjectCommandInput = {
|
||||
Body: file.buffer,
|
||||
Bucket: this.s3Configs.bucket,
|
||||
Key: `images/${randomUUID()}${Date.now()}${extName}`,
|
||||
ACL: "public-read",
|
||||
};
|
||||
//
|
||||
|
||||
try {
|
||||
await this.client.send(new PutObjectCommand(params));
|
||||
const url = `${this.s3Configs.url}/${params.Key}`;
|
||||
return {
|
||||
url,
|
||||
originalname: file.originalname,
|
||||
size: file.size,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new InternalServerErrorException("Failed to upload file");
|
||||
}
|
||||
}
|
||||
|
||||
async uploadMultiple(files: IFile[]) {
|
||||
const uploadPromises = files.map((file) => this.upload(file));
|
||||
const results = await Promise.all(uploadPromises);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
Executable
+543
@@ -0,0 +1,543 @@
|
||||
import { HttpService } from "@nestjs/axios";
|
||||
import { Inject, Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
|
||||
import { AxiosError } from "axios";
|
||||
import { catchError, firstValueFrom } from "rxjs";
|
||||
|
||||
import { SmsMessage } from "../../../common/enums/message.enum";
|
||||
import { ISmsConfigs } from "../../../configs/sms.config";
|
||||
import { SMS_CONFIG } from "../constants";
|
||||
import { ISmsGetLineResponse, ISmsVerifyBody, ISmsVerifyResponse } from "../interfaces/ISms";
|
||||
|
||||
@Injectable()
|
||||
export class SmsService {
|
||||
private readonly logger = new Logger(SmsService.name);
|
||||
|
||||
constructor(
|
||||
@Inject(SMS_CONFIG) private smsConfigs: ISmsConfigs,
|
||||
private readonly httpService: HttpService,
|
||||
) {}
|
||||
//************************************************* */
|
||||
|
||||
async sendSmsVerifyCode(mobile: string, otpCode: string) {
|
||||
//
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [{ name: "code", value: otpCode }],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_OTP,
|
||||
};
|
||||
//
|
||||
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 verify sms", err);
|
||||
throw new InternalServerErrorException("error in sending verify sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
console.log(data, "================");
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
throw new InternalServerErrorException(SmsMessage.SEND_SMS_ERROR);
|
||||
}
|
||||
}
|
||||
//************************************************* */
|
||||
|
||||
async sendInvoiceVerifyCode(mobile: string, otpCode: string, invoiceId: number, price: number, items: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "code", value: otpCode },
|
||||
{ name: "invoiceId", value: invoiceId.toString() },
|
||||
{ name: "price", value: Intl.NumberFormat("fa-IR").format(price) },
|
||||
{ name: "items", value: items },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE,
|
||||
};
|
||||
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 verify sms", err);
|
||||
throw new InternalServerErrorException("error in sending verify sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
throw new InternalServerErrorException(SmsMessage.SEND_SMS_ERROR);
|
||||
}
|
||||
}
|
||||
//************************************************* */
|
||||
|
||||
async sendLoginSms(mobile: string, loginDate: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [{ name: "loginDate", value: loginDate }],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_LOGIN,
|
||||
};
|
||||
|
||||
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 verify sms", err);
|
||||
throw new InternalServerErrorException("error in sending verify sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
}
|
||||
}
|
||||
//************************************************* */
|
||||
|
||||
async sendInvoiceCreationSms(mobile: string, invoiceId: string, price: number, items: string, invoiceDate: string, dueDate: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "invoiceId", value: invoiceId },
|
||||
{ name: "price", value: Intl.NumberFormat("fa-IR").format(price) },
|
||||
{ name: "items", value: items },
|
||||
{ name: "invoiceDate", value: invoiceDate },
|
||||
{ name: "dueDate", value: dueDate },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_CREATED,
|
||||
};
|
||||
|
||||
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) => {
|
||||
console.error(err);
|
||||
|
||||
this.logger.error("error in sending invoice created sms", err);
|
||||
throw new InternalServerErrorException("error in sending invoice created sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
}
|
||||
}
|
||||
//************************************************* */
|
||||
async sendAnnouncementSms(mobile: string, title: string, description: string, date: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "title", value: title },
|
||||
{ name: "description", value: description },
|
||||
{ name: "date", value: date },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_ANNOUNCEMENT,
|
||||
};
|
||||
|
||||
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 announcement sms", err);
|
||||
throw new InternalServerErrorException("error in sending announcement sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
}
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
|
||||
async sendCreateTicketSms(mobile: string, ticketId: string, subject: string, date: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "ticketId", value: ticketId },
|
||||
{ name: "subject", value: subject },
|
||||
{ name: "date", value: date },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_TICKET_CREATED,
|
||||
};
|
||||
|
||||
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 ticket created sms", err);
|
||||
throw new InternalServerErrorException("error in sending ticket created sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
}
|
||||
}
|
||||
//************************************************* */
|
||||
|
||||
async sendAnswerTicketSms(mobile: string, ticketId: string, subject: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "ticketId", value: ticketId },
|
||||
{ name: "subject", value: subject },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_TICKET_ANSWERED,
|
||||
};
|
||||
|
||||
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 ticket answered sms", err);
|
||||
throw new InternalServerErrorException("error in sending ticket answered sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
}
|
||||
}
|
||||
//************************************************* */
|
||||
|
||||
async sendTicketAssignedToAdminSms(mobile: string, ticketId: string, subject: string, user: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "ticketId", value: ticketId },
|
||||
{ name: "subject", value: subject },
|
||||
{ name: "user", value: user },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_TICKET_ASSIGNED_ADMIN,
|
||||
};
|
||||
|
||||
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 ticket assigned sms", err);
|
||||
throw new InternalServerErrorException("error in sending ticket assigned sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
}
|
||||
}
|
||||
//************************************************* */
|
||||
async sendInvoiceApprovedSms(mobile: string, invoiceId: string, price: number, dueDate: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "invoiceId", value: invoiceId },
|
||||
{ name: "price", value: Intl.NumberFormat("fa-IR").format(price) },
|
||||
{ name: "dueDate", value: dueDate },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_APPROVED,
|
||||
};
|
||||
|
||||
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 invoice approved sms", err);
|
||||
throw new InternalServerErrorException("error in sending invoice approved sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
// throw new InternalServerErrorException("error in sending sms");
|
||||
}
|
||||
}
|
||||
//************************************************* */
|
||||
async sendInvoicePaidSms(mobile: string, invoiceId: string, amount: number, paidDate: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "invoiceId", value: invoiceId },
|
||||
{ name: "amount", value: amount.toString() },
|
||||
{ name: "paidDate", value: paidDate },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_PAID,
|
||||
};
|
||||
|
||||
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 invoice paid sms", err);
|
||||
throw new InternalServerErrorException("error in sending invoice paid sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
// throw new InternalServerErrorException("error in sending sms");
|
||||
}
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
|
||||
async sendInvoicePaymentReminderSms(mobile: string, invoiceId: string, amount: number, dueDate: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "invoiceId", value: invoiceId },
|
||||
{ name: "amount", value: amount.toString() },
|
||||
{ name: "dueDate", value: dueDate },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_REMINDER,
|
||||
};
|
||||
|
||||
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 invoice reminder sms", err);
|
||||
throw new InternalServerErrorException("error in sending invoice reminder sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
}
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
|
||||
async sendInvoiceOverdueSms(mobile: string, invoiceId: string, amount: number, lateFee: number, dueDate: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "invoiceId", value: invoiceId },
|
||||
{ name: "amount", value: amount.toString() },
|
||||
{ name: "lateFee", value: lateFee.toString() },
|
||||
{ name: "dueDate", value: dueDate },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_OVERDUE,
|
||||
};
|
||||
|
||||
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 invoice overdue sms", err);
|
||||
throw new InternalServerErrorException("error in sending invoice overdue sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
}
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
|
||||
async sendRecurringInvoiceDraftSms(adminMobile: string, invoiceId: string, price: number) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "invoiceId", value: invoiceId },
|
||||
{ name: "price", value: Intl.NumberFormat("fa-IR").format(price) },
|
||||
],
|
||||
Mobile: adminMobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_RECURRING_INVOICE_DRAFT,
|
||||
};
|
||||
|
||||
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 recurring invoice draft sms", err);
|
||||
throw new InternalServerErrorException("error in sending recurring invoice draft sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
// throw new InternalServerErrorException("error in sending sms");
|
||||
}
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
|
||||
async sendBlockServiceSms(mobile: string, invoiceId: string, planName: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "invoiceId", value: invoiceId },
|
||||
{ name: "planName", value: planName },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_SUBSCRIPTION_CANCELLED,
|
||||
};
|
||||
|
||||
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 subscription cancelled sms", err);
|
||||
throw new InternalServerErrorException("error in sending subscription cancelled sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
// throw new InternalServerErrorException("error in sending sms");
|
||||
}
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
//************************************************* */
|
||||
|
||||
async sendPaymentReminderSms(mobile: string, amount: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [{ name: "amount", value: amount }],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_PAYMENT_REMINDER,
|
||||
};
|
||||
|
||||
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 payment reminder sms", err);
|
||||
throw new InternalServerErrorException("error in sending payment reminder sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
}
|
||||
}
|
||||
//************************************************* */
|
||||
|
||||
async sendPaymentCancellationSms(mobile: string, amount: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [{ name: "amount", value: amount }],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_PAYMENT_CANCELLATION,
|
||||
};
|
||||
|
||||
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 payment cancellation sms", err);
|
||||
throw new InternalServerErrorException("error in sending payment cancellation sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
}
|
||||
}
|
||||
|
||||
async getSmsLines() {
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.get<ISmsGetLineResponse>(`${this.smsConfigs.API_URL}/lines`, {
|
||||
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
|
||||
})
|
||||
.pipe(
|
||||
catchError((error: AxiosError) => {
|
||||
this.logger.error("error in getting sms lines", error);
|
||||
throw new InternalServerErrorException("error in getting sms lines");
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in getting sms lines", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { S3_CONFIG, SMS_CONFIG } from "./constants";
|
||||
import { CacheService } from "./providers/cache.service";
|
||||
import { EmailService } from "./providers/email.service";
|
||||
import { OTPService } from "./providers/otp.service";
|
||||
import { PasswordService } from "./providers/password.service";
|
||||
import { S3Service } from "./providers/s3.service";
|
||||
import { SmsService } from "./providers/sms.service";
|
||||
import { S3Configs } from "../../configs/s3.config";
|
||||
import { smsConfigs } from "../../configs/sms.config";
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
EmailService,
|
||||
OTPService,
|
||||
PasswordService,
|
||||
CacheService,
|
||||
SmsService,
|
||||
S3Service,
|
||||
{
|
||||
provide: SMS_CONFIG,
|
||||
useFactory: smsConfigs().useFactory,
|
||||
inject: smsConfigs().inject,
|
||||
},
|
||||
{
|
||||
provide: S3_CONFIG,
|
||||
useFactory: S3Configs().useFactory,
|
||||
inject: S3Configs().inject,
|
||||
},
|
||||
],
|
||||
exports: [SMS_CONFIG, S3_CONFIG, S3Service, OTPService, PasswordService, CacheService, SmsService, EmailService],
|
||||
})
|
||||
export class UtilsModule {}
|
||||
Reference in New Issue
Block a user