Skip to content

Commit

Permalink
feat(Authentication---Login-Implementation,-Caching-Implementation,-s…
Browse files Browse the repository at this point in the history
…ome-code-refactoring): Apply auth guard to controller. Cache service methods response to redis, refactor utility functions

BREAKING CHANGE:
  • Loading branch information
AllStackDev1 committed Aug 5, 2024
1 parent 060953c commit 87c1904
Show file tree
Hide file tree
Showing 12 changed files with 253 additions and 134 deletions.
38 changes: 33 additions & 5 deletions src/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ResetPasswordSchema,
} from 'validators';
import { Route, Validator, Controller } from 'decorators';
import { cookiesConfig, isProd } from 'configs/env.config';

@Controller('/auth')
@injectable()
Expand All @@ -22,7 +23,7 @@ export class AuthController {

@Route('post', '/signup')
@Validator({ body: SignupSchema })
async signup(req: Request, res: Response) {
async signup(req: Request<[], [], SignupSchema>, res: Response) {
return res.status(CREATED).json({
user: await this.service.register(req.body),
message: 'User created successfully',
Expand All @@ -31,21 +32,48 @@ export class AuthController {

@Route('post', '/login')
@Validator({ body: LoginSchema })
async login(req: Request, res: Response) {
return res.status(OK).json(await this.service.authenticate(req.body));
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 });
}
}

@Route('post', '/logout')
async logout(req: Request, res: Response) {
return req.session.destroy(function () {
// Clear the session cookie
return res
.clearCookie('auth_token')
.status(OK)
.json({ message: 'Successfully logged out 😏 🍀' });
});
}

@Route('post', '/request-password-reset')
@Validator({ body: EmailSchema })
async requestPasswordReset(req: Request, res: Response) {
async requestPasswordReset(req: Request<[], [], EmailSchema>, res: Response) {
return res
.status(OK)
.json(await this.service.sendPasswordResetEmail(req.body));
}

@Route('post', '/password-reset')
@Validator({ body: ResetPasswordSchema })
async passwordReset(req: Request, res: Response) {
async passwordReset(
req: Request<[], [], ResetPasswordSchema>,
res: Response,
) {
return res.status(OK).json(await this.service.resetPassword(req.body));
}
}
63 changes: 38 additions & 25 deletions src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { OK } from 'http-status';
import { Request, Response } from 'express';
import { injectable, inject } from 'inversify';
import { ACCEPTED, NO_CONTENT, OK } from 'http-status';

import {
QuerySchema,
ParamsWithId,
UpdateSchema,
DeleteTypeSchema,
} from 'validators';
import { TYPES } from 'di/types';
import { auth } from 'middlewares';
import { IUserService } from 'services';
import { Route, Controller, Validator } from 'decorators';
import { miscSchema, UpdateSchema, QuerySchema } from 'validators';

@Controller('/users')
@injectable()
Expand All @@ -15,41 +21,48 @@ export class UserController {
private service: IUserService,
) {}

@Route('get')
@Route('get', '', auth)
async getAll(_: Request, res: Response) {
return res.status(OK).json(await this.service.getAllUsers());
}

@Route('get', '/:id')
@Validator({ params: miscSchema('id') })
async getById(req: Request, res: Response) {
return res.status(OK).json(await this.service.getUserById(req.params.id));
@Route('get', '/search', auth)
@Validator({ query: QuerySchema })
async query(req: Request<[], [], [], QuerySchema>, res: Response) {
return res.status(OK).json(await this.service.getUsersByQuery(req.query));
}

@Route('get', '/search')
@Validator({ query: QuerySchema })
async query(req: Request, res: Response) {
return res
.status(OK)
.json(await this.service.getUsersBasedOnQuery(req.query));
@Route('get', '/:id', auth)
@Validator({ params: ParamsWithId })
async getById(req: Request<ParamsWithId>, res: Response) {
return res.status(OK).json(await this.service.getUserById(req.params.id));
}

@Route('patch')
@Validator({ body: UpdateSchema, params: miscSchema('id') })
async update(req: Request, res: Response) {
@Route('patch', '/:id', auth)
@Validator({ body: UpdateSchema, params: ParamsWithId })
async update(req: Request<ParamsWithId, [], UpdateSchema>, res: Response) {
return res.status(OK).json({
message: 'User details updated successfully',
data: await this.service.updateUser(req.params.id, req.body),
data: await this.service.update(req.params.id, req.body),
});
}

@Route('delete', '/:id')
@Validator({ params: miscSchema('id') })
async delete(req: Request, res: Response) {
await this.service.softDeleteUserById(req.params.id);
return res.status(OK).json({
message:
'Account deleted succesfully. Account will be parmantly deleted in 30day',
});
@Route('delete', '/:id', auth)
@Validator({ params: ParamsWithId, body: DeleteTypeSchema })
async delete(
req: Request<ParamsWithId, [], DeleteTypeSchema>,
res: Response,
) {
if (req.body.type === 'soft') {
await this.service.softDeleteById(req.params.id);
return res.status(ACCEPTED).json({
message: 'Account will be parmantly deleted in 30day',
});
} else {
await this.service.forceDeleteById(req.params.id);
return res.status(NO_CONTENT).json({
message: 'Account deleted succesfully.',
});
}
}
}
21 changes: 14 additions & 7 deletions src/decorators/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,24 @@ interface ParamsOk extends BaseSchema {

type SchemaPayload = BodyOk | QueryOk | ParamsOk;

const errorMessageBuilder = (issue: ZodIssue) => {
let str = '';
if (issue.message.toLowerCase().includes('invalid')) {
str = `${issue.path.join('.')} contains ${issue.message?.toLowerCase()}`;
const errorMessageBuilder = (
issue: ZodIssue,
): string | Record<string, string | number | (string | number)[]> => {
const message = issue.message.toLowerCase();

if (message.includes('one of')) {
return { message: issue.message, path: issue.path };
}

if (message.includes('invalid')) {
return `${issue.path} contains ${message}`;
}

if (issue.message.toLowerCase().includes('required')) {
str = `${issue.path.join('.')} is ${issue.message?.toLowerCase()}`;
if (message.includes('required')) {
return `${issue.path} is ${message}`;
}

return str;
return '';
};

export function Validator({ body, query, params }: SchemaPayload) {
Expand Down
5 changes: 1 addition & 4 deletions src/di/modules/repositories.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import { UserRepository } from 'repositories';
import { TYPES } from 'di/types';

const initializeModule = (bind: interfaces.Bind) => {
/* bind<IRepository<UserModel>>(TYPES.UserRepository).toConstantValue(
new UserRepository(new UserModel()),
); */
bind<UserRepository>(TYPES.UserRepository).to(UserRepository);
};

export const RepositoryModule = new ContainerModule(initializeModule);
export const RepositoriesModule = new ContainerModule(initializeModule);
24 changes: 11 additions & 13 deletions src/repositories/base.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export class BaseRepository<K, T extends Model> {
return await this.model.create(payload);
}

public async getAll() {
return await this.model.findAll();
public async getAll(query: WhereOptions<Attributes<T>> = {}) {
return await this.model.findAll({ where: query });
}

public async getById(id: string) {
Expand All @@ -30,18 +30,16 @@ export class BaseRepository<K, T extends Model> {
return await this.model.findOne({ where: query });
}

public async query(query: WhereOptions<Attributes<T>>) {
return await this.model.findAll({ where: query });
}

public async update(
id: WhereAttributeHashValue<Attributes<T>[string]>,
payload: Partial<K>,
) {
return await this.model.update(payload, { where: { id: id } });
public async updateById(id: string, payload: Partial<K>) {
return await this.model.update(payload, {
where: { id: id as WhereAttributeHashValue<Attributes<T>[string]> },
});
}

public async delete(id: WhereAttributeHashValue<Attributes<T>[string]>) {
return await this.model.destroy({ where: { id } });
public async deleteById(id: string, forceDel?: boolean) {
return await this.model.destroy({
where: { id: id as WhereAttributeHashValue<Attributes<T>[string]> },
force: forceDel,
});
}
}
80 changes: 41 additions & 39 deletions src/services/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import bcrypt from 'bcrypt';
import {
CONFLICT,
BAD_REQUEST,
UNPROCESSABLE_ENTITY,
INTERNAL_SERVER_ERROR,
} from 'http-status';
import { injectable, inject } from 'inversify';
import { CacheUpdate } from '@type-cacheable/core';
import { useAdapter } from '@type-cacheable/ioredis-adapter';

import { TYPES } from 'di/types';
import { AppError } from 'utils';
import { UserModelDto } from 'db/models';
import { HASHING_SALT } from 'configs/env';
import { AppError, exclude } from 'utils';
import { BaseService } from './base.service';
import { UserRepository } from 'repositories';
import { RedisClient } from 'configs/redis.config';
import type {
EmailSchema,
LoginSchema,
SignupSchema,
ResetPasswordSchema,
} from 'validators';

import { BaseService } from './base.service';

export interface IAuthService {
register(dto: SignupSchema): Promise<
| AppError
Expand All @@ -29,7 +29,9 @@ export interface IAuthService {
email: string;
}
>;
authenticate(dto: LoginSchema): Promise<UserModelDto | undefined>;
authenticate(
dto: LoginSchema,
): Promise<{ user: UserModelDto; token: string }>;
sendPasswordResetEmail(payload: EmailSchema): Promise<{ message: string }>;
resetPassword(payload: ResetPasswordSchema): Promise<{ message: string }>;
}
Expand All @@ -39,58 +41,57 @@ export class AuthService extends BaseService implements IAuthService {
constructor(
@inject(TYPES.UserRepository)
private repo: UserRepository,
@inject(TYPES.RedisClient)
redisClient: RedisClient,
) {
super();

useAdapter(
redisClient.get({
enableOfflineQueue: true,
}),
false,
);

this.on('user_login', async () => {});
this.on('new_sign_up', async () => {});
this.on('user_failed_login', async () => {});
}

private async hashPassword(password: string) {
return await bcrypt.hash(password, HASHING_SALT);
}

private async isPasswordMatch(p1: string, p2: string) {
return await bcrypt.compare(p1, p2);
}

// private generateAuthToken(u: UserModelDto) {}

private async validateJWT(token: string) {
if (token) return token;

return null;
}

@CacheUpdate({
cacheKey: (_, __, result) => result.id,
cacheKeysToClear: ['users'],
})
public async register(dto: SignupSchema) {
const isEmailTaken = await this.repo.getOne({ email: dto.email });
if (isEmailTaken) {
return new AppError('A user with this email already exist', CONFLICT);
let user = await this.repo.getOne({ email: dto.email });
if (user) {
throw new AppError('A user with this email already exist', CONFLICT);
}

const password = await this.hashPassword(dto.password);

const user = await this.repo.create({ ...dto, password });

return { name: `${user.firstName} ${user.lastName}`, email: user.email };
user = await this.repo.create(dto);
return { name: user.getFullname(), email: user.email };
}

public async authenticate(dto: LoginSchema) {
const user = await this.repo.getOne({ email: dto.email });
if (user) {
if (await this.isPasswordMatch(dto.password, user.password)) {
/* const token = this.tokenService.create(user);
const userDto = this.modelToDto(user);
return {
tokenInfo: token,
user: userDto,
}; */

return user;
}
if (user && (await user.isPasswordMatch(dto.password))) {
const token = user.generateAuthToken('auth');
return {
user: exclude(user.toJSON(), [
'password',
'updatedAt',
'createdAt',
'deletedAt',
]),
token,
};
}
new AppError('Invalid email or password', BAD_REQUEST);
throw new AppError('Invalid email or password', BAD_REQUEST);
}

public async sendPasswordResetEmail(payload: EmailSchema) {
Expand All @@ -107,8 +108,9 @@ export class AuthService extends BaseService implements IAuthService {
public async resetPassword(payload: ResetPasswordSchema) {
const id = await this.validateJWT(payload.token);
if (id) {
const password = await this.hashPassword(payload.newPassword);
const [no] = await this.repo.update(id, { password });
const [no] = await this.repo.updateById(id, {
password: payload.newPassword,
});
if (no) {
return { message: 'Your password has been succesfully updated.' };
}
Expand Down
Loading

0 comments on commit 87c1904

Please sign in to comment.