feat: add refresh token and global exception filter
This commit is contained in:
Generated
+88
@@ -21,6 +21,7 @@
|
|||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.3",
|
"class-validator": "^0.14.3",
|
||||||
|
"ioredis": "^5.10.1",
|
||||||
"joi": "^18.0.2",
|
"joi": "^18.0.2",
|
||||||
"nestjs-i18n": "^10.6.0",
|
"nestjs-i18n": "^10.6.0",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
@@ -1378,6 +1379,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ioredis/commands": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
@@ -4417,6 +4424,15 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cluster-key-slot": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/co": {
|
"node_modules/co": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||||
@@ -4809,6 +4825,15 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/denque": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@@ -6203,6 +6228,30 @@
|
|||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/ioredis": {
|
||||||
|
"version": "5.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz",
|
||||||
|
"integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@ioredis/commands": "1.5.1",
|
||||||
|
"cluster-key-slot": "^1.1.0",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"denque": "^2.1.0",
|
||||||
|
"lodash.defaults": "^4.2.0",
|
||||||
|
"lodash.isarguments": "^3.1.0",
|
||||||
|
"redis-errors": "^1.2.0",
|
||||||
|
"redis-parser": "^3.0.0",
|
||||||
|
"standard-as-callback": "^2.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.22.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/ioredis"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
@@ -7406,11 +7455,23 @@
|
|||||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.defaults": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.includes": {
|
"node_modules/lodash.includes": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://mirror-npm.runflare.com/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
"resolved": "https://mirror-npm.runflare.com/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
|
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.isarguments": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.isboolean": {
|
"node_modules/lodash.isboolean": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://mirror-npm.runflare.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
"resolved": "https://mirror-npm.runflare.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||||
@@ -8732,6 +8793,27 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redis-errors": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/redis-parser": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"redis-errors": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reflect-metadata": {
|
"node_modules/reflect-metadata": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||||
@@ -9230,6 +9312,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/standard-as-callback": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.3",
|
"class-validator": "^0.14.3",
|
||||||
|
"ioredis": "^5.10.1",
|
||||||
"joi": "^18.0.2",
|
"joi": "^18.0.2",
|
||||||
"nestjs-i18n": "^10.6.0",
|
"nestjs-i18n": "^10.6.0",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
|
|||||||
+16
-1
@@ -9,8 +9,13 @@ import { HeaderResolver, I18nJsonLoader, I18nModule } from 'nestjs-i18n';
|
|||||||
import { AuthModule } from './modules/auth/auth.module';
|
import { AuthModule } from './modules/auth/auth.module';
|
||||||
import { OtpModule } from './modules/otp/otp.module';
|
import { OtpModule } from './modules/otp/otp.module';
|
||||||
import { HashingModule } from './common/modules/hashing/hashing.module';
|
import { HashingModule } from './common/modules/hashing/hashing.module';
|
||||||
|
import { RedisModule } from './common/modules/redis/redis.module';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import jwtConfig from './config/jwt.config';
|
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;
|
const ENV = process.env.NODE_ENV;
|
||||||
|
|
||||||
@@ -35,7 +40,7 @@ const ENV = process.env.NODE_ENV;
|
|||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
envFilePath: !ENV ? '.env' : `.env.${ENV}`,
|
envFilePath: !ENV ? '.env' : `.env.${ENV}`,
|
||||||
load: [appConfig, databaseConfig, jwtConfig],
|
load: [appConfig, databaseConfig, jwtConfig, redisConfig],
|
||||||
validationSchema: environmentValidation,
|
validationSchema: environmentValidation,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -55,6 +60,9 @@ const ENV = process.env.NODE_ENV;
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// Schedule
|
||||||
|
ScheduleModule.forRoot(),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Local Modules
|
* Local Modules
|
||||||
*/
|
*/
|
||||||
@@ -62,6 +70,13 @@ const ENV = process.env.NODE_ENV;
|
|||||||
AuthModule,
|
AuthModule,
|
||||||
OtpModule,
|
OtpModule,
|
||||||
HashingModule,
|
HashingModule,
|
||||||
|
RedisModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_FILTER,
|
||||||
|
useClass: GlobalExceptionFilter,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
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_REFRESH_TTL: Joi.number().required(),
|
||||||
JWT_ISSUER: Joi.string().required(),
|
JWT_ISSUER: Joi.string().required(),
|
||||||
JWT_AUDIENCE: 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",
|
"phone": "Phone",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"otp": "Otp"
|
"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"
|
"password": "Password"
|
||||||
},
|
},
|
||||||
"errors": {
|
"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": "شماره موبایل",
|
"phone": "شماره موبایل",
|
||||||
"password": "رمز عبور",
|
"password": "رمز عبور",
|
||||||
"otp": "کد تایید"
|
"otp": "کد تایید"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"wrongPhoneOrPass": "شماره موبایل یا رمز عبور اشتباه است.",
|
||||||
|
"unauthorized": "دسترسی غیر مجاز",
|
||||||
|
"tokenReuse": "توکن قبلا استفاده شده.",
|
||||||
|
"invalidOTP": "کد تایید اشتباه است یا منقضی شده."
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"otpSent": "کد تایید به شماره {phone} ارسال شد.",
|
||||||
|
"sendPass": "لطفا رمز عبور را ارسال کنید."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"errors": {
|
||||||
|
"internalServerError": "خطایی رخ داد",
|
||||||
|
"tooManyRequests": "تعداد درخواست ها بیشتر از حد مجاز است! لطفا بعد از {minutes} دقیقه دوباره امتحان کنید.",
|
||||||
|
"requestTimeout": "امکان پردازش درخواست شما در این لحظه وجود ندارد."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"password": "رمز عبور"
|
"password": "رمز عبور"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"passwordPattern": "رمز عبور باید حداقل شامل 8 کاراکتر شامل حروف، اعداد و نشانه ها باشد."
|
"passwordPattern": "رمز عبور باید حداقل شامل 8 کاراکتر شامل حروف، اعداد و نشانه ها باشد.",
|
||||||
|
"userNotExist": "کاربر مورد نظر وجود ندارد."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-2
@@ -1,6 +1,6 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { I18nValidationPipe, I18nValidationExceptionFilter } from 'nestjs-i18n';
|
import { I18nValidationExceptionFilter, I18nValidationPipe } from 'nestjs-i18n';
|
||||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||||
|
|
||||||
async function bootstrap() {
|
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()
|
const config = new DocumentBuilder()
|
||||||
.setTitle('Borna API')
|
.setTitle('Borna API')
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { LoginDTO } from './dtos/login.dto';
|
|||||||
import { AuthService } from './providers/auth.service';
|
import { AuthService } from './providers/auth.service';
|
||||||
import { VerifyOtpDTO } from './dtos/verify-otp.dto';
|
import { VerifyOtpDTO } from './dtos/verify-otp.dto';
|
||||||
import { Public } from './decorators/public.decorator';
|
import { Public } from './decorators/public.decorator';
|
||||||
|
import { RefreshTokenDTO } from './dtos/refresh-token.dto';
|
||||||
|
|
||||||
@Controller('users')
|
@Controller('auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(
|
constructor(
|
||||||
/**
|
/**
|
||||||
@@ -26,4 +27,11 @@ export class AuthController {
|
|||||||
public async verifyOTP(@Body() verifyOtpDto: VerifyOtpDTO) {
|
public async verifyOTP(@Body() verifyOtpDto: VerifyOtpDTO) {
|
||||||
return await this.authService.verifyOTP(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 { JwtStrategy } from './strategies/jwt.strategy';
|
||||||
import { APP_GUARD } from '@nestjs/core';
|
import { APP_GUARD } from '@nestjs/core';
|
||||||
import { GlobalAuthGuard } from './guards/global-auth.guard';
|
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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
UsersModule,
|
UsersModule,
|
||||||
OtpModule,
|
OtpModule,
|
||||||
HashingModule,
|
HashingModule,
|
||||||
|
RedisModule,
|
||||||
JwtModule.registerAsync({
|
JwtModule.registerAsync({
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
useFactory: (configService: ConfigService) => ({
|
useFactory: (configService: ConfigService) => ({
|
||||||
@@ -37,6 +41,8 @@ import { GlobalAuthGuard } from './guards/global-auth.guard';
|
|||||||
LoginProvider,
|
LoginProvider,
|
||||||
VerifyOTPProvider,
|
VerifyOTPProvider,
|
||||||
JwtStrategy,
|
JwtStrategy,
|
||||||
|
RefreshTokensProvider,
|
||||||
|
AuthRedisProvider,
|
||||||
{
|
{
|
||||||
provide: APP_GUARD,
|
provide: APP_GUARD,
|
||||||
useClass: GlobalAuthGuard,
|
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
|
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
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"phone": "09121111111",
|
"phone": "09121111114",
|
||||||
"otp": "83793"
|
"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 { LoginProvider } from './login.provider';
|
||||||
import { VerifyOtpDTO } from '../dtos/verify-otp.dto';
|
import { VerifyOtpDTO } from '../dtos/verify-otp.dto';
|
||||||
import { VerifyOTPProvider } from './verify-otp.provider';
|
import { VerifyOTPProvider } from './verify-otp.provider';
|
||||||
|
import { RefreshTokenDTO } from '../dtos/refresh-token.dto';
|
||||||
|
import { RefreshTokensProvider } from './refresh-tokens.provider';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@@ -16,6 +18,11 @@ export class AuthService {
|
|||||||
* Inject Verify OTP Provider
|
* Inject Verify OTP Provider
|
||||||
*/
|
*/
|
||||||
private readonly verifyOTPProvider: VerifyOTPProvider,
|
private readonly verifyOTPProvider: VerifyOTPProvider,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject RefreshToken Provider
|
||||||
|
*/
|
||||||
|
private readonly refreshTokensProvider: RefreshTokensProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async logIn(loginDto: LoginDTO) {
|
public async logIn(loginDto: LoginDTO) {
|
||||||
@@ -25,4 +32,8 @@ export class AuthService {
|
|||||||
public async verifyOTP(verifyOtpDto: VerifyOtpDTO) {
|
public async verifyOTP(verifyOtpDto: VerifyOtpDTO) {
|
||||||
return await this.verifyOTPProvider.verifyOTP(verifyOtpDto);
|
return await this.verifyOTPProvider.verifyOTP(verifyOtpDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async refreshToken(refreshTokenDto: RefreshTokenDTO) {
|
||||||
|
return await this.refreshTokensProvider.refreshTokens(refreshTokenDto);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
AccessTokenPayload,
|
AccessTokenPayload,
|
||||||
RefreshTokenPayload,
|
RefreshTokenPayload,
|
||||||
} from '../interfaces/jwt.interface';
|
} from '../interfaces/jwt.interface';
|
||||||
|
import { AuthRedisProvider } from './auth-redis.provider';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GenerateTokenProvider {
|
export class GenerateTokenProvider {
|
||||||
@@ -21,6 +22,11 @@ export class GenerateTokenProvider {
|
|||||||
*/
|
*/
|
||||||
@Inject(jwtConfig.KEY)
|
@Inject(jwtConfig.KEY)
|
||||||
private readonly jwtConfiguration: ConfigType<typeof jwtConfig>,
|
private readonly jwtConfiguration: ConfigType<typeof jwtConfig>,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject AuthRedis Provider
|
||||||
|
*/
|
||||||
|
private readonly authRedisProvider: AuthRedisProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async signToken<T>(
|
public async signToken<T>(
|
||||||
@@ -60,6 +66,13 @@ export class GenerateTokenProvider {
|
|||||||
this.jwtConfiguration.refresh.secret,
|
this.jwtConfiguration.refresh.secret,
|
||||||
{ jti },
|
{ jti },
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Store Refresh JTI in redis
|
||||||
|
this.authRedisProvider.storeRefreshToken(
|
||||||
|
jti,
|
||||||
|
user.id,
|
||||||
|
this.jwtConfiguration.refresh.expiresIn,
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { access, refresh };
|
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 { HashingProvider } from '@/common/modules/hashing/providers/hashing.provider';
|
||||||
import { GenerateTokenProvider } from './generate-token.provider';
|
import { GenerateTokenProvider } from './generate-token.provider';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
|
import { I18nService } from 'nestjs-i18n';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LoginProvider {
|
export class LoginProvider {
|
||||||
@@ -28,6 +29,11 @@ export class LoginProvider {
|
|||||||
* Inject GenerateToken Provider
|
* Inject GenerateToken Provider
|
||||||
*/
|
*/
|
||||||
private readonly generateTokenProvider: GenerateTokenProvider,
|
private readonly generateTokenProvider: GenerateTokenProvider,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject I18n Service
|
||||||
|
*/
|
||||||
|
private readonly i18nService: I18nService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async logIn(loginDto: LoginDTO) {
|
public async logIn(loginDto: LoginDTO) {
|
||||||
@@ -38,9 +44,13 @@ export class LoginProvider {
|
|||||||
// TODO: Send OTP
|
// TODO: Send OTP
|
||||||
const otpCode = await this.otpService.createOTP(phone);
|
const otpCode = await this.otpService.createOTP(phone);
|
||||||
|
|
||||||
|
const message = this.i18nService.translate('auth.messages.otpSent', {
|
||||||
|
args: { phone },
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newUser: true,
|
newUser: true,
|
||||||
message: `Otp sent to ${phone}`,
|
message,
|
||||||
// Just for development
|
// Just for development
|
||||||
code: otpCode,
|
code: otpCode,
|
||||||
};
|
};
|
||||||
@@ -49,7 +59,7 @@ export class LoginProvider {
|
|||||||
if (!password) {
|
if (!password) {
|
||||||
return {
|
return {
|
||||||
newUser: false,
|
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);
|
return await this.generateTokenProvider.generateTokens(user, jti);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new UnauthorizedException(
|
throw new UnauthorizedException('auth.errors.wrongPhoneOrPass');
|
||||||
'Entered phone number or password is wrong.',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
const user = await this.usersService.findOneByPhone(payload.phone);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException('auth.errors.unauthorized');
|
||||||
}
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
HttpException,
|
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Injectable,
|
Injectable,
|
||||||
RequestTimeoutException,
|
RequestTimeoutException,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { LessThan, Repository } from 'typeorm';
|
import { LessThan, MoreThanOrEqual, Repository } from 'typeorm';
|
||||||
import { OTP } from '../otp.entity';
|
import { OTP } from '../otp.entity';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Cron } from '@nestjs/schedule';
|
import { Cron } from '@nestjs/schedule';
|
||||||
import { HashingProvider } from '@/common/modules/hashing/providers/hashing.provider';
|
import { HashingProvider } from '@/common/modules/hashing/providers/hashing.provider';
|
||||||
|
import { AppException } from '@/common/exceptions/app.exception';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OtpService {
|
export class OtpService {
|
||||||
@@ -44,9 +44,10 @@ export class OtpService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (lastOTP && Date.now() - lastOTP.createdAt.getTime() < 60_000) {
|
if (lastOTP && Date.now() - lastOTP.createdAt.getTime() < 60_000) {
|
||||||
throw new HttpException(
|
throw new AppException(
|
||||||
'Too many requests! Try again after 1 min.',
|
'common.errors.tooManyRequests',
|
||||||
HttpStatus.TOO_MANY_REQUESTS,
|
HttpStatus.TOO_MANY_REQUESTS,
|
||||||
|
{ minutes: 1 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,26 +69,22 @@ export class OtpService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
otp = await this.otpRepository.findOne({
|
otp = await this.otpRepository.findOne({
|
||||||
where: { phone, used: false },
|
where: { phone, used: false, expiresAt: MoreThanOrEqual(new Date()) },
|
||||||
order: { createdAt: 'DESC' },
|
order: { createdAt: 'DESC' },
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new RequestTimeoutException(
|
throw new RequestTimeoutException('common.errors.requestTimeout', {
|
||||||
'Unable to process your request at the moment. Please try again later.',
|
cause: err,
|
||||||
{ cause: err, description: 'Error connecting to the database' },
|
description: 'Error connecting to the database',
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!otp) throw new UnauthorizedException('Invalid OTP');
|
if (!otp) throw new UnauthorizedException('auth.errors.invalidOTP');
|
||||||
|
|
||||||
if (otp.expiresAt < new Date()) {
|
|
||||||
throw new UnauthorizedException('OTP expired');
|
|
||||||
}
|
|
||||||
|
|
||||||
const isOTPCorrect = await this.hashingProvider.compare(code, otp.code);
|
const isOTPCorrect = await this.hashingProvider.compare(code, otp.code);
|
||||||
|
|
||||||
if (!isOTPCorrect) {
|
if (!isOTPCorrect) {
|
||||||
throw new UnauthorizedException('Invalid OTP');
|
throw new UnauthorizedException('auth.errors.invalidOTP');
|
||||||
}
|
}
|
||||||
|
|
||||||
otp.used = true;
|
otp.used = true;
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
GET http://localhost:3000/users/profile
|
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 { Repository } from 'typeorm';
|
||||||
import { User } from '../user.entity';
|
import { User } from '../user.entity';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
@@ -13,14 +17,32 @@ export class UsersService {
|
|||||||
private readonly userRepository: Repository<User>,
|
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) {
|
public async findOneByPhone(phone: string) {
|
||||||
try {
|
try {
|
||||||
return await this.userRepository.findOneBy({ phone });
|
return await this.userRepository.findOneBy({ phone });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new RequestTimeoutException(
|
throw new RequestTimeoutException('common.errors.requestTimeout', {
|
||||||
'Unable to process your request at the moment. Please try again later.',
|
cause: err,
|
||||||
{ cause: err, description: 'Error connecting to the database' },
|
description: 'Error connecting to the database',
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,10 +52,10 @@ export class UsersService {
|
|||||||
try {
|
try {
|
||||||
await this.userRepository.save(newUser);
|
await this.userRepository.save(newUser);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new RequestTimeoutException(
|
throw new RequestTimeoutException('common.errors.requestTimeout', {
|
||||||
'Unable to process your request at the moment. Please try again later.',
|
cause: err,
|
||||||
{ cause: err, description: 'Error connecting to the database' },
|
description: 'Error connecting to the database',
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return newUser;
|
return newUser;
|
||||||
|
|||||||
Reference in New Issue
Block a user