chore: first

This commit is contained in:
Mahyargdz
2025-05-11 10:27:30 +03:30
commit 4e563c497d
102 changed files with 18602 additions and 0 deletions
+24
View File
@@ -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
View File
@@ -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;
}
+18
View File
@@ -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) {}
+9
View File
@@ -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;
}
+12
View File
@@ -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;
}
+20
View File
@@ -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"]) {}
+98
View File
@@ -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);
// }
}
+20
View File
@@ -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 {}
+25
View File
@@ -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;
}
}
+28
View File
@@ -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;
}
}
+32
View File
@@ -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;
// }
// }
+27
View File
@@ -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
View File
@@ -0,0 +1,6 @@
import { RoleEnum } from "../../users/enums/role.enum";
export interface ITokenPayload {
id: string;
role: RoleEnum;
}
+227
View File
@@ -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;
}
}
+143
View File
@@ -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");
}
}
+23
View File
@@ -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 };
}
}
+23
View File
@@ -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
});
+26
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+14
View File
@@ -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);
}
+51
View File
@@ -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;
}
+4
View File
@@ -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;
}
}
+16
View File
@@ -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);
}
}
+14
View File
@@ -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 {}
+2
View File
@@ -0,0 +1,2 @@
export const SMS_CONFIG = "SMS_CONFIG";
export const S3_CONFIG = "S3_CONFIG";
+8
View File
@@ -0,0 +1,8 @@
export interface IFile {
fieldname: string;
originalname: string;
encoding: string;
mimetype: string;
buffer: Buffer;
size: number;
}
+1
View File
@@ -0,0 +1 @@
export type OtpCacheKeyType = "LOGIN" | "REGISTER" | "FORGOT_PASSWORD" | "INVOICE_VERIFY";
+51
View File
@@ -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";
+23
View File
@@ -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);
}
}
View File
+156
View File
@@ -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
View File
@@ -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);
}
+44
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
+57
View File
@@ -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;
}
}
+543
View File
@@ -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);
}
}
}
+34
View File
@@ -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 {}