From 6baedd6596e7772439a53105142e8c0ac2d99b6e Mon Sep 17 00:00:00 2001 From: Chinedu Ekene Okpala Date: Fri, 25 Oct 2024 02:33:13 +0000 Subject: [PATCH] perf(Authentication-flow): modify auth logic and flow created an auth decorator - @AuthGuard, instead of pass it authHandler as a middleware, we can extend it to check for role base access permission --- src/configs/redis.config.ts | 9 +- src/controllers/auth.controller.ts | 57 +++++++--- src/controllers/user.controller.ts | 18 ++-- src/db/models/user.model.ts | 21 ++-- src/decorators/auth-guard.ts | 30 ++++++ src/decorators/index.ts | 1 + src/decorators/validator.ts | 4 +- src/di/container.ts | 10 +- src/di/modules/thirdparty.module.ts | 3 +- src/di/types.ts | 1 + src/middlewares/authHandler.ts | 107 +++++++++++++------ src/middlewares/globalErrorHandler.ts | 5 +- src/middlewares/index.ts | 2 +- src/middlewares/rateLimitHandler.ts | 2 +- src/middlewares/sessionHandler.ts | 2 +- src/server.ts | 6 +- src/services/auth.service.ts | 143 +++++++++++++++++++++++--- src/services/user.service.ts | 2 +- src/tests/test.context.ts | 4 - src/tests/test.model.ts | 1 - src/utils/appError.ts | 9 +- src/utils/catchAsync.ts | 2 +- src/validators/user.zod.schema.ts | 6 ++ 23 files changed, 338 insertions(+), 107 deletions(-) create mode 100644 src/decorators/auth-guard.ts diff --git a/src/configs/redis.config.ts b/src/configs/redis.config.ts index df970b2..b9171fd 100644 --- a/src/configs/redis.config.ts +++ b/src/configs/redis.config.ts @@ -7,14 +7,19 @@ import { injectable } from 'inversify'; export class RedisClient { private client?: Redis; - get(opts?: RedisOptions) { + /** + * + * @param opts RedisOptions + * @returns Redis client + */ + getClient(opts?: RedisOptions) { this.client = this.client || this.createClient(opts); return this.client; } close() { - this.get().disconnect(); + this.getClient().disconnect(); } private createClient(opts?: RedisOptions) { diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index d1006fd..8ee2bf6 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -9,9 +9,11 @@ import { EmailSchema, SignupSchema, ResetPasswordSchema, + RefreshTokenSchema, } from 'validators'; import { Route, Validator, Controller } from 'decorators'; import { cookiesConfig, isProd } from 'configs/env.config'; +import { UserModelDto } from 'db/models'; @Controller('/auth') @injectable() @@ -30,33 +32,56 @@ export class AuthController { }); } + private setSessionAndRespond( + req: Request | Request<[], [], LoginSchema>, + res: Response, + result: { + user: UserModelDto; + accessToken: string; + refreshToken: string; + }, + ) { + // set session + req.session.user = result.user; + req.session.authorized = true; + return res + .cookie('refreshToken', result.refreshToken, { + httpOnly: true, + maxAge: +cookiesConfig.maxAge, + secure: isProd, + }) + .status(OK) + .json({ ...result.user, accessToken: result.accessToken }); + } + @Route('post', '/login') @Validator({ body: LoginSchema }) async login(req: Request<[], [], LoginSchema>, res: Response) { const result = await this.service.authenticate(req.body); - if (result) { - // set session - req.session.user = result.user; - req.session.authorized = true; - return res - .cookie('auth_token', result.token, { - httpOnly: true, - maxAge: +cookiesConfig.maxAge, - secure: isProd, - }) - .status(OK) - .json({ ...result.user, token: result.token }); - } + if (result) this.setSessionAndRespond(req, res, result); + } + + @Route('get', '/refresh-token') + async refreshToken(req: Request, res: Response) { + const result = await this.service.refreshAccessToken( + res, + req.cookies as RefreshTokenSchema, + ); + + if (result) this.setSessionAndRespond(req, res, result); } - @Route('post', '/logout') + @Route('delete', '/session') async logout(req: Request, res: Response) { return req.session.destroy(function () { // Clear the session cookie return res - .clearCookie('auth_token') + .clearCookie('refreshToken', { + httpOnly: true, + secure: isProd, + }) .status(OK) - .json({ message: 'Successfully logged out 😏 πŸ€' }); + .json({ message: 'Successfully logged out: πŸšͺ πŸšΆβ€β™‚οΈ πŸ‘‹' }); }); } diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 0ea6093..5c19ee5 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -9,9 +9,8 @@ import { DeleteTypeSchema, } from 'validators'; import { TYPES } from 'di/types'; -import { auth } from 'middlewares'; import { IUserService } from 'services'; -import { Route, Controller, Validator } from 'decorators'; +import { Route, Controller, Validator, AuthGuard } from 'decorators'; @Controller('/users') @injectable() @@ -21,24 +20,28 @@ export class UserController { private service: IUserService, ) {} - @Route('get', '', auth) + @Route('get', '/') + @AuthGuard() async getAll(_: Request, res: Response) { return res.status(OK).json(await this.service.getAllUsers()); } - @Route('get', '/search', auth) + @Route('get', '/search') + @AuthGuard() @Validator({ query: QuerySchema }) async query(req: Request<[], [], [], QuerySchema>, res: Response) { return res.status(OK).json(await this.service.getUsersByQuery(req.query)); } - @Route('get', '/:id', auth) + @Route('get', '/:id') + @AuthGuard() @Validator({ params: ParamsWithId }) async getById(req: Request, res: Response) { return res.status(OK).json(await this.service.getUserById(req.params.id)); } - @Route('patch', '/:id', auth) + @Route('patch', '/:id') + @AuthGuard() @Validator({ body: UpdateSchema, params: ParamsWithId }) async update(req: Request, res: Response) { return res.status(OK).json({ @@ -47,7 +50,8 @@ export class UserController { }); } - @Route('delete', '/:id', auth) + @Route('delete', '/:id') + @AuthGuard() @Validator({ params: ParamsWithId, body: DeleteTypeSchema }) async delete( req: Request, diff --git a/src/db/models/user.model.ts b/src/db/models/user.model.ts index 9eddd76..1e9a109 100644 --- a/src/db/models/user.model.ts +++ b/src/db/models/user.model.ts @@ -6,7 +6,7 @@ import { CreationOptional, } from 'sequelize'; import bcrypt from 'bcrypt'; -import jwt from 'jsonwebtoken'; +import { sign } from 'jsonwebtoken'; import { decorate, injectable } from 'inversify'; import sequelize from 'configs/sequelize.config'; @@ -47,14 +47,17 @@ export class UserModel extends Model { return await bcrypt.compare(password, this.password); } - generateAuthToken(type: 'auth' | 'reset' | 'verify') { - return jwt.sign( - { sub: this.id, email: this.email, type }, - jwtConfig.secretKey, - { - expiresIn: jwtConfig.expiresIn, - }, - ); + generateJWT(type: 'access' | 'refresh' | 'reset' | 'verify') { + const { secretKey, accessExpiresIn, refreshExpiresIn, defaultExpiresIn } = + jwtConfig; + return sign({ sub: this.id, email: this.email, type }, secretKey, { + expiresIn: + type === 'access' + ? accessExpiresIn + : type === 'refresh' + ? refreshExpiresIn + : defaultExpiresIn, + }); } } diff --git a/src/decorators/auth-guard.ts b/src/decorators/auth-guard.ts new file mode 100644 index 0000000..4a173e9 --- /dev/null +++ b/src/decorators/auth-guard.ts @@ -0,0 +1,30 @@ +import { INTERNAL_SERVER_ERROR } from 'http-status'; +import { TYPES } from 'di/types'; +import { AppError } from 'utils'; +import container from 'di/container'; +import { RouteHandler } from 'utils/catchAsync'; +import { AuthHandler } from 'middlewares/authHandler'; + +export function AuthGuard() { + return function (target: object, _: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value; + descriptor.value = async function (...args: RouteHandler) { + if (!container) { + throw new AppError('Container not set', INTERNAL_SERVER_ERROR); + } + const authHandler = container.get(TYPES.AuthHandler); + const handlerMiddleware = await authHandler.handler(); + + return new Promise((resolve, reject) => { + handlerMiddleware(args[0], args[1], (error) => { + if (error) { + reject(error); + } else { + originalMethod.apply(this, args).then(resolve).catch(reject); + } + }); + }); + }; + return descriptor; + }; +} diff --git a/src/decorators/index.ts b/src/decorators/index.ts index bc8f37a..b730d30 100644 --- a/src/decorators/index.ts +++ b/src/decorators/index.ts @@ -1,3 +1,4 @@ export * from './route'; +export * from './auth-guard'; export * from './validator'; export * from './controller'; diff --git a/src/decorators/validator.ts b/src/decorators/validator.ts index bc78a31..40b175c 100644 --- a/src/decorators/validator.ts +++ b/src/decorators/validator.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { AppError } from 'utils'; +import httpStatus from 'http-status'; import { Request, Response, NextFunction } from 'express'; import { ZodEffects, ZodError, ZodIssue, ZodObject } from 'zod'; -import httpStatus from 'http-status'; -import { AppError } from 'utils'; type ZodSchema = ZodObject | ZodEffects>; diff --git a/src/di/container.ts b/src/di/container.ts index 2a47889..64cd29b 100644 --- a/src/di/container.ts +++ b/src/di/container.ts @@ -1,4 +1,4 @@ -import { Container as InversifyContainer } from 'inversify'; +import { interfaces, Container as InversifyContainer } from 'inversify'; import { ModelsModule, @@ -18,8 +18,8 @@ export class Container { this.register(); } - public getApp() { - return this._container.get(App); + public get(serviceIdentifier: interfaces.ServiceIdentifier): T { + return this._container.get(serviceIdentifier); } private register() { @@ -31,3 +31,7 @@ export class Container { this._container.bind(App).toSelf(); } } + +const container = new Container(); + +export default container; diff --git a/src/di/modules/thirdparty.module.ts b/src/di/modules/thirdparty.module.ts index 06190ab..95c28f9 100644 --- a/src/di/modules/thirdparty.module.ts +++ b/src/di/modules/thirdparty.module.ts @@ -2,10 +2,11 @@ import { ContainerModule, interfaces } from 'inversify'; import { TYPES } from 'di/types'; import { RedisClient } from 'configs/redis.config'; -import { RateLimitHandler, SessionHandler } from 'middlewares'; +import { RateLimitHandler, SessionHandler, AuthHandler } from 'middlewares'; const initializeModule = (bind: interfaces.Bind) => { bind(TYPES.RedisClient).to(RedisClient); + bind(TYPES.AuthHandler).to(AuthHandler); bind(TYPES.SessionHandler).to(SessionHandler); bind(TYPES.RateLimitHandler).to(RateLimitHandler); }; diff --git a/src/di/types.ts b/src/di/types.ts index 1d573b0..6c1c7bf 100644 --- a/src/di/types.ts +++ b/src/di/types.ts @@ -15,6 +15,7 @@ export const TYPES = { // Thirdparty RedisClient: Symbol.for('RedisClient'), + AuthHandler: Symbol.for('AuthHandler'), SessionHandler: Symbol.for('SessionHandler'), RateLimitHandler: Symbol.for('RateLimitHandler'), }; diff --git a/src/middlewares/authHandler.ts b/src/middlewares/authHandler.ts index 7123e51..59c386d 100644 --- a/src/middlewares/authHandler.ts +++ b/src/middlewares/authHandler.ts @@ -1,10 +1,12 @@ -import jwt from 'jsonwebtoken'; +import { inject, injectable } from 'inversify'; import { UNAUTHORIZED } from 'http-status'; import { Request, Response, NextFunction } from 'express'; import { UserModel } from 'db/models'; -import { jwtConfig } from 'configs/env.config'; import { AppError, catchAsync } from 'utils'; +import { TYPES } from 'di/types'; +import { RedisClient } from 'configs/redis.config'; +import { IAuthService } from 'services'; declare module 'express-session' { interface SessionData { @@ -19,40 +21,81 @@ declare module 'express' { } } -async function authenticateToken( - req: Request, - res: Response, - next: NextFunction, -) { - let token: string = req.cookies.auth_token; - - if (!token && req.headers.authorization?.startsWith('Bearer')) { - token = req.headers.authorization.split(' ')[1]; +@injectable() +export class AuthHandler { + constructor( + @inject(TYPES.AuthService) + private authService: IAuthService, + @inject(TYPES.RedisClient) private redisClient: RedisClient, + ) { + this.handler = this.handler.bind(this); } - if (!token) { - return next(new AppError('Please login to gain access', UNAUTHORIZED)); - } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private async authenticateToken(req: Request, _: Response) { + if (req.headers.authorization?.startsWith('Bearer')) { + const accessToken = req.headers.authorization.split(' ')[1]; + if (!accessToken) { + throw new AppError('Please login to gain access', UNAUTHORIZED); + } + // verify access token + const { decoded, error } = + await this.authService.validateJWT(accessToken); - const tokenDetails = jwt.verify(token, jwtConfig.secretKey); + if (!error) { + const storedUser = (await this.redisClient + .getClient() + .get(decoded?.sub as string)) as string; - const user = await UserModel.findByPk(tokenDetails?.sub as string); + if (!storedUser) { + throw new AppError( + 'Access token expired. Please request a new one using your refresh token.', + UNAUTHORIZED, + null, + 'TOKEN_EXPIRED', + ); + } - if (!user) { - return next(new AppError('Could not find user', 400)); - } - req.user = user; - req.session.user = user; - next(); // Continue to the protected route -} + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { refreshToken: _, ...user } = JSON.parse(storedUser); + + req.user = user; + req.session.user = user; + return; + } + } -export const authHandler = catchAsync( - (req: Request, res: Response, next: NextFunction) => { - const user = req.session.user; - const authorized = req.session.authorized; - if (authorized && user) { - return next(); + // if no access token in header, check for refresh token in cookies + // and ask client to request a new access token + const refreshToken = req.cookies.refreshToken; + if (refreshToken) { + const { error } = await this.authService.validateJWT(refreshToken); + + if (!error) { + // Instead of creating a new access token here, instruct the client to request one + throw new AppError( + 'Access token expired. Please request a new one using your refresh token.', + UNAUTHORIZED, + null, + 'TOKEN_EXPIRED', + ); + } } - return authenticateToken(req, res, next); - }, -); + + throw new AppError('Please login to gain access', UNAUTHORIZED); + } + + public async handler() { + return catchAsync( + async (req: Request, res: Response, next: NextFunction) => { + const user = req.session.user; + const authorized = req.session.authorized; + if (authorized && user) { + return next(); + } + await this.authenticateToken(req, res); + next(); + }, + ); + } +} diff --git a/src/middlewares/globalErrorHandler.ts b/src/middlewares/globalErrorHandler.ts index 3095021..1b1a7d5 100644 --- a/src/middlewares/globalErrorHandler.ts +++ b/src/middlewares/globalErrorHandler.ts @@ -10,11 +10,13 @@ const sendErrorDev = (err: AppError, res: Response) => { const message = err.message; const errors = err.errors; const stack = err.stack; + const code = err.code; return res.status(statusCode).json({ status, message, errors, + code, stack, }); }; @@ -24,12 +26,13 @@ const sendErrorProd = (err: AppError, res: Response) => { const IsOperational = err.IsOperational; const status = err.status || 'error'; const message = err.message; - // const stack = err.stack; + const errors = err.errors; if (IsOperational) { return res.status(statusCode).json({ status, message, + errors, }); } diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts index a00b7ba..d2210bb 100644 --- a/src/middlewares/index.ts +++ b/src/middlewares/index.ts @@ -1,5 +1,5 @@ export { corsHandler } from './corsHandler'; -export { authHandler as auth } from './authHandler'; +export { AuthHandler } from './authHandler'; export { defineRoutes } from './defineRoutes'; export { loggerHandler } from './loggerHandler'; export { SessionHandler } from './sessionHandler'; diff --git a/src/middlewares/rateLimitHandler.ts b/src/middlewares/rateLimitHandler.ts index 372b977..4d7ddb1 100644 --- a/src/middlewares/rateLimitHandler.ts +++ b/src/middlewares/rateLimitHandler.ts @@ -12,7 +12,7 @@ decorate(injectable(), RateLimiterRedis); export class RateLimitHandler extends RateLimiterRedis { constructor(@inject(TYPES.RedisClient) private redisClient: RedisClient) { super({ - storeClient: redisClient.get({ + storeClient: redisClient.getClient({ enableOfflineQueue: false, }), keyPrefix: 'rate-limit', diff --git a/src/middlewares/sessionHandler.ts b/src/middlewares/sessionHandler.ts index 86b7d11..89cbb60 100644 --- a/src/middlewares/sessionHandler.ts +++ b/src/middlewares/sessionHandler.ts @@ -33,7 +33,7 @@ export const sessionHandler = (client: Redis) => { export class SessionHandler extends RedisStore { constructor(@inject(TYPES.RedisClient) private redisClient: RedisClient) { super({ - client: redisClient.get(), + client: redisClient.getClient(), prefix: 'myapp:super-parakeet', }); diff --git a/src/server.ts b/src/server.ts index 7f42944..9f002db 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,9 +1,9 @@ import 'reflect-metadata'; -import { Container } from 'di/container'; +import container from 'di/container'; +import { App } from 'app'; (async () => { - const container = new Container(); - const app = container.getApp(); + const app = container.get(App); await app.initialize(); app.start(); })(); diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 796a588..01bc228 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -3,7 +3,10 @@ import { BAD_REQUEST, UNPROCESSABLE_ENTITY, INTERNAL_SERVER_ERROR, + FORBIDDEN, + UNAUTHORIZED, } from 'http-status'; +import { verify, decode, JwtPayload } from 'jsonwebtoken'; import { injectable, inject } from 'inversify'; import { CacheUpdate } from '@type-cacheable/core'; import { useAdapter } from '@type-cacheable/ioredis-adapter'; @@ -14,12 +17,18 @@ import { AppError, exclude } from 'utils'; import { BaseService } from './base.service'; import { UserRepository } from 'repositories'; import { RedisClient } from 'configs/redis.config'; +import { cookiesConfig, jwtConfig } from 'configs/env.config'; import type { EmailSchema, LoginSchema, SignupSchema, ResetPasswordSchema, + RefreshTokenSchema, } from 'validators'; +import { Redis } from 'ioredis'; +import { Response } from 'express'; + +const REDIS_BUFFER = 3 * 60; export interface IAuthService { register(dto: SignupSchema): Promise< @@ -29,39 +38,72 @@ export interface IAuthService { email: string; } >; + validateJWT( + token: string, + ): Promise<{ decoded: JwtPayload | null; error?: unknown }>; authenticate( dto: LoginSchema, - ): Promise<{ user: UserModelDto; token: string }>; + ): Promise<{ user: UserModelDto; accessToken: string; refreshToken: string }>; + refreshAccessToken( + res: Response, + dto: RefreshTokenSchema, + ): Promise<{ user: UserModelDto; accessToken: string; refreshToken: string }>; sendPasswordResetEmail(payload: EmailSchema): Promise<{ message: string }>; resetPassword(payload: ResetPasswordSchema): Promise<{ message: string }>; } @injectable() export class AuthService extends BaseService implements IAuthService { + private _redisClient: Redis; constructor( @inject(TYPES.UserRepository) private repo: UserRepository, @inject(TYPES.RedisClient) - redisClient: RedisClient, + private redisClient: RedisClient, ) { super(); - useAdapter( - redisClient.get({ - enableOfflineQueue: true, - }), - false, - ); + this._redisClient = this.redisClient.getClient({ + enableOfflineQueue: true, + }); + + useAdapter(this._redisClient, false); this.on('user_login', async () => {}); this.on('new_sign_up', async () => {}); this.on('user_failed_login', async () => {}); } - private async validateJWT(token: string) { - if (token) return token; + public async validateJWT(token: string) { + let response: { + decoded: JwtPayload | null; + error?: unknown; + } = { decoded: null }; + + try { + const decoded = verify(token, jwtConfig.secretKey) as JwtPayload; + response = { decoded, error: null }; + } catch (error) { + response = { decoded: decode(token) as JwtPayload, error }; + } + + return response; + } + + private async handleInvalidSession(res: Response, userId?: string) { + // Clear refresh token from Redis + if (userId) { + await this._redisClient.del(`${userId}`); + } + + // Clear cookie + res.clearCookie('refreshToken', { + httpOnly: true, + secure: true, + sameSite: 'strict', + }); - return null; + throw new AppError('Invalid session', UNAUTHORIZED); } @CacheUpdate({ @@ -80,7 +122,16 @@ export class AuthService extends BaseService implements IAuthService { public async authenticate(dto: LoginSchema) { const user = await this.repo.getOne({ email: dto.email }); if (user && (await user.isPasswordMatch(dto.password))) { - const token = user.generateAuthToken('auth'); + const accessToken = user.generateJWT('access'); + const refreshToken = user.generateJWT('refresh'); + + this._redisClient.set( + `${user.id}`, + JSON.stringify({ ...user, refreshToken }), + 'EX', + +cookiesConfig.maxAge / 1000 + REDIS_BUFFER, + ); + return { user: exclude(user.toJSON(), [ 'password', @@ -88,12 +139,69 @@ export class AuthService extends BaseService implements IAuthService { 'createdAt', 'deletedAt', ]), - token, + accessToken, + refreshToken, }; } throw new AppError('Invalid email or password', BAD_REQUEST); } + public async refreshAccessToken( + res: Response, + { refreshToken }: RefreshTokenSchema, + ) { + if (!refreshToken) + throw new AppError('No refresh token provided', FORBIDDEN); + + const { decoded, error } = await this.validateJWT(refreshToken); + + if (error) { + await this.handleInvalidSession(res, decoded?.sub); + } + + // Check if token exists in Redis + const storedUser = await this._redisClient.get(`${decoded?.sub}`); + + if (!storedUser) { + await this.handleInvalidSession(res, decoded?.sub); + } + + // convert storedUser string to object + const { refreshToken: storedToken } = JSON.parse(storedUser as string); + + if (storedToken !== refreshToken) { + await this.handleInvalidSession(res, decoded?.sub); + } + + const user = await this.repo.getById(decoded?.sub as string); + + if (!user) throw new AppError('User not found', UNAUTHORIZED); + + // clear previous redis store + await this._redisClient.del(`${user.id}`); + + const accessToken = user.generateJWT('access'); + const newRefreshToken = user.generateJWT('refresh'); + + this._redisClient.set( + `${user.id}`, + JSON.stringify({ ...user, refreshToken: newRefreshToken }), + 'EX', + +cookiesConfig.maxAge / 1000 + REDIS_BUFFER, + ); + + return { + user: exclude(user.toJSON(), [ + 'password', + 'updatedAt', + 'createdAt', + 'deletedAt', + ]), + accessToken, + refreshToken: newRefreshToken, + }; + } + public async sendPasswordResetEmail(payload: EmailSchema) { const user = await this.repo.getOne({ email: payload.email }); if (user) { @@ -106,9 +214,12 @@ export class AuthService extends BaseService implements IAuthService { } public async resetPassword(payload: ResetPasswordSchema) { - const id = await this.validateJWT(payload.token); - if (id) { - const [no] = await this.repo.updateById(id, { + const { decoded, error } = await this.validateJWT(payload.token); + + if (error) throw new AppError('Invalid token', UNPROCESSABLE_ENTITY); + + if (decoded?.sub) { + const [no] = await this.repo.updateById(decoded.sub, { password: payload.newPassword, }); if (no) { diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 9830f38..3f90715 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -33,7 +33,7 @@ export class UserService extends BaseService implements IUserService { ) { super(); useAdapter( - redisClient.get({ + redisClient.getClient({ enableOfflineQueue: true, }), false, diff --git a/src/tests/test.context.ts b/src/tests/test.context.ts index c8f3a91..abfb14e 100644 --- a/src/tests/test.context.ts +++ b/src/tests/test.context.ts @@ -22,8 +22,4 @@ export class TestContext { private mockClass(implementation: () => Partial): T { return jest.fn(implementation)() as T; } - - private mockFunc(implementation: () => Partial): T { - return jest.fn(implementation)() as T; - } } diff --git a/src/tests/test.model.ts b/src/tests/test.model.ts index c2b4d8f..f07d8a0 100644 --- a/src/tests/test.model.ts +++ b/src/tests/test.model.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ - const table = new Map(); const indexedTable = new Map(); diff --git a/src/utils/appError.ts b/src/utils/appError.ts index d21d7cc..b35c872 100644 --- a/src/utils/appError.ts +++ b/src/utils/appError.ts @@ -4,11 +4,10 @@ export class AppError extends Error { constructor( public message: string, public statusCode: number, - public errors?: ( - | string - | Record - )[], - public code?: number, + public errors?: + | (string | Record)[] + | null, + public code?: number | string, ) { super(message); this.code = code; diff --git a/src/utils/catchAsync.ts b/src/utils/catchAsync.ts index 785bcf8..15866a4 100644 --- a/src/utils/catchAsync.ts +++ b/src/utils/catchAsync.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from 'express'; -type RouteHandler = [req: Request, res: Response, next: NextFunction]; +export type RouteHandler = [req: Request, res: Response, next: NextFunction]; export const catchAsync = (fn: (...rest: RouteHandler) => void) => { const errorHandler = (...rest: RouteHandler) => { diff --git a/src/validators/user.zod.schema.ts b/src/validators/user.zod.schema.ts index ee60595..3023b43 100644 --- a/src/validators/user.zod.schema.ts +++ b/src/validators/user.zod.schema.ts @@ -33,6 +33,12 @@ export const LoginSchema = z.object({ export type LoginSchema = z.infer; +export const RefreshTokenSchema = z.object({ + refreshToken: z.string(), +}); + +export type RefreshTokenSchema = z.infer; + export const EmailSchema = z.object({ email: z.string().email(), });