feat: add refresh token and global exception filter
This commit is contained in:
+16
-1
@@ -9,8 +9,13 @@ import { HeaderResolver, I18nJsonLoader, I18nModule } from 'nestjs-i18n';
|
||||
import { AuthModule } from './modules/auth/auth.module';
|
||||
import { OtpModule } from './modules/otp/otp.module';
|
||||
import { HashingModule } from './common/modules/hashing/hashing.module';
|
||||
import { RedisModule } from './common/modules/redis/redis.module';
|
||||
import path from 'path';
|
||||
import jwtConfig from './config/jwt.config';
|
||||
import redisConfig from './config/redis.config';
|
||||
import { APP_FILTER } from '@nestjs/core';
|
||||
import { GlobalExceptionFilter } from './common/filters/global-exception.filter';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
|
||||
const ENV = process.env.NODE_ENV;
|
||||
|
||||
@@ -35,7 +40,7 @@ const ENV = process.env.NODE_ENV;
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath: !ENV ? '.env' : `.env.${ENV}`,
|
||||
load: [appConfig, databaseConfig, jwtConfig],
|
||||
load: [appConfig, databaseConfig, jwtConfig, redisConfig],
|
||||
validationSchema: environmentValidation,
|
||||
}),
|
||||
|
||||
@@ -55,6 +60,9 @@ const ENV = process.env.NODE_ENV;
|
||||
}),
|
||||
}),
|
||||
|
||||
// Schedule
|
||||
ScheduleModule.forRoot(),
|
||||
|
||||
/**
|
||||
* Local Modules
|
||||
*/
|
||||
@@ -62,6 +70,13 @@ const ENV = process.env.NODE_ENV;
|
||||
AuthModule,
|
||||
OtpModule,
|
||||
HashingModule,
|
||||
RedisModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: GlobalExceptionFilter,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||
|
||||
export class AppException extends HttpException {
|
||||
constructor(
|
||||
public messageKey: string,
|
||||
status: HttpStatus,
|
||||
public params?: Record<string, any>,
|
||||
) {
|
||||
super({ message: messageKey, params }, status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import {
|
||||
ArgumentsHost,
|
||||
Catch,
|
||||
ExceptionFilter,
|
||||
HttpException,
|
||||
} from '@nestjs/common';
|
||||
import type { Request, Response } from 'express';
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
|
||||
@Catch()
|
||||
export class GlobalExceptionFilter implements ExceptionFilter {
|
||||
constructor(
|
||||
/**
|
||||
* Inject I18n Service
|
||||
*/
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
catch(exception: any, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const request = ctx.getRequest<Request>();
|
||||
const response = ctx.getResponse<Response>();
|
||||
|
||||
const isHttpException = exception instanceof HttpException;
|
||||
|
||||
const status = isHttpException ? exception.getStatus() : 500;
|
||||
|
||||
const exceptionResponse = isHttpException
|
||||
? exception.getResponse()
|
||||
: 'common.errors.internalServerError';
|
||||
|
||||
const messageKey =
|
||||
typeof exceptionResponse === 'string'
|
||||
? exceptionResponse
|
||||
: (exceptionResponse as any).message;
|
||||
|
||||
const message = this.i18nService.translate(messageKey as string, {
|
||||
args: (exceptionResponse as any).params,
|
||||
});
|
||||
|
||||
response.status(status).json({
|
||||
success: false,
|
||||
statusCode: status,
|
||||
message,
|
||||
path: request.url,
|
||||
timestamp: new Date().toISOString(),
|
||||
...(process.env.NODE_ENV !== 'production' && {
|
||||
raw: exceptionResponse,
|
||||
stack: exception instanceof Error ? exception.stack : undefined,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const REDIS = 'REDIS_CLIENT';
|
||||
@@ -0,0 +1,41 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { REDIS } from '../constants/redis.constants';
|
||||
import Redis from 'ioredis';
|
||||
|
||||
@Injectable()
|
||||
export class RedisService {
|
||||
constructor(
|
||||
/**
|
||||
* Inject Redis
|
||||
*/
|
||||
@Inject(REDIS)
|
||||
private readonly redis: Redis,
|
||||
) {}
|
||||
|
||||
get client() {
|
||||
return this.redis;
|
||||
}
|
||||
|
||||
async set(key: string, val: string, ttl?: number) {
|
||||
if (ttl) {
|
||||
await this.redis.set(key, val, 'EX', ttl);
|
||||
} else {
|
||||
await this.redis.set(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
async get(key: string) {
|
||||
return await this.redis.get(key);
|
||||
}
|
||||
|
||||
async del(key: string) {
|
||||
return await this.redis.del(key);
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.redis.quit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RedisService } from './providers/redis.service';
|
||||
import { REDIS } from './constants/redis.constants';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import Redis from 'ioredis';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: REDIS,
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => {
|
||||
return new Redis({
|
||||
host: configService.get<string>('redis.host'),
|
||||
port: configService.get<number>('redis.port'),
|
||||
});
|
||||
},
|
||||
},
|
||||
RedisService,
|
||||
],
|
||||
exports: [RedisService],
|
||||
})
|
||||
export class RedisModule {}
|
||||
@@ -17,4 +17,6 @@ export default Joi.object({
|
||||
JWT_REFRESH_TTL: Joi.number().required(),
|
||||
JWT_ISSUER: Joi.string().required(),
|
||||
JWT_AUDIENCE: Joi.string().required(),
|
||||
REDIS_HOST: Joi.string().required(),
|
||||
REDIS_PORT: Joi.number().port().default(6379),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { registerAs } from '@nestjs/config';
|
||||
|
||||
export default registerAs('redis', () => ({
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: parseInt(process.env.REDIS_PORT || '6379', 10),
|
||||
}));
|
||||
@@ -3,5 +3,15 @@
|
||||
"phone": "Phone",
|
||||
"password": "Password",
|
||||
"otp": "Otp"
|
||||
},
|
||||
"errors": {
|
||||
"wrongPhoneOrPass": "Entered phone number or password is wrong.",
|
||||
"unauthorized": "Unauthorized access",
|
||||
"tokenReuse": "Token reuse detected.",
|
||||
"invalidOTP": "OTP is invalid or expired."
|
||||
},
|
||||
"messages": {
|
||||
"otpSent": "OTP sent to {phone}.",
|
||||
"sendPass": "Please send password."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"errors": {
|
||||
"internalServerError": "Internal Server Error",
|
||||
"tooManyRequests": "Too many requests! Try again after {minutes} min.",
|
||||
"requestTimeout": "Unable to process your request at the moment."
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"password": "Password"
|
||||
},
|
||||
"errors": {
|
||||
"passwordPattern": "Password should contain atleast 8 characters, including letters, numbers and symbols."
|
||||
"passwordPattern": "Password should contain atleast 8 characters, including letters, numbers and symbols.",
|
||||
"userNotExist": "User does not exist."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,5 +3,15 @@
|
||||
"phone": "شماره موبایل",
|
||||
"password": "رمز عبور",
|
||||
"otp": "کد تایید"
|
||||
},
|
||||
"errors": {
|
||||
"wrongPhoneOrPass": "شماره موبایل یا رمز عبور اشتباه است.",
|
||||
"unauthorized": "دسترسی غیر مجاز",
|
||||
"tokenReuse": "توکن قبلا استفاده شده.",
|
||||
"invalidOTP": "کد تایید اشتباه است یا منقضی شده."
|
||||
},
|
||||
"messages": {
|
||||
"otpSent": "کد تایید به شماره {phone} ارسال شد.",
|
||||
"sendPass": "لطفا رمز عبور را ارسال کنید."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"errors": {
|
||||
"internalServerError": "خطایی رخ داد",
|
||||
"tooManyRequests": "تعداد درخواست ها بیشتر از حد مجاز است! لطفا بعد از {minutes} دقیقه دوباره امتحان کنید.",
|
||||
"requestTimeout": "امکان پردازش درخواست شما در این لحظه وجود ندارد."
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"password": "رمز عبور"
|
||||
},
|
||||
"errors": {
|
||||
"passwordPattern": "رمز عبور باید حداقل شامل 8 کاراکتر شامل حروف، اعداد و نشانه ها باشد."
|
||||
"passwordPattern": "رمز عبور باید حداقل شامل 8 کاراکتر شامل حروف، اعداد و نشانه ها باشد.",
|
||||
"userNotExist": "کاربر مورد نظر وجود ندارد."
|
||||
}
|
||||
}
|
||||
|
||||
+11
-2
@@ -1,6 +1,6 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { I18nValidationPipe, I18nValidationExceptionFilter } from 'nestjs-i18n';
|
||||
import { I18nValidationExceptionFilter, I18nValidationPipe } from 'nestjs-i18n';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
|
||||
async function bootstrap() {
|
||||
@@ -17,7 +17,16 @@ async function bootstrap() {
|
||||
}),
|
||||
);
|
||||
|
||||
app.useGlobalFilters(new I18nValidationExceptionFilter());
|
||||
app.useGlobalFilters(
|
||||
new I18nValidationExceptionFilter({
|
||||
errorFormatter: (errors) => {
|
||||
return errors.map((err) => ({
|
||||
field: err.property,
|
||||
errors: Object.values(err.constraints ?? {}),
|
||||
}));
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('Borna API')
|
||||
|
||||
@@ -3,8 +3,9 @@ import { LoginDTO } from './dtos/login.dto';
|
||||
import { AuthService } from './providers/auth.service';
|
||||
import { VerifyOtpDTO } from './dtos/verify-otp.dto';
|
||||
import { Public } from './decorators/public.decorator';
|
||||
import { RefreshTokenDTO } from './dtos/refresh-token.dto';
|
||||
|
||||
@Controller('users')
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(
|
||||
/**
|
||||
@@ -26,4 +27,11 @@ export class AuthController {
|
||||
public async verifyOTP(@Body() verifyOtpDto: VerifyOtpDTO) {
|
||||
return await this.authService.verifyOTP(verifyOtpDto);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('refresh')
|
||||
@HttpCode(200)
|
||||
public async refreshToken(@Body() refreshTokenDto: RefreshTokenDTO) {
|
||||
return await this.authService.refreshToken(refreshTokenDto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,16 @@ import { VerifyOTPProvider } from './providers/verify-otp.provider';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { GlobalAuthGuard } from './guards/global-auth.guard';
|
||||
import { RefreshTokensProvider } from './providers/refresh-tokens.provider';
|
||||
import { RedisModule } from '@/common/modules/redis/redis.module';
|
||||
import { AuthRedisProvider } from './providers/auth-redis.provider';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UsersModule,
|
||||
OtpModule,
|
||||
HashingModule,
|
||||
RedisModule,
|
||||
JwtModule.registerAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
@@ -37,6 +41,8 @@ import { GlobalAuthGuard } from './guards/global-auth.guard';
|
||||
LoginProvider,
|
||||
VerifyOTPProvider,
|
||||
JwtStrategy,
|
||||
RefreshTokensProvider,
|
||||
AuthRedisProvider,
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: GlobalAuthGuard,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class RefreshTokenDTO {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
refreshToken: string;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
POST http://localhost:3000/users/login
|
||||
POST http://localhost:3000/auth/login
|
||||
Content-Type: application/json
|
||||
lang: fa
|
||||
|
||||
{
|
||||
"phone": "09121111111"
|
||||
"phone": "09121111114"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
POST http://localhost:3000/auth/refresh
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjksImp0aSI6IjljOTY5OWRhLWI0NTgtNDc0Ny04YTRkLTQyODQxMDllOTVkZCIsImlhdCI6MTc3ODMwNDMyNSwiZXhwIjoxNzc5MTY4MzI1LCJhdWQiOiJsb2NhbGhvc3QiLCJpc3MiOiJsb2NhbGhvc3QifQ.oiHuj3vJRQl7rG8H0fFEfU9cG91KrLGdXqNAYFGTXVA"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
POST http://localhost:3000/users/verify-otp
|
||||
POST http://localhost:3000/auth/verify-otp
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"phone": "09121111111",
|
||||
"otp": "83793"
|
||||
"phone": "09121111114",
|
||||
"otp": "99696"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { RedisService } from '@/common/modules/redis/providers/redis.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AuthRedisProvider {
|
||||
constructor(
|
||||
/**
|
||||
* Injecting Redis Service
|
||||
*/
|
||||
private readonly redisService: RedisService,
|
||||
) {}
|
||||
|
||||
async storeRefreshToken(jti: string, userId: number, ttl: number) {
|
||||
await this.redisService.set(`rt:${jti}`, userId.toString(), ttl);
|
||||
}
|
||||
|
||||
async hasRefreshToken(jti: string) {
|
||||
return !!(await this.redisService.get(`rt:${jti}`));
|
||||
}
|
||||
|
||||
async revokeRefreshToken(jti: string) {
|
||||
await this.redisService.del(`rt:${jti}`);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import { LoginDTO } from '../dtos/login.dto';
|
||||
import { LoginProvider } from './login.provider';
|
||||
import { VerifyOtpDTO } from '../dtos/verify-otp.dto';
|
||||
import { VerifyOTPProvider } from './verify-otp.provider';
|
||||
import { RefreshTokenDTO } from '../dtos/refresh-token.dto';
|
||||
import { RefreshTokensProvider } from './refresh-tokens.provider';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@@ -16,6 +18,11 @@ export class AuthService {
|
||||
* Inject Verify OTP Provider
|
||||
*/
|
||||
private readonly verifyOTPProvider: VerifyOTPProvider,
|
||||
|
||||
/**
|
||||
* Inject RefreshToken Provider
|
||||
*/
|
||||
private readonly refreshTokensProvider: RefreshTokensProvider,
|
||||
) {}
|
||||
|
||||
public async logIn(loginDto: LoginDTO) {
|
||||
@@ -25,4 +32,8 @@ export class AuthService {
|
||||
public async verifyOTP(verifyOtpDto: VerifyOtpDTO) {
|
||||
return await this.verifyOTPProvider.verifyOTP(verifyOtpDto);
|
||||
}
|
||||
|
||||
public async refreshToken(refreshTokenDto: RefreshTokenDTO) {
|
||||
return await this.refreshTokensProvider.refreshTokens(refreshTokenDto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
AccessTokenPayload,
|
||||
RefreshTokenPayload,
|
||||
} from '../interfaces/jwt.interface';
|
||||
import { AuthRedisProvider } from './auth-redis.provider';
|
||||
|
||||
@Injectable()
|
||||
export class GenerateTokenProvider {
|
||||
@@ -21,6 +22,11 @@ export class GenerateTokenProvider {
|
||||
*/
|
||||
@Inject(jwtConfig.KEY)
|
||||
private readonly jwtConfiguration: ConfigType<typeof jwtConfig>,
|
||||
|
||||
/**
|
||||
* Inject AuthRedis Provider
|
||||
*/
|
||||
private readonly authRedisProvider: AuthRedisProvider,
|
||||
) {}
|
||||
|
||||
public async signToken<T>(
|
||||
@@ -60,6 +66,13 @@ export class GenerateTokenProvider {
|
||||
this.jwtConfiguration.refresh.secret,
|
||||
{ jti },
|
||||
),
|
||||
|
||||
// Store Refresh JTI in redis
|
||||
this.authRedisProvider.storeRefreshToken(
|
||||
jti,
|
||||
user.id,
|
||||
this.jwtConfiguration.refresh.expiresIn,
|
||||
),
|
||||
]);
|
||||
|
||||
return { access, refresh };
|
||||
|
||||
@@ -5,6 +5,7 @@ import { OtpService } from '@/modules/otp/providers/otp.service';
|
||||
import { HashingProvider } from '@/common/modules/hashing/providers/hashing.provider';
|
||||
import { GenerateTokenProvider } from './generate-token.provider';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
|
||||
@Injectable()
|
||||
export class LoginProvider {
|
||||
@@ -28,6 +29,11 @@ export class LoginProvider {
|
||||
* Inject GenerateToken Provider
|
||||
*/
|
||||
private readonly generateTokenProvider: GenerateTokenProvider,
|
||||
|
||||
/**
|
||||
* Inject I18n Service
|
||||
*/
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
public async logIn(loginDto: LoginDTO) {
|
||||
@@ -38,9 +44,13 @@ export class LoginProvider {
|
||||
// TODO: Send OTP
|
||||
const otpCode = await this.otpService.createOTP(phone);
|
||||
|
||||
const message = this.i18nService.translate('auth.messages.otpSent', {
|
||||
args: { phone },
|
||||
});
|
||||
|
||||
return {
|
||||
newUser: true,
|
||||
message: `Otp sent to ${phone}`,
|
||||
message,
|
||||
// Just for development
|
||||
code: otpCode,
|
||||
};
|
||||
@@ -49,7 +59,7 @@ export class LoginProvider {
|
||||
if (!password) {
|
||||
return {
|
||||
newUser: false,
|
||||
message: 'Please send password',
|
||||
message: this.i18nService.translate('auth.messages.sendPass'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,8 +73,6 @@ export class LoginProvider {
|
||||
return await this.generateTokenProvider.generateTokens(user, jti);
|
||||
}
|
||||
|
||||
throw new UnauthorizedException(
|
||||
'Entered phone number or password is wrong.',
|
||||
);
|
||||
throw new UnauthorizedException('auth.errors.wrongPhoneOrPass');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthRedisProvider } from './auth-redis.provider';
|
||||
import { RefreshTokenDTO } from '../dtos/refresh-token.dto';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import type { ConfigType } from '@nestjs/config';
|
||||
import jwtConfig from '@/config/jwt.config';
|
||||
import { RefreshTokenPayload } from '../interfaces/jwt.interface';
|
||||
import { UsersService } from '@/modules/users/providers/users.service';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { GenerateTokenProvider } from './generate-token.provider';
|
||||
|
||||
@Injectable()
|
||||
export class RefreshTokensProvider {
|
||||
constructor(
|
||||
/**
|
||||
* Inject JwtService
|
||||
*/
|
||||
private readonly jwtService: JwtService,
|
||||
|
||||
/**
|
||||
* Inject jwtConfig
|
||||
*/
|
||||
@Inject(jwtConfig.KEY)
|
||||
private readonly jwtConfiguration: ConfigType<typeof jwtConfig>,
|
||||
|
||||
/**
|
||||
* Inject Users Service
|
||||
*/
|
||||
private readonly usersService: UsersService,
|
||||
|
||||
/**
|
||||
* Inject AuthRedis Provider
|
||||
*/
|
||||
private readonly authRedisProvider: AuthRedisProvider,
|
||||
|
||||
/**
|
||||
* Inject GenerateToken Provider
|
||||
*/
|
||||
private readonly generateTokenProvider: GenerateTokenProvider,
|
||||
) {}
|
||||
|
||||
public async refreshTokens(refreshTokenDto: RefreshTokenDTO) {
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync<RefreshTokenPayload>(
|
||||
refreshTokenDto.refreshToken,
|
||||
{
|
||||
secret: this.jwtConfiguration.refresh.secret,
|
||||
issuer: this.jwtConfiguration.issuer,
|
||||
audience: this.jwtConfiguration.audience,
|
||||
},
|
||||
);
|
||||
|
||||
const user = await this.usersService.findOneById(payload.sub);
|
||||
|
||||
if (!payload.jti) {
|
||||
throw new UnauthorizedException('auth.errors.unauthorized');
|
||||
}
|
||||
|
||||
const existsInRedis = await this.authRedisProvider.hasRefreshToken(
|
||||
payload.jti,
|
||||
);
|
||||
|
||||
if (!existsInRedis) {
|
||||
throw new UnauthorizedException('auth.errors.tokenReuse');
|
||||
}
|
||||
|
||||
await this.authRedisProvider.revokeRefreshToken(payload.jti);
|
||||
|
||||
const newJti = randomUUID();
|
||||
return await this.generateTokenProvider.generateTokens(user, newJti);
|
||||
} catch (err) {
|
||||
throw new UnauthorizedException('auth.errors.unauthorized', {
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
const user = await this.usersService.findOneByPhone(payload.phone);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException();
|
||||
throw new UnauthorizedException('auth.errors.unauthorized');
|
||||
}
|
||||
|
||||
return user;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import {
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Injectable,
|
||||
RequestTimeoutException,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { LessThan, Repository } from 'typeorm';
|
||||
import { LessThan, MoreThanOrEqual, Repository } from 'typeorm';
|
||||
import { OTP } from '../otp.entity';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { HashingProvider } from '@/common/modules/hashing/providers/hashing.provider';
|
||||
import { AppException } from '@/common/exceptions/app.exception';
|
||||
|
||||
@Injectable()
|
||||
export class OtpService {
|
||||
@@ -44,9 +44,10 @@ export class OtpService {
|
||||
});
|
||||
|
||||
if (lastOTP && Date.now() - lastOTP.createdAt.getTime() < 60_000) {
|
||||
throw new HttpException(
|
||||
'Too many requests! Try again after 1 min.',
|
||||
throw new AppException(
|
||||
'common.errors.tooManyRequests',
|
||||
HttpStatus.TOO_MANY_REQUESTS,
|
||||
{ minutes: 1 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,26 +69,22 @@ export class OtpService {
|
||||
|
||||
try {
|
||||
otp = await this.otpRepository.findOne({
|
||||
where: { phone, used: false },
|
||||
where: { phone, used: false, expiresAt: MoreThanOrEqual(new Date()) },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
} catch (err) {
|
||||
throw new RequestTimeoutException(
|
||||
'Unable to process your request at the moment. Please try again later.',
|
||||
{ cause: err, description: 'Error connecting to the database' },
|
||||
);
|
||||
throw new RequestTimeoutException('common.errors.requestTimeout', {
|
||||
cause: err,
|
||||
description: 'Error connecting to the database',
|
||||
});
|
||||
}
|
||||
|
||||
if (!otp) throw new UnauthorizedException('Invalid OTP');
|
||||
|
||||
if (otp.expiresAt < new Date()) {
|
||||
throw new UnauthorizedException('OTP expired');
|
||||
}
|
||||
if (!otp) throw new UnauthorizedException('auth.errors.invalidOTP');
|
||||
|
||||
const isOTPCorrect = await this.hashingProvider.compare(code, otp.code);
|
||||
|
||||
if (!isOTPCorrect) {
|
||||
throw new UnauthorizedException('Invalid OTP');
|
||||
throw new UnauthorizedException('auth.errors.invalidOTP');
|
||||
}
|
||||
|
||||
otp.used = true;
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
GET http://localhost:3000/users/profile
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjQsInBob25lIjoiMDkxMjYxMTcwMTgiLCJpYXQiOjE3NzIxMjc1MTgsImV4cCI6MTc3MjEzMTExOCwiYXVkIjoibG9jYWxob3N0IiwiaXNzIjoibG9jYWxob3N0In0.oaxzSd0DDakCDMiW129Ou3MJh0oT0zJTFerZEjnQ7Ug
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjYsInBob25lIjoiMDkxMjExMTExMTEiLCJpYXQiOjE3Nzc5NzU3NjQsImV4cCI6MTc3Nzk3OTM2NCwiYXVkIjoibG9jYWxob3N0IiwiaXNzIjoibG9jYWxob3N0In0.m3sIOApct0GYBNQ7TuWjf7wo2ZCvu-3vPYeWEHTuHMc
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Injectable, RequestTimeoutException } from '@nestjs/common';
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
RequestTimeoutException,
|
||||
} from '@nestjs/common';
|
||||
import { Repository } from 'typeorm';
|
||||
import { User } from '../user.entity';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
@@ -13,14 +17,32 @@ export class UsersService {
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
public async findOneById(id: number) {
|
||||
let user: User | null;
|
||||
try {
|
||||
user = await this.userRepository.findOneById(id);
|
||||
} catch (err) {
|
||||
throw new RequestTimeoutException('common.errors.requestTimeout', {
|
||||
cause: err,
|
||||
description: 'Error connecting to the database',
|
||||
});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new BadRequestException('users.errors.userNotExist');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
public async findOneByPhone(phone: string) {
|
||||
try {
|
||||
return await this.userRepository.findOneBy({ phone });
|
||||
} catch (err) {
|
||||
throw new RequestTimeoutException(
|
||||
'Unable to process your request at the moment. Please try again later.',
|
||||
{ cause: err, description: 'Error connecting to the database' },
|
||||
);
|
||||
throw new RequestTimeoutException('common.errors.requestTimeout', {
|
||||
cause: err,
|
||||
description: 'Error connecting to the database',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +52,10 @@ export class UsersService {
|
||||
try {
|
||||
await this.userRepository.save(newUser);
|
||||
} catch (err) {
|
||||
throw new RequestTimeoutException(
|
||||
'Unable to process your request at the moment. Please try again later.',
|
||||
{ cause: err, description: 'Error connecting to the database' },
|
||||
);
|
||||
throw new RequestTimeoutException('common.errors.requestTimeout', {
|
||||
cause: err,
|
||||
description: 'Error connecting to the database',
|
||||
});
|
||||
}
|
||||
|
||||
return newUser;
|
||||
|
||||
Reference in New Issue
Block a user