Skip to content

Commit

Permalink
Merge pull request #18 from victorsoares96/feat/celebrate
Browse files Browse the repository at this point in the history
feat: celebrate
  • Loading branch information
victorsoares96 authored Sep 4, 2022
2 parents d9e4638 + a42d8b7 commit 58386fa
Show file tree
Hide file tree
Showing 18 changed files with 363 additions and 112 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,17 @@
"axios": "^0.27.2",
"bcryptjs": "^2.4.3",
"body-parser": "^1.17.1",
"celebrate": "^15.0.1",
"class-validator": "^0.13.2",
"compression": "^1.6.2",
"cors": "^2.8.3",
"dotenv": "^16.0.1",
"ejs": "^3.1.6",
"escape-html": "^1.0.3",
"express": "^4.17.1",
"express-async-errors": "^3.1.1",
"jsonwebtoken": "^8.5.1",
"mysql": "^2.18.1",
"mysql2": "^2.3.3",
"reflect-metadata": "^0.1.13",
"source-map-support": "^0.5.19",
"tslib": "^2.3.1",
Expand All @@ -80,6 +82,7 @@
"@types/compression": "^1.7.1",
"@types/cors": "^2.8.12",
"@types/ejs": "^3.1.0",
"@types/escape-html": "^1.0.2",
"@types/express": "^4.17.13",
"@types/helmet": "^4.0.0",
"@types/jsonwebtoken": "^8.5.8",
Expand Down
4 changes: 1 addition & 3 deletions renovate.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
]
"extends": ["config:base"]
}
8 changes: 4 additions & 4 deletions src/database/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ const development: DataSourceOptions = {
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
ssl: {
/* ssl: {
rejectUnauthorized: false,
},
}, */
synchronize: false,
logging: false,
entities: ['src/entities/*.ts'],
Expand All @@ -25,9 +25,9 @@ const production: DataSourceOptions = {
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
ssl: {
/* ssl: {
rejectUnauthorized: false,
},
}, */
synchronize: false,
logging: false,
entities: ['entities/*.js', 'dist/entities/*.js'],
Expand Down
4 changes: 2 additions & 2 deletions src/entities/decorators/isValidPassword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import {
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
import { passwordRule } from '../user.entity';
import { passwordRule } from '@/utils/validators.util';

@ValidatorConstraint()
export class IsValidPasswordConstraint implements ValidatorConstraintInterface {
validate(password: string) {
const isValidPassword = passwordRule;
const isValidPassword = new RegExp(passwordRule, 'gm');
return isValidPassword.test(password);
}
}
Expand Down
3 changes: 0 additions & 3 deletions src/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ import {
import { IsEmail, IsNotEmpty, MaxLength, MinLength } from 'class-validator';
import { IsValidPassword } from './decorators/isValidPassword';

export const passwordRule =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/gm;

@Entity('user')
export class User {
@PrimaryGeneratedColumn('increment')
Expand Down
45 changes: 37 additions & 8 deletions src/middlewares/errorHandler.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
/* eslint-disable no-restricted-syntax */
import { Request, Response, NextFunction } from 'express';
import { AppError } from '../errors/AppError';
import { isCelebrateError } from 'celebrate';
import EscapeHtml from 'escape-html';
import { Error as CustomError } from '@/types/error.type';
import { AppError } from '@/errors/AppError';

export function errorHandler(
error: Error,
Expand All @@ -11,20 +15,45 @@ export function errorHandler(

if (error instanceof AppError) {
return response.status(error.statusCode).json({
status: 'error',
statusCode: error.statusCode,
error: error.name,
message: error.message,
});
stack: error.stack,
} as CustomError);
}

if (isCelebrateError(error)) {
const validation: { [key: string]: unknown } = {};

for (const [segment, joiError] of error.details.entries()) {
validation[segment] = {
source: segment,
keys: joiError.details.map(detail => EscapeHtml(detail.path.join('.'))),
message: joiError.message,
};
}

return response.status(400).json({
statusCode: 400,
error: error.name,
message: error.message,
validation,
stack: error.stack,
} as CustomError);
}

if (error instanceof Error) {
return response.status(500).json({
status: 'error',
statusCode: 500,
error: error.name,
message: error.message,
});
stack: error.stack,
} as CustomError);
}

return response.status(500).json({
status: 'error',
message: 'Internal server error.',
});
statusCode: 500,
error: 'Internal Server Error',
message: 'An internal server error has occurred, please try again later.',
} as CustomError);
}
18 changes: 17 additions & 1 deletion src/routes/authenticate.routes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import { Router } from 'express';
import { celebrate, Joi, Segments } from 'celebrate';
import { AuthenticateController } from '@/controllers/authenticate.controller';

export const authenticateRouter = Router();
const authenticateController = new AuthenticateController();

authenticateRouter.post('/auth', authenticateController.execute);
authenticateRouter.post(
'/auth',
celebrate({
[Segments.BODY]: Joi.object().keys({
username: Joi.string().required().messages({
'string.base': 'Username must be a string.',
'any.required': 'Username is required.',
}),
password: Joi.string().required().messages({
'string.base': 'Password must be a string.',
'any.required': 'Password is required.',
}),
}),
}),
authenticateController.execute,
);
140 changes: 136 additions & 4 deletions src/routes/users.routes.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,152 @@
import { Router } from 'express';
import { celebrate, Segments, Joi } from 'celebrate';
import ensureAuthenticated from '@/middlewares/ensureAuthenticated.middleware';
import { UsersController } from '../controllers/user.controller';
import { passwordRule } from '@/utils/validators.util';

export const usersRouter = Router();
const usersController = new UsersController();

usersRouter.get('/users', ensureAuthenticated, usersController.index);
usersRouter.get(
'/users',
celebrate({
[Segments.BODY]: Joi.object().keys({
name: Joi.string().messages({
'string.base': 'Name must be a string.',
}),
username: Joi.string().messages({
'string.base': 'Username must be a string.',
}),
email: Joi.string().messages({
'string.base': 'Email must be a string.',
}),
isDeleted: Joi.boolean().messages({
'boolean.base': 'isDeleted must be a boolean.',
}),
offset: Joi.number().messages({
'number.base': 'Offset must be a number.',
}),
isAscending: Joi.boolean().messages({
'boolean.base': 'isAscending must be a boolean.',
}),
limit: Joi.number().messages({
'number.base': 'Limit must be a number.',
}),
}),
}),
ensureAuthenticated,
usersController.index,
);

usersRouter.post('/users', /* ensureAuthenticated, */ usersController.create);
usersRouter.post(
'/users',
celebrate({
[Segments.BODY]: Joi.object().keys({
name: Joi.string().required().messages({
'string.base': 'Name must be a string.',
'any.required': 'Name is required.',
}),
username: Joi.string().required().messages({
'string.base': 'Username must be a string.',
'any.required': 'Username is required.',
}),
email: Joi.string().email().required().messages({
'string.base': 'Email must be a string.',
'any.required': 'Email is required.',
'string.email': 'Email is invalid.',
}),
password: Joi.string().regex(passwordRule).required().messages({
'string.base': 'Password must be a string.',
'any.required': 'Password is required.',
'string.pattern.base':
'Password must be at least 8 characters, at least 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character.',
}),
}),
}),
usersController.create,
);

usersRouter.put('/users', ensureAuthenticated, usersController.update);
usersRouter.put(
'/users',
celebrate({
[Segments.BODY]: Joi.object().keys({
id: Joi.string().required().messages({
'string.base': 'Id must be a string.',
'any.required': 'The user id must be provided.',
}),
name: Joi.string().messages({
'string.base': 'Name must be a string.',
}),
email: Joi.string().email().messages({
'string.email': 'Email is invalid.',
}),
}),
}),
ensureAuthenticated,
usersController.update,
);

usersRouter.delete(
'/users/remove',
celebrate({
[Segments.BODY]: Joi.object().keys({
ids: Joi.string().required().messages({
'string.base': 'Ids must be a string.',
'any.required': 'The user(s) id(s) must be provided.',
}),
}),
}),
ensureAuthenticated,
usersController.remove,
);

usersRouter.patch('/users/password', usersController.resetPassword);
usersRouter.delete(
'/users/soft-remove',
celebrate({
[Segments.BODY]: Joi.object().keys({
ids: Joi.string().required().messages({
'string.base': 'Ids must be a string.',
'any.required': 'The user(s) id(s) must be provided.',
}),
}),
}),
ensureAuthenticated,
usersController.softRemove,
);

usersRouter.patch(
'/users/recover',
celebrate({
[Segments.BODY]: Joi.object().keys({
ids: Joi.string().required().messages({
'string.base': 'Ids must be a string.',
'any.required': 'The user(s) id(s) must be provided.',
}),
}),
}),
ensureAuthenticated,
usersController.recover,
);

usersRouter.patch(
'/users/password',
celebrate({
[Segments.BODY]: Joi.object().keys({
id: Joi.string().required().messages({
'string.base': 'Id must be a string.',
'any.required': 'The user id must be provided.',
}),
currentPassword: Joi.string().required().messages({
'string.base': 'Current password must be a string.',
'any.required': 'The current password must be provided.',
}),
newPassword: Joi.string().regex(passwordRule).required().messages({
'string.base': 'New password must be a string.',
'any.required': 'The new password must be provided.',
'string.pattern.base':
'Password must be at least 8 characters, at least 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character.',
}),
}),
}),
usersController.resetPassword,
);
4 changes: 4 additions & 0 deletions src/services/authenticate/authenticate.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ export class AuthenticateService {

await dataSource.manager.save(user);

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
delete user.password;

return { user, token };
}
}
12 changes: 6 additions & 6 deletions src/services/user/create.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { hash } from 'bcryptjs';
import { validate } from 'class-validator';
import { CelebrateError } from 'celebrate';
import { dataSource } from '@/database';
import { User } from '@/entities/user.entity';
import { AppError } from '@/errors/AppError';
Expand All @@ -18,11 +19,6 @@ export class CreateUserService {
email,
password,
}: Request): Promise<User> {
if (!name || !username || !email || !password)
throw new AppError(
'Please provide all fields: name, username, email and password.',
);

const userExists = await dataSource.manager.findOne(User, {
where: [{ name }, { email }, { username }],
});
Expand All @@ -42,7 +38,7 @@ export class CreateUserService {

if (error && error.constraints) {
const [message] = Object.values(error.constraints);
throw new AppError(message);
throw new CelebrateError(message);
}

const hashPassword = await hash(password, 8);
Expand All @@ -51,6 +47,10 @@ export class CreateUserService {

await dataSource.manager.save(user);

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
delete user.password;

return user;
}
}
Loading

0 comments on commit 58386fa

Please sign in to comment.