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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user