Skip to content

Commit

Permalink
perf(Authentication-flow): modify auth logic and flow
Browse files Browse the repository at this point in the history
created an auth decorator - @AuthGuard, instead of pass it authHandler as a middleware, we can extend it to check for role base access permission
  • Loading branch information
AllStackDev1 committed Oct 25, 2024
1 parent 8bb76c1 commit 6baedd6
Show file tree
Hide file tree
Showing 23 changed files with 338 additions and 107 deletions.
9 changes: 7 additions & 2 deletions src/configs/redis.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
57 changes: 41 additions & 16 deletions src/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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: 🚪 🚶‍♂️ 👋' });
});
}

Expand Down
18 changes: 11 additions & 7 deletions src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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<ParamsWithId>, 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<ParamsWithId, [], UpdateSchema>, res: Response) {
return res.status(OK).json({
Expand All @@ -47,7 +50,8 @@ export class UserController {
});
}

@Route('delete', '/:id', auth)
@Route('delete', '/:id')
@AuthGuard()
@Validator({ params: ParamsWithId, body: DeleteTypeSchema })
async delete(
req: Request<ParamsWithId, [], DeleteTypeSchema>,
Expand Down
21 changes: 12 additions & 9 deletions src/db/models/user.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -47,14 +47,17 @@ export class UserModel extends Model<UserModelDto> {
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,
});
}
}

Expand Down
30 changes: 30 additions & 0 deletions src/decorators/auth-guard.ts
Original file line number Diff line number Diff line change
@@ -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<AuthHandler>(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;
};
}
1 change: 1 addition & 0 deletions src/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './route';
export * from './auth-guard';
export * from './validator';
export * from './controller';
4 changes: 2 additions & 2 deletions src/decorators/validator.ts
Original file line number Diff line number Diff line change
@@ -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<any> | ZodEffects<ZodObject<any>>;

Expand Down
10 changes: 7 additions & 3 deletions src/di/container.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Container as InversifyContainer } from 'inversify';
import { interfaces, Container as InversifyContainer } from 'inversify';

import {
ModelsModule,
Expand All @@ -18,8 +18,8 @@ export class Container {
this.register();
}

public getApp() {
return this._container.get(App);
public get<T>(serviceIdentifier: interfaces.ServiceIdentifier<T>): T {
return this._container.get<T>(serviceIdentifier);
}

private register() {
Expand All @@ -31,3 +31,7 @@ export class Container {
this._container.bind<App>(App).toSelf();
}
}

const container = new Container();

export default container;
3 changes: 2 additions & 1 deletion src/di/modules/thirdparty.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand Down
1 change: 1 addition & 0 deletions src/di/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const TYPES = {

// Thirdparty
RedisClient: Symbol.for('RedisClient'),
AuthHandler: Symbol.for('AuthHandler'),
SessionHandler: Symbol.for('SessionHandler'),
RateLimitHandler: Symbol.for('RateLimitHandler'),
};
Loading

0 comments on commit 6baedd6

Please sign in to comment.