chore: add user get profile and check validity route
This commit is contained in:
+5
-2
@@ -1,10 +1,12 @@
|
||||
import { CacheModule } from "@nestjs/cache-manager";
|
||||
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { ThrottlerModule } from "@nestjs/throttler";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
|
||||
import { cacheConfig } from "./configs/cache.config";
|
||||
import { DatabaseConfigs } from "./configs/typeorm.config";
|
||||
import { rateLimitConfig } from "./configs/rateLimit.config";
|
||||
import { databaseConfigs } from "./configs/typeorm.config";
|
||||
import { HTTPLogger } from "./core/middlewares/logger.middleware";
|
||||
import { AuthModule } from "./modules/auth/auth.module";
|
||||
import { TicketsModule } from "./modules/tickets/tickets.module";
|
||||
@@ -12,9 +14,10 @@ import { UsersModule } from "./modules/users/users.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ThrottlerModule.forRootAsync(rateLimitConfig()),
|
||||
ConfigModule.forRoot({ cache: true, isGlobal: true }),
|
||||
CacheModule.registerAsync(cacheConfig()),
|
||||
TypeOrmModule.forRootAsync(DatabaseConfigs()),
|
||||
TypeOrmModule.forRootAsync(databaseConfigs()),
|
||||
UsersModule,
|
||||
TicketsModule,
|
||||
AuthModule,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// export const AUTH_THROTTLE = "AUTH_THROTTLE";
|
||||
export const AUTH_THROTTLE_TTL = 5 * 60 * 1000;
|
||||
export const AUTH_THROTTLE_LIMIT = 5;
|
||||
export const JWT_STRATEGY_NAME = "jwt_Strategy";
|
||||
@@ -0,0 +1,9 @@
|
||||
import { UseGuards, applyDecorators } from "@nestjs/common";
|
||||
import { ApiBearerAuth } from "@nestjs/swagger";
|
||||
|
||||
import { JwtAuthGuard } from "../../modules/auth/guards/auth.guard";
|
||||
import { RoleGuard } from "../../modules/auth/guards/role.guard";
|
||||
|
||||
export function AuthGuards() {
|
||||
return applyDecorators(UseGuards(JwtAuthGuard, RoleGuard), ApiBearerAuth("authorization"));
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { SetMetadata } from "@nestjs/common";
|
||||
|
||||
export const ROLES_KEY = "roles";
|
||||
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ExecutionContext, createParamDecorator } from "@nestjs/common";
|
||||
import { FastifyRequest } from "fastify";
|
||||
|
||||
import { User } from "../../modules/users/entities/user.entity";
|
||||
|
||||
declare module "fastify" {
|
||||
interface FastifyRequest {
|
||||
user?: Omit<User, "password">;
|
||||
}
|
||||
}
|
||||
|
||||
export const UserDec = createParamDecorator((_data: unknown, ctx: ExecutionContext) => {
|
||||
const req = ctx.switchToHttp().getRequest<FastifyRequest>();
|
||||
return req.user;
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
export const enum AuthMessage {
|
||||
UNAUTHORIZED_ACCESS = "شما دسترسی لازم برای این عملیات را ندارید.",
|
||||
PHONE_REGISTERED = "شماره ثبت شد",
|
||||
INVALID_PHONE_FORMAT = "فرمت شماره تلفن صحیح نیست",
|
||||
OTP_SENT = "کد یکبار مصرف به شماره شما ارسال شد.",
|
||||
@@ -20,7 +21,7 @@ export const enum AuthMessage {
|
||||
USER_EXISTS = "با این شماره قبلا ثبت نام شده است",
|
||||
USER_REGISTER_SUCCESS = "ثبت نام موفقیت آمیز بود",
|
||||
INVALID_USER = "کاربری با این مشخصات یافت نشد",
|
||||
PHONE_NOT_FOUND = "کاربری با این آیدی یافت نشد",
|
||||
PHONE_NOT_FOUND = "کاربری با این شماره یافت نشد",
|
||||
TOKEN_EXPIRED = "توکن منقضی شده است",
|
||||
TOKEN_INVALID = "توکن نامعتبر است",
|
||||
BANNED = "در حال حاضر شما امکان دسترسی به این سرویس را ندارید",
|
||||
@@ -46,9 +47,19 @@ export const enum AuthMessage {
|
||||
BIRTH_DATE_NOT_EMPTY = "تاریخ تولد نمیتواند خالی باشد",
|
||||
NATIONAL_CODE_INCORRECT = "کد ملی باید ۱۰ رقمی باشد",
|
||||
NATIONAL_NOT_EMPTY = "کد ملی نمیتواند خالی باشد",
|
||||
OTP_ALREADY_SENT = "کد یکبار مصرف قبلا ارسال شده است",
|
||||
TOO_MANY_REQUESTS = "تعداد درخواست های شما بیش از حد مجاز است",
|
||||
}
|
||||
|
||||
export const enum UserMessage {
|
||||
USER_NOT_FOUND = "کاربری با این مشخصات یافت نشد",
|
||||
EMAIL_EXIST = "ایمیل قبلا ثبت شده است",
|
||||
ROLE_NOT_FOUND = "نقش یافت نشد",
|
||||
USER_EXISTS = "کاربر با این مشخصات وجود دارد",
|
||||
}
|
||||
|
||||
export const enum CommonMessage {
|
||||
VALIDITY_TYPE_REQUIRED = "نوع اعتبار سنجی مورد نیاز است",
|
||||
THIS_FILED_IS_REQUIRED = "این فیلد الزامی است",
|
||||
VALID_FOR_CHOOSE = "معتبر برای انتخاب",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { JwtModuleAsyncOptions } from "@nestjs/jwt";
|
||||
|
||||
export function jwtConfig(): JwtModuleAsyncOptions {
|
||||
return {
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
secret: configService.getOrThrow<string>("JWT_SECRET_KEY"),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ThrottlerStorageRedisService } from "@nest-lab/throttler-storage-redis";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { ThrottlerAsyncOptions } from "@nestjs/throttler";
|
||||
|
||||
import { AuthMessage } from "../common/enums/message.enum";
|
||||
|
||||
export function rateLimitConfig(): ThrottlerAsyncOptions {
|
||||
return {
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
throttlers: [
|
||||
{
|
||||
ttl: configService.getOrThrow("THROTTLE_TTL"),
|
||||
limit: configService.getOrThrow("THROTTLE_LIMIT"),
|
||||
},
|
||||
],
|
||||
errorMessage: AuthMessage.TOO_MANY_REQUESTS,
|
||||
storage: new ThrottlerStorageRedisService(configService.getOrThrow("REDIS_URI")),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
export function SmsConfigs() {
|
||||
export function smsConfigs() {
|
||||
return {
|
||||
inject: [ConfigService],
|
||||
useFactory(configService: ConfigService) {
|
||||
|
||||
@@ -5,6 +5,17 @@ export function getSwaggerDocument(app: NestFastifyApplication) {
|
||||
const swaggerConfig = new DocumentBuilder()
|
||||
.setTitle("The Danak dsc api document")
|
||||
.setDescription("The Danak dsc API description")
|
||||
.addBearerAuth(
|
||||
{
|
||||
type: "http",
|
||||
scheme: "bearer",
|
||||
bearerFormat: "JWT",
|
||||
name: "authorization",
|
||||
in: "header",
|
||||
},
|
||||
"authorization",
|
||||
)
|
||||
|
||||
.setVersion("1.0.0")
|
||||
.build();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { TypeOrmModuleAsyncOptions } from "@nestjs/typeorm";
|
||||
|
||||
export function DatabaseConfigs(): TypeOrmModuleAsyncOptions {
|
||||
export function databaseConfigs(): TypeOrmModuleAsyncOptions {
|
||||
return {
|
||||
inject: [ConfigService],
|
||||
useFactory(configService: ConfigService) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from "@nestjs/common";
|
||||
import { FastifyReply, FastifyRequest } from "fastify";
|
||||
import { FastifyReply } from "fastify";
|
||||
|
||||
@Catch(HttpException)
|
||||
export class HttpExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: HttpException, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const reply = ctx.getResponse<FastifyReply>();
|
||||
const request = ctx.getRequest<FastifyRequest>();
|
||||
// const request = ctx.getRequest<FastifyRequest>();
|
||||
const status = exception.getStatus() || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
const exceptionResponse = exception.getResponse();
|
||||
@@ -18,7 +18,7 @@ export class HttpExceptionFilter implements ExceptionFilter {
|
||||
error: {
|
||||
message,
|
||||
// timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
// path: request.url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsEmail, IsMobilePhone, IsNotEmpty, IsNumberString, IsString, Length, MinLength } from "class-validator";
|
||||
import { IsMobilePhone, IsNotEmpty, IsNumberString, IsString, Length, MinLength } from "class-validator";
|
||||
|
||||
import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
@@ -27,10 +27,10 @@ export class CompleteRegistrationDto {
|
||||
@ApiProperty({ description: "Last name", example: "jamshidi" })
|
||||
lastName: string;
|
||||
|
||||
@IsNotEmpty({ message: AuthMessage.EMAIL_NOT_EMPTY })
|
||||
@IsEmail({}, { message: AuthMessage.INVALID_EMAIL_FORMAT })
|
||||
@ApiProperty({ description: "Email", example: "ma@gmail.com" })
|
||||
email: string;
|
||||
// @IsNotEmpty({ message: AuthMessage.EMAIL_NOT_EMPTY })
|
||||
// @IsEmail({}, { message: AuthMessage.INVALID_EMAIL_FORMAT })
|
||||
// @ApiProperty({ description: "Email", example: "ma@gmail.com" })
|
||||
// email: string;
|
||||
|
||||
@IsNotEmpty({ message: AuthMessage.BIRTH_DATE_NOT_EMPTY })
|
||||
@IsString({ message: AuthMessage.BIRTH_DATE_NOT_EMPTY })
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ApiProperty } 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 })
|
||||
@IsMobilePhone("fa-IR", {}, { message: AuthMessage.INVALID_PHONE_FORMAT })
|
||||
@ApiProperty({ description: "phone number", default: "09123456789" })
|
||||
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;
|
||||
}
|
||||
@@ -1,41 +1,54 @@
|
||||
import { Body, Controller, Post } from "@nestjs/common";
|
||||
import { Body, Controller, HttpCode, HttpStatus, Post, UseGuards } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
import { Throttle, ThrottlerGuard } from "@nestjs/throttler";
|
||||
|
||||
import { AuthService } from "./auth.service";
|
||||
import { CompleteRegistrationDto } from "./DTO/complete-register.dto";
|
||||
import { LoginPasswordDTO } from "./DTO/loginPassword.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 } from "../../common/constants";
|
||||
|
||||
@ApiTags("Auth")
|
||||
@Controller("auth")
|
||||
@Throttle({ default: { limit: AUTH_THROTTLE_LIMIT, ttl: AUTH_THROTTLE_TTL } })
|
||||
@UseGuards(ThrottlerGuard)
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
@ApiOperation({ summary: "Initiate registration" })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("register/initiate")
|
||||
async register(@Body() registerDto: RequestOtpDto) {
|
||||
register(@Body() registerDto: RequestOtpDto) {
|
||||
return this.authService.initiateRegistration(registerDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "complete registration ==> step 2" })
|
||||
@Post("register/complete")
|
||||
async completeRegistration(@Body() completeRegistrationDto: CompleteRegistrationDto) {
|
||||
completeRegistration(@Body() completeRegistrationDto: CompleteRegistrationDto) {
|
||||
return this.authService.completeRegistration(completeRegistrationDto);
|
||||
}
|
||||
|
||||
// @Post("login")
|
||||
// async login(@Body() loginDto: LoginDto) {
|
||||
// return this.authService.login(loginDto);
|
||||
// }
|
||||
@ApiOperation({ summary: "request to login with otp" })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("otp/send")
|
||||
sendOtp(@Body() requestOtpDto: RequestOtpDto) {
|
||||
return this.authService.requestLoginOtp(requestOtpDto);
|
||||
}
|
||||
|
||||
// @Post("otp/register/send")
|
||||
// async sendOtp(@Body() sendOtpDto: RequestOtpDto) {
|
||||
// return this.authService.requestRegisterOTP(sendOtpDto);
|
||||
// }
|
||||
@ApiOperation({ summary: "verify otp for login" })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("otp/verify")
|
||||
verifyOtp(@Body() verifyOtpDto: VerifyOtpDto) {
|
||||
return this.authService.verifyLoginOtp(verifyOtpDto);
|
||||
}
|
||||
|
||||
// @Post("otp/verify")
|
||||
// async verifyOtp(@Body() verifyOtpDto: VerifyOtpDto) {
|
||||
// return this.authService.verifyOtp(verifyOtpDto);
|
||||
// }
|
||||
@ApiOperation({ summary: "login with password" })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("login/password")
|
||||
loginWithPassword(@Body() loginPasswordDto: LoginPasswordDTO) {
|
||||
return this.authService.loginWithPassword(loginPasswordDto);
|
||||
}
|
||||
|
||||
// @Post("forgot-password")
|
||||
// async forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto) {
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { JwtModule } from "@nestjs/jwt";
|
||||
import { PassportModule } from "@nestjs/passport";
|
||||
|
||||
import { AuthController } from "./auth.controller";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { jwtConfig } from "../../configs/jwt.config";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
import { UtilsModule } from "../utils/utils.module";
|
||||
import { AuthService } from "./providers/auth.service";
|
||||
import { TokensService } from "./providers/tokens.service";
|
||||
import { JwtStrategy } from "./strategies/jwt.strategy";
|
||||
|
||||
@Module({
|
||||
imports: [UtilsModule, UsersModule],
|
||||
imports: [UtilsModule, UsersModule, PassportModule, JwtModule.registerAsync(jwtConfig())],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService],
|
||||
providers: [AuthService, TokensService, JwtStrategy],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
||||
|
||||
import { CompleteRegistrationDto } from "./DTO/complete-register.dto";
|
||||
import { LoginPasswordDTO } from "./DTO/loginPassword.dto";
|
||||
import { RequestOtpDto } from "./DTO/request-otp.dto";
|
||||
import { AuthMessage, UserMessage } from "../../common/enums/message.enum";
|
||||
import { UsersService } from "../users/providers/users.service";
|
||||
import { OTPService } from "../utils/providers/otp.service";
|
||||
import { PasswordService } from "../utils/providers/password.service";
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private readonly logger = new Logger(AuthService.name);
|
||||
constructor(
|
||||
private readonly userService: UsersService,
|
||||
private readonly passwordService: PasswordService,
|
||||
private readonly otpService: OTPService,
|
||||
) {}
|
||||
//****************** */
|
||||
async initiateRegistration(requestOtpDto: RequestOtpDto) {
|
||||
const { phone } = requestOtpDto;
|
||||
const otpCode = await this.otpService.generateAndSetInCache(phone, "REGISTER");
|
||||
// await SmsService.sendOtp(phone, otpCode);
|
||||
this.logger.log(`OTP sent to ${phone}: ${otpCode}`);
|
||||
|
||||
return {
|
||||
message: AuthMessage.OTP_SENT,
|
||||
otpCode,
|
||||
};
|
||||
}
|
||||
//****************** */
|
||||
async completeRegistration(completeRegistrationDto: CompleteRegistrationDto) {
|
||||
const { phone, code } = completeRegistrationDto;
|
||||
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.userService.createUser(completeRegistrationDto, hashedPassword);
|
||||
|
||||
return {
|
||||
message: AuthMessage.USER_REGISTER_SUCCESS,
|
||||
user,
|
||||
};
|
||||
}
|
||||
//****************** */
|
||||
|
||||
async loginWithPassword(loginDto: LoginPasswordDTO) {
|
||||
const { email, password } = loginDto;
|
||||
|
||||
const user = await this.userService.findOneWithEmail(email);
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
|
||||
const passCompare = await this.passwordService.comparePassword(password, user.password);
|
||||
if (!passCompare) throw new BadRequestException(AuthMessage.INVALID_PASSWORD);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
|
||||
import { JWT_STRATEGY_NAME } from "../../../common/constants";
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard(JWT_STRATEGY_NAME) {}
|
||||
@@ -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.includes(user.role.name);
|
||||
if (!hasRequiredRole) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { RoleEnum } from "../../users/enums/role.enum";
|
||||
|
||||
export interface ITokenPayload {
|
||||
sub: string;
|
||||
role: RoleEnum;
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
||||
|
||||
import { TokensService } from "./tokens.service";
|
||||
import { AuthMessage, UserMessage } from "../../../common/enums/message.enum";
|
||||
import { UsersService } from "../../users/providers/users.service";
|
||||
import { OTPService } from "../../utils/providers/otp.service";
|
||||
import { PasswordService } from "../../utils/providers/password.service";
|
||||
import { CompleteRegistrationDto } from "../DTO/complete-register.dto";
|
||||
import { LoginPasswordDTO } from "../DTO/loginPassword.dto";
|
||||
import { RequestOtpDto } from "../DTO/request-otp.dto";
|
||||
import { VerifyOtpDto } from "../DTO/verify-otp.dto";
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private readonly logger = new Logger(AuthService.name);
|
||||
constructor(
|
||||
private readonly usersService: UsersService,
|
||||
private readonly passwordService: PasswordService,
|
||||
private readonly otpService: OTPService,
|
||||
private readonly tokensService: TokensService,
|
||||
) {}
|
||||
//****************** */
|
||||
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 SmsService.sendOtp(phone, otpCode);
|
||||
this.logger.log(`OTP sent to ${phone}: ${otpCode}`);
|
||||
|
||||
return {
|
||||
message: AuthMessage.OTP_SENT,
|
||||
otpCode,
|
||||
};
|
||||
}
|
||||
//****************** */
|
||||
async completeRegistration(completeRegistrationDto: CompleteRegistrationDto) {
|
||||
const { phone, code } = completeRegistrationDto;
|
||||
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);
|
||||
|
||||
const tokens = this.tokensService.generateAccessAndRefreshToken({ sub: user.id, role: user.role.name });
|
||||
|
||||
return {
|
||||
message: AuthMessage.USER_REGISTER_SUCCESS,
|
||||
...tokens,
|
||||
};
|
||||
}
|
||||
//****************** */
|
||||
|
||||
async loginWithPassword(loginDto: LoginPasswordDTO) {
|
||||
const { email, password } = loginDto;
|
||||
|
||||
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);
|
||||
|
||||
const tokens = this.tokensService.generateAccessAndRefreshToken({ sub: user.id, role: user.role.name });
|
||||
|
||||
return {
|
||||
message: AuthMessage.PASSWORD_LOGIN_SUCCESS,
|
||||
...tokens,
|
||||
};
|
||||
}
|
||||
|
||||
//****************** */
|
||||
|
||||
async requestLoginOtp(requestOtpDto: RequestOtpDto) {
|
||||
const { phone } = requestOtpDto;
|
||||
const user = await this.usersService.findOneWithPhone(phone);
|
||||
if (!user) throw new BadRequestException(AuthMessage.PHONE_NOT_FOUND);
|
||||
|
||||
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 SmsService.sendOtp(phone, otpCode);
|
||||
this.logger.log(`OTP sent to ${phone}: ${otpCode}`);
|
||||
|
||||
return {
|
||||
message: AuthMessage.OTP_SENT,
|
||||
otpCode,
|
||||
};
|
||||
}
|
||||
|
||||
//****************** */
|
||||
async verifyLoginOtp(verifyOtpDto: VerifyOtpDto) {
|
||||
const { code, phone } = verifyOtpDto;
|
||||
|
||||
const isValid = await this.otpService.verifyOtp(phone, code, "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);
|
||||
|
||||
const tokens = this.tokensService.generateAccessAndRefreshToken({ sub: user.id, role: user.role.name });
|
||||
return {
|
||||
message: AuthMessage.LOGIN_SUCCESS,
|
||||
...tokens,
|
||||
};
|
||||
}
|
||||
//****************** */
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
|
||||
import { ITokenPayload } from "../interfaces/IToken-payload";
|
||||
// import { UserRepository } from "../users/providers/users.repository";
|
||||
|
||||
@Injectable()
|
||||
export class TokensService {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly jwtService: JwtService,
|
||||
// private readonly userRepository: UserRepository,
|
||||
) {}
|
||||
// ----------- generate token -----------------
|
||||
generateAccessAndRefreshToken(payload: ITokenPayload) {
|
||||
const access_expire = this.configService.getOrThrow<number>("ACCESS_TOKEN_EXPIRE");
|
||||
const refresh_expire = this.configService.getOrThrow<number>("REFRESH_TOKEN_EXPIRE");
|
||||
|
||||
const accessToken = this.jwtService.sign(payload, { expiresIn: `${access_expire}h` });
|
||||
|
||||
const refreshToken = this.jwtService.sign(payload, { expiresIn: `${refresh_expire}d` });
|
||||
|
||||
return {
|
||||
accessToken: { token: accessToken, expire: Date.now() + 1000 * 60 * 60 * access_expire },
|
||||
refreshToken: { token: refreshToken, expire: Date.now() + 1000 * 60 * 60 * 24 * refresh_expire },
|
||||
};
|
||||
}
|
||||
|
||||
//****************************** */
|
||||
// validateRefreshToken(token: string) {
|
||||
// const { sub } = this.verifyToken(token, "refresh");
|
||||
// return this.generateAccessAndRefreshToken({ sub: sub });
|
||||
// }
|
||||
|
||||
// verifyToken(token: string, type: "access" | "refresh"): ITokenPayload {
|
||||
// try {
|
||||
// if (type === "access") {
|
||||
// return this.jwtService.verify(token, {
|
||||
// secret: process.env.ACCESS_TOKEN_SECRET,
|
||||
// ignoreExpiration: false,
|
||||
// });
|
||||
// } else if (type === "refresh") {
|
||||
// return this.jwtService.verify(token, {
|
||||
// secret: process.env.REFRESH_TOKEN_SECRET,
|
||||
// ignoreExpiration: false,
|
||||
// });
|
||||
// } else throw new Error("Invalid token type => access / refresh");
|
||||
// } catch {
|
||||
// throw new HttpException(AuthMessage.LoginAgain, HttpStatus.UNAUTHORIZED);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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_KEY"),
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: ITokenPayload) {
|
||||
return { id: payload.sub, role: payload.role };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsEnum, IsNotEmpty, IsString } from "class-validator";
|
||||
|
||||
import { CommonMessage } from "../../../common/enums/message.enum";
|
||||
import { ValidityType } from "../enums/validity-type.enum";
|
||||
|
||||
export class CheckValidityDTO {
|
||||
@IsNotEmpty({ message: CommonMessage.VALIDITY_TYPE_REQUIRED })
|
||||
@IsEnum(ValidityType)
|
||||
@ApiProperty({ enum: ValidityType, example: ValidityType.USERNAME })
|
||||
type: ValidityType;
|
||||
|
||||
@IsNotEmpty({ message: CommonMessage.THIS_FILED_IS_REQUIRED })
|
||||
@IsString({ message: CommonMessage.THIS_FILED_IS_REQUIRED })
|
||||
@ApiProperty({ example: "mahyargdz" })
|
||||
value: string;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export class UpdateProfileDto {
|
||||
userName: string;
|
||||
}
|
||||
@@ -5,6 +5,6 @@ import { RoleEnum } from "../enums/role.enum";
|
||||
|
||||
@Entity()
|
||||
export class Role extends BaseEntity {
|
||||
@Column({ type: "enum", enum: RoleEnum, default: RoleEnum.USER, nullable: false })
|
||||
@Column({ type: "enum", enum: RoleEnum, default: RoleEnum.USER, nullable: false, unique: true })
|
||||
name: RoleEnum;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,14 @@ import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
|
||||
@Entity()
|
||||
export class User extends BaseEntity {
|
||||
@Column({ type: "varchar", length: 150, unique: true, nullable: true })
|
||||
@Column({ type: "varchar", length: 150, nullable: true })
|
||||
email: string;
|
||||
|
||||
@Column({ type: "varchar", length: 11, unique: true, nullable: true })
|
||||
phoneNumber: string;
|
||||
@Column({ type: "varchar", length: 11, unique: true, nullable: false })
|
||||
phone: string;
|
||||
|
||||
@Column({ type: "varchar", length: 150, unique: true, nullable: true })
|
||||
userName: string;
|
||||
|
||||
@Column({ type: "varchar", length: 150 })
|
||||
password: string;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum ValidityType {
|
||||
USERNAME = "userName",
|
||||
}
|
||||
@@ -11,8 +11,14 @@ export class UserRepository extends Repository<User> {
|
||||
}
|
||||
|
||||
async findOneWithEmail(email: string): Promise<User | null> {
|
||||
return await this.findOneBy({
|
||||
return this.findOneBy({
|
||||
email,
|
||||
});
|
||||
}
|
||||
|
||||
async findOneWithPhone(phone: string): Promise<User | null> {
|
||||
return this.findOneBy({
|
||||
phone,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,35 +3,82 @@ import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
||||
import { RoleRepository } from "./roles.repository";
|
||||
import { UserRepository } from "./users.repository";
|
||||
import { roles } from "../../../../db/seeders/role.seeder";
|
||||
import { UserMessage } from "../../../common/enums/message.enum";
|
||||
import { CommonMessage, UserMessage } from "../../../common/enums/message.enum";
|
||||
import { CompleteRegistrationDto } from "../../auth/DTO/complete-register.dto";
|
||||
import { CheckValidityDTO } from "../DTO/check-validity.dto";
|
||||
import { User } from "../entities/user.entity";
|
||||
import { RoleEnum } from "../enums/role.enum";
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
private readonly userSelect = {
|
||||
id: true,
|
||||
phone: true,
|
||||
email: true,
|
||||
userName: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
nationalCode: true,
|
||||
birthDate: true,
|
||||
role: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
};
|
||||
private logger = new Logger(UsersService.name);
|
||||
constructor(
|
||||
private userRepository: UserRepository,
|
||||
private roleRepository: RoleRepository,
|
||||
) {}
|
||||
|
||||
/************************************************************ */
|
||||
|
||||
async getMe(userId: string) {
|
||||
const user = await this.userRepository.findOne({ where: { id: userId }, select: this.userSelect });
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
return { user };
|
||||
}
|
||||
/************************************************************ */
|
||||
|
||||
async findOneWithEmail(email: string): Promise<User> {
|
||||
const user = await this.userRepository.findOneWithEmail(email);
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
return user;
|
||||
}
|
||||
/************************************************************ */
|
||||
|
||||
async findOneWithPhone(phone: string): Promise<User | null> {
|
||||
const user = await this.userRepository.findOneWithPhone(phone);
|
||||
return user;
|
||||
}
|
||||
|
||||
/************************************************************ */
|
||||
|
||||
async createUser(registerDto: CompleteRegistrationDto, hashedPassword: string): Promise<User> {
|
||||
// const existEmail = await this.userRepository.findOneWithEmail(registerDto.email);
|
||||
// if (existEmail) throw new BadRequestException(UserMessage.EMAIL_EXIST);
|
||||
|
||||
const role = await this.roleRepository.findOneBy({ name: RoleEnum.USER });
|
||||
if (!role) throw new BadRequestException(UserMessage.ROLE_NOT_FOUND);
|
||||
|
||||
const user = this.userRepository.create({ ...registerDto, password: hashedPassword, role });
|
||||
return await this.userRepository.save(user);
|
||||
}
|
||||
/************************************************************ */
|
||||
|
||||
async checkValidity(checkDto: CheckValidityDTO) {
|
||||
const { type, value } = checkDto;
|
||||
const user = await this.userRepository.findOneBy({ [type]: value });
|
||||
if (user) throw new BadRequestException(UserMessage.USER_EXISTS);
|
||||
return { message: CommonMessage.VALID_FOR_CHOOSE };
|
||||
}
|
||||
|
||||
///****************************************************** */
|
||||
async roleSeeder() {
|
||||
const countRole = await this.roleRepository.count();
|
||||
if (countRole > 0) return;
|
||||
const createdRoles = await this.roleRepository.insert(roles);
|
||||
this.logger.log({ createdRoles });
|
||||
return createdRoles;
|
||||
}
|
||||
|
||||
async createUser(registerDto: CompleteRegistrationDto, hashedPassword: string): Promise<User> {
|
||||
const existEmail = await this.userRepository.findOneWithEmail(registerDto.email);
|
||||
if (existEmail) throw new BadRequestException(UserMessage.EMAIL_EXIST);
|
||||
|
||||
const user = this.userRepository.create({ ...registerDto, password: hashedPassword });
|
||||
return await this.userRepository.save(user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,31 @@
|
||||
import { Controller } from "@nestjs/common";
|
||||
import { ApiTags } from "@nestjs/swagger";
|
||||
import { Body, Controller, Get, Patch, Post } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
|
||||
import { CheckValidityDTO } from "./DTO/check-validity.dto";
|
||||
import { User } from "./entities/user.entity";
|
||||
import { UsersService } from "./providers/users.service";
|
||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||
import { UserDec } from "../../common/decorators/user.decorator";
|
||||
|
||||
@Controller("users")
|
||||
@ApiTags("users")
|
||||
export class UsersController {}
|
||||
export class UsersController {
|
||||
constructor(private usersService: UsersService) {}
|
||||
|
||||
@AuthGuards()
|
||||
@ApiOperation({ summary: "Get user profile" })
|
||||
@Get("me")
|
||||
getMe(@UserDec() user: User) {
|
||||
return this.usersService.getMe(user.id);
|
||||
}
|
||||
|
||||
@Patch("update-profile")
|
||||
updateProfile() {}
|
||||
|
||||
@AuthGuards()
|
||||
@ApiOperation({ summary: "Check validity of user field" })
|
||||
@Post("check-validity")
|
||||
checkValidity(@Body() checkValidityDto: CheckValidityDTO) {
|
||||
return this.usersService.checkValidity(checkValidityDto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
|
||||
import { Role } from "./entities/role.entity";
|
||||
import { User } from "./entities/user.entity";
|
||||
import { RoleRepository } from "./providers/roles.repository";
|
||||
import { UserRepository } from "./providers/users.repository";
|
||||
import { UsersService } from "./providers/users.service";
|
||||
import { UsersController } from "./users.controller";
|
||||
import { RoleRepository } from "./providers/roles.repository";
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User, Role])],
|
||||
@@ -15,8 +15,8 @@ import { RoleRepository } from "./providers/roles.repository";
|
||||
exports: [UsersService, TypeOrmModule],
|
||||
})
|
||||
export class UsersModule implements OnApplicationBootstrap {
|
||||
constructor(private userService: UsersService) {}
|
||||
// constructor(private userService: UsersService) {}
|
||||
async onApplicationBootstrap() {
|
||||
await this.userService.roleSeeder();
|
||||
// await this.userService.roleSeeder();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,4 +16,8 @@ export class CacheService {
|
||||
async del(key: string) {
|
||||
await this.cacheManager.del(key);
|
||||
}
|
||||
|
||||
async getTtl(key: string) {
|
||||
return await this.cacheManager.ttl(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,28 +12,33 @@ export class OTPService {
|
||||
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);
|
||||
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 verify(identifier: string, otp: string): Promise<boolean> {
|
||||
// // Here you would typically verify the OTP against stored value
|
||||
// // and check if it's not expired
|
||||
// return true; // Placeholder implementation
|
||||
// }
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { CacheService } from "./providers/cache.service";
|
||||
import { OTPService } from "./providers/otp.service";
|
||||
import { PasswordService } from "./providers/password.service";
|
||||
import { SmsService } from "./providers/sms.service";
|
||||
import { SmsConfigs } from "../../configs/sms.config";
|
||||
import { smsConfigs } from "../../configs/sms.config";
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
@@ -15,8 +15,8 @@ import { SmsConfigs } from "../../configs/sms.config";
|
||||
SmsService,
|
||||
{
|
||||
provide: SMS_CONFIG,
|
||||
useFactory: SmsConfigs().useFactory,
|
||||
inject: SmsConfigs().inject,
|
||||
useFactory: smsConfigs().useFactory,
|
||||
inject: smsConfigs().inject,
|
||||
},
|
||||
],
|
||||
exports: [SMS_CONFIG, OTPService, PasswordService, CacheService, SmsService],
|
||||
|
||||
Reference in New Issue
Block a user