DEV Community

Cover image for Setting up Node JS, Express, MongoDB, Prettier, ESLint and Husky Application with Babel and authentication as an example
Mohammad Abu Mattar
Mohammad Abu Mattar

Posted on

Setting up Node JS, Express, MongoDB, Prettier, ESLint and Husky Application with Babel and authentication as an example

Table of Contents


Introduction

All code from this tutorial as a complete package is available in this repository. If you find this tutorial helpful, please share it with your friends and colleagues, and make sure to star the repository.

Since the ECMAScript JavaScript Standard is revised annually, it is a good idea to update our code as well.

The most recent JavaScript standards are occasionally incompatible with the browser. Something like to babel, which is nothing more than a JavaScript transpiler, is what we need to fix this sort of issue.

So, in this little tutorial, I'll explain how to set up babel for a basic NodeJS Express application so that we may utilize the most recent ES6 syntax in it.

What is Babel?

Babel is a toolchain that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments. Here are the main things Babel can do for you:

  • Transform syntax
  • Polyfill features that are missing in your target environment (through a third-party polyfill such as core-js)
  • Source code transformations (codemods)

Project Setup

We'll begin by creating a new directory called backend-template and then we'll create a new package.json file. We're going to be using yarn for this example, but you could just as easily use NPM if you choose, but yarn is a lot more convenient.

mkdir backend-template
cd backend-template
yarn init -y
Enter fullscreen mode Exit fullscreen mode

Engine Locking

The same Node engine and package management that we use should be available to all developers working on this project. We create two new files in order to achieve that:

  • .nvmrc: Will disclose to other project users the Node version that is being utilized.
  • .npmrc: reveals to other project users the package manager being used.

.nvmrc

touch .nvmrc
echo "lts/fermium" > .nvmrc
Enter fullscreen mode Exit fullscreen mode

.npmrc

touch .npmrc
echo 'engine-strict=true\r\nsave-exact = true\r\ntag-version-prefix=""\r\nstrict-peer-dependencies = false\r\nauto-install-peers = true\r\nlockfile = true' > .npmrc
Enter fullscreen mode Exit fullscreen mode

Notably, the usage of engine-strict said nothing about yarn in particular; we handle that in packages.json:

open packages.json add the engines:

{
  "name": "tutorial",
  "version": "0.0.0",
  "description": "",
  "keywords": [],
  "main": "index.js",
  "license": "MIT",
  "author": {
    "name": "Mohammad Abu Mattar",
    "email": "mohammad.khaled@outlook.com",
    "url": "https://mkabumattar.github.io/"
  },
  "homepage": "YOUR_GIT_REPO_URL#readme",
  "repository": {
    "type": "git",
    "url": "git+YOUR_GIT_REPO_URL.git"
  },
  "bugs": {
    "url": "YOUR_GIT_REPO_URL/issues"
  },
  "engines": {
    "node": ">=14.0.0",
    "yarn": ">=1.20.0",
    "npm": "please-use-yarn"
  }
}
Enter fullscreen mode Exit fullscreen mode

Babel Setup

In order to set up babel in the project, we must first install three main packages.

  • babel-core: The primary package for running any babel setup or configuration is babel-core.
  • babel-node: Any version of ES may be converted to ordinary JavaScript using the babel-node library.
  • babel-preset-env: This package gives us access to forthcoming functionalities that node.js does not yet comprehend. New features are constantly being developed, thus it will probably take some time for NodeJS to incorporate them.

Babel is mostly used in the code base to take advantage of new JavaScript capabilities. Unless the code is pure JavaScript, we don't know if the server's NodeJS will comprehend the specific code or not.

Therefore, transiling the code before deployment is usually advised. Two different types of babel transpiling code exist.

Development Setup:

yarn global add nodemon

# for linux users
sudo yarn global add nodemon
Enter fullscreen mode Exit fullscreen mode
yarn add express mongoose cors dotenv @babel/core @babel/node @babel/preset-env

##  compression cookie-parser core-js crypto-js helmet jsonwebtoken lodash regenerator-runtime
Enter fullscreen mode Exit fullscreen mode
yarn add -D @babel/cli babel-plugin-module-resolver
Enter fullscreen mode Exit fullscreen mode

Here, we initialize the package.json and install the basic express server, mongoose, cors, dotenv, babel-core, babel-node, babel-preset-env, babel-plugin-module-resolver.

Babel Configration

After that, we need to create a file called .babelrc in the project's root directory, and we paste the following block of code there.

touch .babelrc
Enter fullscreen mode Exit fullscreen mode
{
  "presets": [
    "@babel/preset-env"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Code Formatting and Quality Tools

We will be using two tools in order to establish a standard that will be utilized by all project participants to maintain consistency in the coding style and the use of fundamental best practices:

  • Prettier: A tool that will help us to format our code consistently.
  • ESLint: A tool that will help us to enforce a consistent coding style.

Prettier

Prettier will handle the automated file formatting for us. Add it to the project right now.

It's only needed during development, so I'll add it as a devDependency with -D

yarn add -D prettier
Enter fullscreen mode Exit fullscreen mode

Additionally, I advise getting the Prettier VS Code extension so that you may avoid using the command line tool and have VS Code take care of the file formatting for you. It's still required to include it here even when it's installed and set up in your project since VSCode will utilize your project's settings.

We'll create two files in the root:

  • .prettierrc: This file will contain the configuration for prettier.
  • .prettierignore: This file will contain the list of files that should be ignored by prettier.

.prettierrc

{
  "trailingComma": "all",
  "printWidth": 80,
  "tabWidth": 2,
  "useTabs": false,
  "semi": false,
  "singleQuote": true
}
Enter fullscreen mode Exit fullscreen mode

.prettierignore

node_modules
build
Enter fullscreen mode Exit fullscreen mode

I've listed the folders in that file that I don't want Prettier to waste any time working on. If you'd want to disregard specific file types in groups, you may also use patterns like *.html.

Now we add a new script to package.json so we can run Prettier:

package.json

...
"scripts: {
  ...
  "prettier": "prettier --write \"src/**/*.js\"",
  "prettier:check": "prettier --check \"src/**/*.js\""
}
Enter fullscreen mode Exit fullscreen mode

You can now run yarn prettier to format all files in the project, or yarn prettier:check to check if all files are formatted correctly.

yarn prettier:check
yarn prettier
Enter fullscreen mode Exit fullscreen mode

to automatically format, repair, and save all files in your project that you haven't ignored. My formatter updated around 7 files by default. The source control tab on the left of VS Code has a list of altered files where you may find them.

ESLint

We'll begin with ESLint, which is a tool that will help us to enforce a consistent coding style, at first need to install the dependencies.

yarn add -D eslint eslint-config-airbnb-base eslint-config-prettier eslint-plugin-import eslint-plugin-prettier
Enter fullscreen mode Exit fullscreen mode

We'll create two files in the root:

  • .eslintrc: This file will contain the configuration for ESLint.
  • .eslintignore: This file will contain the list of files that should be ignored by ESLint.

.eslintrc

{
  "extends": [
    "airbnb-base",
    "plugin:prettier/recommended",
    "plugin:import/errors",
    "plugin:import/warnings"
  ],
  "plugins": [
    "prettier"
  ],
  "rules": {
    "prettier/prettier": "error",
    "import/no-named-as-default": "off",
    "no-underscore-dangle": "off"
  }
}
Enter fullscreen mode Exit fullscreen mode

.eslintignore

node_modules
build
Enter fullscreen mode Exit fullscreen mode

Now we add a new script to package.json so we can run ESLint:

package.json

...
"scripts: {
  ...
  "lint": "eslint \"src/**/*.js\" --fix",
  "lint:check": "eslint \"src/**/*.js\""
}
Enter fullscreen mode Exit fullscreen mode

You can test out your config by running:

You can now run yarn lint to format all files in the project, or yarn lint:check to check if all files are formatted correctly.

yarn lint:check
yarn lint
Enter fullscreen mode Exit fullscreen mode

Setup logger for development

I had a problem setting up the logger when constructing a server-side application based on Node and Express router. Conditions for the answer:

  • Logging application event
  • Ability to specify multiple logs level
  • Logging of HTTP requests
  • Ability to write logs into a different source (console and file)

I found two possible solutions:

  • Morgan: HTTP logging middleware for express. It provides the ability to log incoming requests by specifying the formatting of log instance based on different request related information.
  • Winston: Multiple types of transports are supported by a lightweight yet effective logging library. Because I want to simultaneously log events into a file and a terminal, this practical feature is crucial for me.

I'll use Winston for the logging, first I'll install the package:

yarn add winston
Enter fullscreen mode Exit fullscreen mode

Then I'll create a file called utils/logger.util.js in it:

touch src/utils/logger.util.js
Enter fullscreen mode Exit fullscreen mode
import winston from 'winston'
import { NODE_ENV } from '../env/variable.env'

const levels = {
  error: 0,
  warn: 1,
  info: 2,
  http: 3,
  debug: 4,
}

const level = () => {
  const env = NODE_ENV || 'development'
  const isDevelopment = env === 'development'
  return isDevelopment ? 'debug' : 'warn'
}

const colors = {
  error: 'red',
  warn: 'yellow',
  info: 'green',
  http: 'magenta',
  debug: 'white',
}

winston.addColors(colors)

const format = winston.format.combine(
  winston.format.timestamp({
    format: 'YYYY-MM-DD HH:mm:ss:ms',
  }),
  winston.format.colorize({ all: true }),
  winston.format.printf(
    (info) => `${info.timestamp} ${info.level}: ${info.message}`,
  ),
)

const transports = [
  new winston.transports.Console(),
  new winston.transports.File({
    filename: 'logs/error.log',
    level: 'error',
  }),
  new winston.transports.File({ filename: 'logs/all.log' }),
]

const logger = winston.createLogger({
  level: level(),
  levels,
  format,
  transports,
})

export default logger
Enter fullscreen mode Exit fullscreen mode

Build file structure and basic express application

at first we'll create a directory called src, and we'll build the file structure inside of it.

mkdir src
cd src

mkdir bin config constants controllers env middlewares models repositories routers schemas security services validations

Enter fullscreen mode Exit fullscreen mode

for robust and don't repeat some text we'll create constants files in the constants directory:

touch src/constants/api.constant.js src/constants/dateformat.constant.js src/constants/http.code.constant.js src/constants/http.reason.constant.js src/constants/message.constant.js src/constants/model.constant.js src/constants/number.constant.js src/constants/path.constant.js src/constants/regex.constant.js
Enter fullscreen mode Exit fullscreen mode

there are some constants that are used in the application, but that are not related to the application itself. For example, the constants that are used in the API are in the api.constant.js file, and there is a file that provides freely for HTTP codes and replies, but we'll make one from start.

dateformat.constant.js

export const YYYY_MM_DD_HH_MM_SS_MS = 'YYYY-MM-DD HH:mm:ss:ms'

export default {
  YYYY_MM_DD_HH_MM_SS_MS,
}
Enter fullscreen mode Exit fullscreen mode

http.code.constant.js

export const CONTINUE = 100
export const SWITCHING_PROTOCOLS = 101
export const PROCESSING = 102
export const OK = 200
export const CREATED = 201
export const ACCEPTED = 202
export const NON_AUTHORITATIVE_INFORMATION = 203
export const NO_CONTENT = 204
export const RESET_CONTENT = 205
export const PARTIAL_CONTENT = 206
export const MULTI_STATUS = 207
export const ALREADY_REPORTED = 208
export const IM_USED = 226
export const MULTIPLE_CHOICES = 300
export const MOVED_PERMANENTLY = 301
export const MOVED_TEMPORARILY = 302
export const SEE_OTHER = 303
export const NOT_MODIFIED = 304
export const USE_PROXY = 305
export const SWITCH_PROXY = 306
export const TEMPORARY_REDIRECT = 307
export const BAD_REQUEST = 400
export const UNAUTHORIZED = 401
export const PAYMENT_REQUIRED = 402
export const FORBIDDEN = 403
export const NOT_FOUND = 404
export const METHOD_NOT_ALLOWED = 405
export const NOT_ACCEPTABLE = 406
export const PROXY_AUTHENTICATION_REQUIRED = 407
export const REQUEST_TIMEOUT = 408
export const CONFLICT = 409
export const GONE = 410
export const LENGTH_REQUIRED = 411
export const PRECONDITION_FAILED = 412
export const PAYLOAD_TOO_LARGE = 413
export const REQUEST_URI_TOO_LONG = 414
export const UNSUPPORTED_MEDIA_TYPE = 415
export const REQUESTED_RANGE_NOT_SATISFIABLE = 416
export const EXPECTATION_FAILED = 417
export const IM_A_TEAPOT = 418
export const METHOD_FAILURE = 420
export const MISDIRECTED_REQUEST = 421
export const UNPROCESSABLE_ENTITY = 422
export const LOCKED = 423
export const FAILED_DEPENDENCY = 424
export const UPGRADE_REQUIRED = 426
export const PRECONDITION_REQUIRED = 428
export const TOO_MANY_REQUESTS = 429
export const REQUEST_HEADER_FIELDS_TOO_LARGE = 431
export const UNAVAILABLE_FOR_LEGAL_REASONS = 451
export const INTERNAL_SERVER_ERROR = 500
export const NOT_IMPLEMENTED = 501
export const BAD_GATEWAY = 502
export const SERVICE_UNAVAILABLE = 503
export const GATEWAY_TIMEOUT = 504
export const HTTP_VERSION_NOT_SUPPORTED = 505
export const VARIANT_ALSO_NEGOTIATES = 506
export const INSUFFICIENT_STORAGE = 507
export const LOOP_DETECTED = 508
export const NOT_EXTENDED = 510
export const NETWORK_AUTHENTICATION_REQUIRED = 511
export const NETWORK_CONNECT_TIMEOUT_ERROR = 599

export default {
  CONTINUE,
  SWITCHING_PROTOCOLS,
  PROCESSING,
  OK,
  CREATED,
  ACCEPTED,
  NON_AUTHORITATIVE_INFORMATION,
  NO_CONTENT,
  RESET_CONTENT,
  PARTIAL_CONTENT,
  MULTI_STATUS,
  ALREADY_REPORTED,
  IM_USED,
  MULTIPLE_CHOICES,
  MOVED_PERMANENTLY,
  MOVED_TEMPORARILY,
  SEE_OTHER,
  NOT_MODIFIED,
  USE_PROXY,
  SWITCH_PROXY,
  TEMPORARY_REDIRECT,
  BAD_REQUEST,
  UNAUTHORIZED,
  PAYMENT_REQUIRED,
  FORBIDDEN,
  NOT_FOUND,
  METHOD_NOT_ALLOWED,
  NOT_ACCEPTABLE,
  PROXY_AUTHENTICATION_REQUIRED,
  REQUEST_TIMEOUT,
  CONFLICT,
  GONE,
  LENGTH_REQUIRED,
  PRECONDITION_FAILED,
  PAYLOAD_TOO_LARGE,
  REQUEST_URI_TOO_LONG,
  UNSUPPORTED_MEDIA_TYPE,
  REQUESTED_RANGE_NOT_SATISFIABLE,
  EXPECTATION_FAILED,
  IM_A_TEAPOT,
  METHOD_FAILURE,
  MISDIRECTED_REQUEST,
  UNPROCESSABLE_ENTITY,
  LOCKED,
  FAILED_DEPENDENCY,
  UPGRADE_REQUIRED,
  PRECONDITION_REQUIRED,
  TOO_MANY_REQUESTS,
  REQUEST_HEADER_FIELDS_TOO_LARGE,
  UNAVAILABLE_FOR_LEGAL_REASONS,
  INTERNAL_SERVER_ERROR,
  NOT_IMPLEMENTED,
  BAD_GATEWAY,
  SERVICE_UNAVAILABLE,
  GATEWAY_TIMEOUT,
  HTTP_VERSION_NOT_SUPPORTED,
  VARIANT_ALSO_NEGOTIATES,
  INSUFFICIENT_STORAGE,
  LOOP_DETECTED,
  NOT_EXTENDED,
  NETWORK_AUTHENTICATION_REQUIRED,
  NETWORK_CONNECT_TIMEOUT_ERROR,
}
Enter fullscreen mode Exit fullscreen mode

http.reason.constant.js

export const CONTINUE = 'Continue'
export const SWITCHING_PROTOCOLS = 'Switching Protocols'
export const PROCESSING = 'Processing'
export const OK = 'OK'
export const CREATED = 'Created'
export const ACCEPTED = 'Accepted'
export const NON_AUTHORITATIVE_INFORMATION = 'Non-Authoritative Information'
export const NO_CONTENT = 'No Content'
export const RESET_CONTENT = 'Reset Content'
export const PARTIAL_CONTENT = 'Partial Content'
export const MULTI_STATUS = 'Multi-Status'
export const ALREADY_REPORTED = 'Already Reported'
export const IM_USED = 'IM Used'
export const MULTIPLE_CHOICES = 'Multiple Choices'
export const MOVED_PERMANENTLY = 'Moved Permanently'
export const MOVED_TEMPORARILY = 'Moved Temporarily'
export const SEE_OTHER = 'See Other'
export const NOT_MODIFIED = 'Not Modified'
export const USE_PROXY = 'Use Proxy'
export const SWITCH_PROXY = 'Switch Proxy'
export const TEMPORARY_REDIRECT = 'Temporary Redirect'
export const BAD_REQUEST = 'Bad Request'
export const UNAUTHORIZED = 'Unauthorized'
export const PAYMENT_REQUIRED = 'Payment Required'
export const FORBIDDEN = 'Forbidden'
export const NOT_FOUND = 'Not Found'
export const METHOD_NOT_ALLOWED = 'Method Not Allowed'
export const NOT_ACCEPTABLE = 'Not Acceptable'
export const PROXY_AUTHENTICATION_REQUIRED = 'Proxy Authentication Required'
export const REQUEST_TIMEOUT = 'Request Timeout'
export const CONFLICT = 'Conflict'
export const GONE = 'Gone'
export const LENGTH_REQUIRED = 'Length Required'
export const PRECONDITION_FAILED = 'Precondition Failed'
export const PAYLOAD_TOO_LARGE = 'Payload Too Large'
export const REQUEST_URI_TOO_LONG = 'Request URI Too Long'
export const UNSUPPORTED_MEDIA_TYPE = 'Unsupported Media Type'
export const REQUESTED_RANGE_NOT_SATISFIABLE = 'Requested Range Not Satisfiable'
export const EXPECTATION_FAILED = 'Expectation Failed'
export const IM_A_TEAPOT = "I'm a teapot"
export const METHOD_FAILURE = 'Method Failure'
export const MISDIRECTED_REQUEST = 'Misdirected Request'
export const UNPROCESSABLE_ENTITY = 'Unprocessable Entity'
export const LOCKED = 'Locked'
export const FAILED_DEPENDENCY = 'Failed Dependency'
export const UPGRADE_REQUIRED = 'Upgrade Required'
export const PRECONDITION_REQUIRED = 'Precondition Required'
export const TOO_MANY_REQUESTS = 'Too Many Requests'
export const REQUEST_HEADER_FIELDS_TOO_LARGE = 'Request Header Fields Too Large'
export const UNAVAILABLE_FOR_LEGAL_REASONS = 'Unavailable For Legal Reasons'
export const INTERNAL_SERVER_ERROR = 'Internal Server Error'
export const NOT_IMPLEMENTED = 'Not Implemented'
export const BAD_GATEWAY = 'Bad Gateway'
export const SERVICE_UNAVAILABLE = 'Service Unavailable'
export const GATEWAY_TIMEOUT = 'Gateway Timeout'
export const HTTP_VERSION_NOT_SUPPORTED = 'HTTP Version Not Supported'
export const VARIANT_ALSO_NEGOTIATES = 'Variant Also Negotiates'
export const INSUFFICIENT_STORAGE = 'Insufficient Storage'
export const LOOP_DETECTED = 'Loop Detected'
export const NOT_EXTENDED = 'Not Extended'
export const NETWORK_AUTHENTICATION_REQUIRED = 'Network Authentication Required'
export const NETWORK_CONNECT_TIMEOUT_ERROR = 'Network Connect Timeout Error'

export default {
  CONTINUE,
  SWITCHING_PROTOCOLS,
  PROCESSING,
  OK,
  CREATED,
  ACCEPTED,
  NON_AUTHORITATIVE_INFORMATION,
  NO_CONTENT,
  RESET_CONTENT,
  PARTIAL_CONTENT,
  MULTI_STATUS,
  ALREADY_REPORTED,
  IM_USED,
  MULTIPLE_CHOICES,
  MOVED_PERMANENTLY,
  MOVED_TEMPORARILY,
  SEE_OTHER,
  NOT_MODIFIED,
  USE_PROXY,
  SWITCH_PROXY,
  TEMPORARY_REDIRECT,
  BAD_REQUEST,
  UNAUTHORIZED,
  PAYMENT_REQUIRED,
  FORBIDDEN,
  NOT_FOUND,
  METHOD_NOT_ALLOWED,
  NOT_ACCEPTABLE,
  PROXY_AUTHENTICATION_REQUIRED,
  REQUEST_TIMEOUT,
  CONFLICT,
  GONE,
  LENGTH_REQUIRED,
  PRECONDITION_FAILED,
  PAYLOAD_TOO_LARGE,
  REQUEST_URI_TOO_LONG,
  UNSUPPORTED_MEDIA_TYPE,
  REQUESTED_RANGE_NOT_SATISFIABLE,
  EXPECTATION_FAILED,
  IM_A_TEAPOT,
  METHOD_FAILURE,
  MISDIRECTED_REQUEST,
  UNPROCESSABLE_ENTITY,
  LOCKED,
  FAILED_DEPENDENCY,
  UPGRADE_REQUIRED,
  PRECONDITION_REQUIRED,
  TOO_MANY_REQUESTS,
  REQUEST_HEADER_FIELDS_TOO_LARGE,
  UNAVAILABLE_FOR_LEGAL_REASONS,
  INTERNAL_SERVER_ERROR,
  NOT_IMPLEMENTED,
  BAD_GATEWAY,
  SERVICE_UNAVAILABLE,
  GATEWAY_TIMEOUT,
  HTTP_VERSION_NOT_SUPPORTED,
  VARIANT_ALSO_NEGOTIATES,
  INSUFFICIENT_STORAGE,
  LOOP_DETECTED,
  NOT_EXTENDED,
  NETWORK_AUTHENTICATION_REQUIRED,
  NETWORK_CONNECT_TIMEOUT_ERROR,
}
Enter fullscreen mode Exit fullscreen mode

path.constant.js

export const LOGS_ALL = 'logs/all.log'
export const LOGS_ERROR = 'logs/error.log'

export default {
  LOGS_ALL,
  LOGS_ERROR,
}
Enter fullscreen mode Exit fullscreen mode

and we'll add to other constants in the future.

after creating the directory structure, we'll create a file called .env and .env.example in the root directory:

  • .env: This file will contain the configuration for the application.
  • .env.example: is the file that contains all of the configurations for constants that .env has, but without values; only this one is versioned. env.example serves as a template for building a .env file that contains the information required to start the program.
cd ..

touch .env .env.example
Enter fullscreen mode Exit fullscreen mode

Now we add a new variable to .env:

.env

NODE_ENV=development
# NODE_ENV=production
PORT=3030
DATABASE_URL=mongodb://127.0.0.1:27017/example
Enter fullscreen mode Exit fullscreen mode

.env.example

NODE_ENV=development
# NODE_ENV=production
PORT=3030
DATABASE_URL=mongodb://
Enter fullscreen mode Exit fullscreen mode

after creating the directory structure and .env files, we'll create a file called index.js, bin/www.js, config/db.config.js and env/variable.env.js in the src directory.

touch src/index.js src/bin/www.js src/config/db.config.js src/env/variable.env.js
Enter fullscreen mode Exit fullscreen mode

new files will be created in the src directory, and we'll add the following code to each of them:

db.config.js

import { connect } from 'mongoose'
import logger from '../utils/logger.util'

const connectDb = async (URL) => {
  const connectionParams = {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  }

  try {
    const connection = await connect(URL, connectionParams)
    logger.info(`Mongo DB is connected to: ${connection.connection.host}`)
  } catch (err) {
    logger.error(`An error ocurred\n\r\n\r${err}`)
  }
}

export default connectDb
Enter fullscreen mode Exit fullscreen mode

variable.env.js

import dotenv from 'dotenv'

dotenv.config()

export const { NODE_ENV } = process.env
export const { PORT } = process.env
export const { DATABASE_URL } = process.env

export default {
  NODE_ENV,
  PORT,
  DATABASE_URL,
}
Enter fullscreen mode Exit fullscreen mode

index.js

import express from 'express'
import cors from 'cors'

import { DATABASE_URL } from './env/variable.env'
import connectDb from './config/db.config'

//http constant
import ConstantHttpCode from './constants/http.code.constant'
import ConstantHttpReason from './constants/http.reason.constant'

connectDb(DATABASE_URL)

const app = express()

app.use(express.urlencoded({ extended: true }))
app.use(express.json())
app.use(cors())

app.get('/', (req, res, next) => {
  try {
    res.status(ConstantHttpCode.OK).json({
      status: {
        code: ConstantHttpCode.OK,
        msg: ConstantHttpReason.OK,
      },
      API: 'Work',
    })
  } catch (err) {
    next(err)
  }
})

export default app
Enter fullscreen mode Exit fullscreen mode

bin/www.js

#!/user/bin/env node

import http from 'http'
import app from '..'

import { PORT } from '../env/variable.env'
import logger from '../utils/logger.util'

/**
 * Normalize a port into a number, string, or false.
 */
const normalizePort = (val) => {
  const port = parseInt(val, 10)

  if (Number.isNaN(port)) {
    // named pipe
    return val
  }

  if (port >= 0) {
    // port number
    return port
  }

  return false
}

const port = normalizePort(PORT || '3000')
app.set('port', port)

/**
 * Create HTTP server.
 */
const server = http.createServer(app)

/**
 * Event listener for HTTP server "error" event.
 */
const onError = (error) => {
  if (error.syscall !== 'listen') {
    throw error
  }

  const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      logger.error(`${bind} requires elevated privileges`)
      process.exit(1)
      break
    case 'EADDRINUSE':
      logger.error(`${bind} is already in use`)
      process.exit(1)
      break
    default:
      throw error
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */
const onListening = () => {
  const addr = server.address()
  const bind = typeof addr === 'string' ? `pipe ${addr}` : `port ${addr.port}`
  logger.info(`Listening on ${bind}`)
}

server.listen(port)
server.on('error', onError)
server.on('listening', onListening)

Enter fullscreen mode Exit fullscreen mode

Now we add a new script to package.json so we can run the application:

...
"scripts: {
  ...
  "start": "node build/bin/www.js",
  "dev": "nodemon --exec babel-node src/bin/www.js",
  "clean": "rm -rf build",
  "build": "yarn clean && npx babel src -d build --minified --presets @babel/preset-env",
  ...
}
Enter fullscreen mode Exit fullscreen mode

New you can run the application with yarn start or yarn dev, and you can also run the application with yarn build to create a production version.

yarn dev

yarn start

yarn build
Enter fullscreen mode Exit fullscreen mode

New we'll add a new package:

compression: Your Node.js app's main file contains middleware for compression. GZIP, which supports a variety of compression techniques, will then be enabled. Your JSON response and any static file replies will be smaller as a result.

yarn add compression
Enter fullscreen mode Exit fullscreen mode

cookie-parser: Your Node.js app's main file contains middleware for cookie-parser. This middleware will parse the cookies in the request and set them as properties of the request object.

yarn add cookie-parser
Enter fullscreen mode Exit fullscreen mode

core-js: Your Node.js app's main file contains middleware for core-js. This middleware will add the necessary polyfills to your application.

yarn add core-js
Enter fullscreen mode Exit fullscreen mode

helmet: Your Node.js app's main file contains middleware for helmet. This middleware will add security headers to your application.

yarn add helmet
Enter fullscreen mode Exit fullscreen mode

regenerator-runtime: Your Node.js app's main file contains middleware for regenerator-runtime. This middleware will add the necessary polyfills to your application.

yarn add regenerator-runtime
Enter fullscreen mode Exit fullscreen mode

we'll change the index.js file:

import express from 'express'
import helmet from 'helmet'
import cors from 'cors'
import compression from 'compression'
import cookieParser from 'cookie-parser'

import { DATABASE_URL } from './env/variable.env'
import connectDb from './config/db.config'

//http constant
import ConstantHttpCode from './constants/http.code.constant'
import ConstantHttpReason from './constants/http.reason.constant'

connectDb(DATABASE_URL)

const app = express()

//helmet
app.use(helmet())

app.use(express.urlencoded({ extended: true }))
app.use(express.json())
app.use(compression())
app.use(cors())
app.use(cookieParser())

app.get('/', (req, res, next) => {
  try {
    res.status(ConstantHttpCode.OK).json({
      status: {
        code: ConstantHttpCode.OK,
        msg: ConstantHttpReason.OK,
      },
      API: 'Work',
    })
  } catch (err) {
    next(err)
  }
})

export default app
Enter fullscreen mode Exit fullscreen mode

and we'll change the bin/www.js file:

#!/user/bin/env node
import 'core-js/stable'
import 'regenerator-runtime/runtime'

import http from 'http'
import app from '..'

import { PORT } from '../env/variable.env'
import logger from '../utils/logger.util'

/**
 * Normalize a port into a number, string, or false.
 */
const normalizePort = (val) => {
  const port = parseInt(val, 10)

  if (Number.isNaN(port)) {
    // named pipe
    return val
  }

  if (port >= 0) {
    // port number
    return port
  }

  return false
}

const port = normalizePort(PORT || '3000')
app.set('port', port)

/**
 * Create HTTP server.
 */
const server = http.createServer(app)

/**
 * Event listener for HTTP server "error" event.
 */
const onError = (error) => {
  if (error.syscall !== 'listen') {
    throw error
  }

  const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      logger.error(`${bind} requires elevated privileges`)
      process.exit(1)
      break
    case 'EADDRINUSE':
      logger.error(`${bind} is already in use`)
      process.exit(1)
      break
    default:
      throw error
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */
const onListening = () => {
  const addr = server.address()
  const bind = typeof addr === 'string' ? `pipe ${addr}` : `port ${addr.port}`
  logger.info(`Listening on ${bind}`)
}

server.listen(port)
server.on('error', onError)
server.on('listening', onListening)
Enter fullscreen mode Exit fullscreen mode

new we'll run the application with yarn dev:

╭─mkabumattar@mkabumattar in ~/work/tutorial is  v0.0.0 via  v18.3.0 took 243ms
╰─λ yarn dev
yarn run v1.22.18
$ nodemon --exec babel-node src/bin/www.js
[nodemon] 2.0.16
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `babel-node src/bin/www.js`
2022-06-25 11:57:38:5738 info: Listening on port 3030
2022-06-25 11:57:38:5738 info: Mongo DB is connected to: 127.0.0.1
Enter fullscreen mode Exit fullscreen mode

Git Hooks

Before moving on to component development, there is one more section on configuration. If you want to expand on this project in the future, especially with a team of other developers, keep in mind that you'll want it to be as stable as possible. To get it right from the beginning is time well spent.

We're going to use a program called Husky.

Husky

Husky is a tool for executing scripts at various git stages, such as add, commit, push, etc. We would like to be able to specify requirements and, provided our project is of acceptable quality, only enable actions like commit and push to proceed if our code satisfies those requirements.

To install Husky run

yarn add husky

git init

npx husky install
Enter fullscreen mode Exit fullscreen mode

add .gitignore file:

touch .gitignore
Enter fullscreen mode Exit fullscreen mode

.gitignore

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# vuepress build output
.vuepress/dist

# vuepress v2.x temp and cache directory
.temp
.cache

# Docusaurus cache and generated files
.docusaurus

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
Enter fullscreen mode Exit fullscreen mode

A .husky directory will be created in your project by the second command. Your hooks will be located here. As it is meant for other developers as well as yourself, make sure this directory is included in your code repository.

Add the following script to your package.json file:

package.json

...
"scripts: {
  ...
  "prepare": "husky install"
}
Enter fullscreen mode Exit fullscreen mode

This will ensure Husky gets installed automatically when other developers run the project.

To create a hook run:

npx husky add .husky/pre-commit "yarn lint"
Enter fullscreen mode Exit fullscreen mode

The aforementioned states that the yarn lint script must run and be successful before our commit may be successful. Success here refers to the absence of mistakes. You will be able to get warnings (remember in the ESLint config a setting of 1 is a warning and 2 is an error in case you want to adjust settings).

We're going to add another one:

npx husky add .husky/pre-push "yarn build"
Enter fullscreen mode Exit fullscreen mode

This makes sure that we can't push to the remote repository until our code has built correctly. That sounds like a very acceptable requirement, don't you think? By making this adjustment and attempting to push, feel free to test it.

Commitlint

Finally, we'll add one more tool. Let's make sure that everyone on the team is adhering to them as well (including ourselves! ), since we have been using a uniform format for all of our commit messages so far. For our commit messages, we may add a linter.

yarn add -D @commitlint/config-conventional @commitlint/cli
Enter fullscreen mode Exit fullscreen mode

We will configure it using a set of common defaults, but since I occasionally forget what prefixes are available, I like to explicitly provide that list in a commitlint.config.js file:

touch commitlint.config.js
Enter fullscreen mode Exit fullscreen mode

commitlint.config.js

// build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
// ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
// docs: Documentation only changes
// feat: A new feature
// fix: A bug fix
// perf: A code change that improves performance
// refactor: A code change that neither fixes a bug nor adds a feature
// style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
// test: Adding missing tests or correcting existing tests

module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'body-leading-blank': [1, 'always'],
    'body-max-line-length': [2, 'always', 100],
    'footer-leading-blank': [1, 'always'],
    'footer-max-line-length': [2, 'always', 100],
    'header-max-length': [2, 'always', 100],
    'scope-case': [2, 'always', 'lower-case'],
    'subject-case': [
      2,
      'never',
      ['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
    ],
    'subject-empty': [2, 'never'],
    'subject-full-stop': [2, 'never', '.'],
    'type-case': [2, 'always', 'lower-case'],
    'type-empty': [2, 'never'],
    'type-enum': [
      2,
      'always',
      [
        'build',
        'chore',
        'ci',
        'docs',
        'feat',
        'fix',
        'perf',
        'refactor',
        'revert',
        'style',
        'test',
        'translation',
        'security',
        'changeset',
      ],
    ],
  },
};
Enter fullscreen mode Exit fullscreen mode

Afterward, use Husky to enable commitlint by using:

npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'
Enter fullscreen mode Exit fullscreen mode

Know connected this repository with GitHub, you can now push your commits to GitHub.

echo "# Setting up Node JS, Express, MongoDB, Prettier, ESLint and Husky Application with Babel and authentication as an example" >> README.md
git init
git add README.md
git commit -m "ci: Initial commit"
git branch -M main
git remote add origin git@github.com:<your-github-username>/<your-github-repository-name>.git
git push -u origin main
Enter fullscreen mode Exit fullscreen mode

VS Code

Configuration

We can now take advantage of some useful VS Code functionality to have ESLint and Prettier run automatically since we have implemented them.

Make a settings.json file and a directory called .vscode at the top of your project. This will be a list of values that overrides the VS Code installation's default settings.

Because we may set up particular parameters that only apply to this project and share them with the rest of our team by adding them to the code repository, we want to put them in a folder for the project.

Within settings.json we will add the following values:

mkdir .vscode
touch .vscode/settings.json
Enter fullscreen mode Exit fullscreen mode

settings.json

{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll": true,
    "source.organizeImports": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Debugging

In case we encounter any problems while developing our program, let's set up a handy environment for debugging.

Inside of your .vscode directory create a launch.json file:

touch .vscode/launch.json
Enter fullscreen mode Exit fullscreen mode

launch.json

{
  "version": "0.1.0",
  "configurations": [
    {
      "name": "debug server",
      "type": "node-terminal",
      "request": "launch",
      "command": "yarn dev"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Authentication

Authentication Setup

We'll add a new packages to our project:

  • crypto-js: A JavaScript library for encryption and decryption.
  • jsonwebtoken: A JavaScript library for creating and verifying JSON Web Tokens.
yarn add crypto-js jsonwebtoken
Enter fullscreen mode Exit fullscreen mode

add secret key to .env file:

.env

...
JWT_SECRET=secret
PASS_SECRET=secret
Enter fullscreen mode Exit fullscreen mode

add JWT_SECRET and PASS_SECRET to variable.env.js file:

variable.env.js

...

export const { JWT_SECRET } = process.env
export const { PASS_SECRET } = process.env

export default {
  ...,
  JWT_SECRET,
  PASS_SECRET,
}
Enter fullscreen mode Exit fullscreen mode

know we'll add the constants to:

api.constant.js

// api
export const API_AUTH = '/api/auth'
export const API_USERS = '/api/users'

// auth
export const AUTH_REGISTER = '/register'
export const AUTH_LOGIN = '/login'

// users
export const USER_UPDATE_USERNAME = '/update-username/:id'
export const USER_UPDATE_NAME = '/update-name/:id'
export const USER_UPDATE_EMAIL = '/update-email/:id'
export const USER_UPDATE_PASSWORD = '/update-password/:id'
export const USER_UPDATE_PHONE = '/update-phone/:id'
export const USER_UPDATE_ADDRESS = '/update-address/:id'
export const USER_DELETE = '/delete/:id'
export const USER_GET = '/find/:id'
export const USER_GET_ALL = '/'
export const USER_GET_ALL_STATS = '/stats'

export default {
  // api
  API_AUTH,
  API_USERS,

  // auth
  AUTH_REGISTER,
  AUTH_LOGIN,

  // users
  USER_UPDATE_USERNAME,
  USER_UPDATE_NAME,
  USER_UPDATE_EMAIL,
  USER_UPDATE_PASSWORD,
  USER_UPDATE_PHONE,
  USER_UPDATE_ADDRESS,
  USER_DELETE,
  USER_GET,
  USER_GET_ALL,
  USER_GET_ALL_STATS,
}
Enter fullscreen mode Exit fullscreen mode

message.constant.js

// token
export const TOKEN_NOT_VALID = 'Token not valid'
export const NOT_AUTHENTICATED = 'Not authenticated'
export const NOT_ALLOWED = 'Not allowed'

// auth
export const USERNAME_NOT_VALID = 'username is not valid'
export const NAME_NOT_VALID = 'name is not valid'
export const EMAIL_NOT_VALID = 'email is not valid'
export const PASSWORD_NOT_VALID = 'password is not valid'
export const PHONE_NOT_VALID = 'phone is not valid'
export const ADDRESS_NOT_VALID = 'address is not valid'
export const USERNAME_EXIST = 'username is exist'
export const EMAIL_EXIST = 'email is exist'
export const PHONE_EXIST = 'phone is exist'
export const USER_NOT_CREATE = 'user is not create, please try again'
export const USER_CREATE_SUCCESS = 'user is create success, please login'
export const USER_NOT_FOUND = 'user is not found'
export const PASSWORD_NOT_MATCH = 'password is not match'
export const USER_LOGIN_SUCCESS = 'user is login success'

// user
export const USERNAME_NOT_CHANGE = 'username is not change'
export const USERNAME_CHANGE_SUCCESS = 'username is change success'
export const NAME_NOT_CHANGE = 'name is not change'
export const NAME_CHANGE_SUCCESS = 'name is change success'
export const EMAIL_NOT_CHANGE = 'email is not change'
export const EMAIL_CHANGE_SUCCESS = 'email is change success'
export const PASSWORD_NOT_CHANGE = 'password is not change'
export const PASSWORD_CHANGE_SUCCESS = 'password is change success'
export const PHONE_NOT_CHANGE = 'phone is not change'
export const PHONE_CHANGE_SUCCESS = 'phone is change success'
export const ADDRESS_NOT_CHANGE = 'address is not change'
export const ADDRESS_CHANGE_SUCCESS = 'address is change success'
export const USER_NOT_DELETE = 'user is not delete, please try again'
export const USER_DELETE_SUCCESS = 'user is delete success'
export const USER_FOUND = 'user is found'

export default {
  // token
  TOKEN_NOT_VALID,
  NOT_AUTHENTICATED,
  NOT_ALLOWED,

  // auth
  USERNAME_NOT_VALID,
  NAME_NOT_VALID,
  EMAIL_NOT_VALID,
  PASSWORD_NOT_VALID,
  PHONE_NOT_VALID,
  ADDRESS_NOT_VALID,
  USERNAME_EXIST,
  EMAIL_EXIST,
  PHONE_EXIST,
  USER_NOT_CREATE,
  USER_CREATE_SUCCESS,
  USER_NOT_FOUND,
  PASSWORD_NOT_MATCH,
  USER_LOGIN_SUCCESS,

  // user
  USERNAME_NOT_CHANGE,
  USERNAME_CHANGE_SUCCESS,
  NAME_NOT_CHANGE,
  NAME_CHANGE_SUCCESS,
  EMAIL_NOT_CHANGE,
  EMAIL_CHANGE_SUCCESS,
  PASSWORD_NOT_CHANGE,
  PASSWORD_CHANGE_SUCCESS,
  PHONE_NOT_CHANGE,
  PHONE_CHANGE_SUCCESS,
  ADDRESS_NOT_CHANGE,
  ADDRESS_CHANGE_SUCCESS,
  USER_NOT_DELETE,
  USER_DELETE_SUCCESS,
  USER_FOUND,
}
Enter fullscreen mode Exit fullscreen mode

model.constant.js

export const USER_MODEL = 'UserModel'

export default {
  USER_MODEL,
}
Enter fullscreen mode Exit fullscreen mode

number.constant.js

// user
export const USERNAME_MIN_LENGTH = 3
export const USERNAME_MAX_LENGTH = 20
export const NAME_MIN_LENGTH = 3
export const NAME_MAX_LENGTH = 80
export const EMAIL_MAX_LENGTH = 50
export const PASSWORD_MIN_LENGTH = 8
export const PHONE_MIN_LENGTH = 10
export const PHONE_MAX_LENGTH = 20
export const ADDRESS_MIN_LENGTH = 10
export const ADDRESS_MAX_LENGTH = 200

export default {
  USERNAME_MIN_LENGTH,
  USERNAME_MAX_LENGTH,
  NAME_MIN_LENGTH,
  NAME_MAX_LENGTH,
  EMAIL_MAX_LENGTH,
  PASSWORD_MIN_LENGTH,
  PHONE_MIN_LENGTH,
  PHONE_MAX_LENGTH,
  ADDRESS_MIN_LENGTH,
  ADDRESS_MAX_LENGTH,
}
Enter fullscreen mode Exit fullscreen mode

regex.constant.js

export const USERNAME = /^(?!.*\.\.)(?!.*\.$)[^\W][\w.]{3,32}$/
export const EMAIL =
  /^(([^<>()\\[\]\\.,;:\s@"]+(\.[^<>()\\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
export const PASSWORD =
  /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/
export const NAME = /^[a-zA-Z ]{2,35}$/
export const PHONE =
  /^\s*(?:\+?(\d{1,3}))?([-. (]*(\d{3})[-. )]*)?((\d{3})[-. ]*(\d{2,4})(?:[-.x ]*(\d+))?)\s*$/gm
export const ADDRESS = /^[a-zA-Z0-9\s,'-]{10,200}$/

export default {
  USERNAME,
  EMAIL,
  PASSWORD,
  NAME,
  PHONE,
  ADDRESS,
}
Enter fullscreen mode Exit fullscreen mode

Authentication Middleware

token.middleware.js

import jwt from 'jsonwebtoken'

import ConstantMessage from '../constants/message.constant'
import { JWT_SECRET } from '../env/variable.env'

// http constant
import ConstantHttpCode from '../constants/http.code.constant'
import ConstantHttpReason from '../constants/http.reason.constant'

// logger
import logger from '../utils/logger.util'

export const verifyToken = (req, res, next) => {
  const authHeader = req.headers.token
  logger.info(`authHeader: ${authHeader}`)
  if (authHeader) {
    const token = authHeader.split(' ')[1]
    return jwt.verify(token, JWT_SECRET, (err, user) => {
      if (err) {
        res.status(ConstantHttpCode.FORBIDDEN).json({
          status: {
            code: ConstantHttpCode.FORBIDDEN,
            msg: ConstantHttpReason.FORBIDDEN,
          },
          msg: ConstantMessage.TOKEN_NOT_VALID,
        })
      }
      req.user = user
      return next()
    })
  }

  return res.status(ConstantHttpCode.UNAUTHORIZED).json({
    status: {
      code: ConstantHttpCode.UNAUTHORIZED,
      msg: ConstantHttpReason.UNAUTHORIZED,
    },
    msg: ConstantMessage.NOT_AUTHENTICATED,
  })
}

export const verifyTokenAndAuthorization = (req, res, next) => {
  verifyToken(req, res, () => {
    if (req.user.id === req.params.id || req.user.isAdmin) {
      return next()
    }

    return res.status(ConstantHttpCode.FORBIDDEN).json({
      status: {
        code: ConstantHttpCode.FORBIDDEN,
        msg: ConstantHttpReason.FORBIDDEN,
      },
      msg: ConstantMessage.NOT_ALLOWED,
    })
  })
}

export const verifyTokenAndAdmin = (req, res, next) => {
  verifyToken(req, res, () => {
    if (req.user.isAdmin) {
      return next()
    }

    return res.status(ConstantHttpCode.FORBIDDEN).json({
      status: {
        code: ConstantHttpCode.FORBIDDEN,
        msg: ConstantHttpReason.FORBIDDEN,
      },
      msg: ConstantMessage.NOT_ALLOWED,
    })
  })
}

export default {
  verifyToken,
  verifyTokenAndAuthorization,
  verifyTokenAndAdmin,
}
Enter fullscreen mode Exit fullscreen mode

Authentication Security

user.security.js

import CryptoJS from 'crypto-js'
import jwt from 'jsonwebtoken'

import { JWT_SECRET, PASS_SECRET } from '../env/variable.env'

export const encryptedPassword = (password) => {
  return CryptoJS.AES.encrypt(password, PASS_SECRET).toString()
}

export const decryptedPassword = (password) => {
  return CryptoJS.AES.decrypt(password, PASS_SECRET).toString(CryptoJS.enc.Utf8)
}

export const comparePassword = (password, dPassword) => {
  const compare = decryptedPassword(dPassword) === password
  return compare
}

export const generateAccessToken = (id, isAdmin) => {
  return jwt.sign({ id, isAdmin }, JWT_SECRET, { expiresIn: '3d' })
}

export default {
  encryptedPassword,
  decryptedPassword,
  comparePassword,
  generateAccessToken,
}
Enter fullscreen mode Exit fullscreen mode

Authentication validations

user.validation.js

import ConstantRegex from '../constants/regex.constant'

export const validateUsername = async (username) => {
  return ConstantRegex.USERNAME.test(username)
}

export const validateName = async (name) => {
  return ConstantRegex.NAME.test(name)
}

export const validateEmail = async (email) => {
  return ConstantRegex.EMAIL.test(email)
}

export const validatePassword = async (password) => {
  return ConstantRegex.PASSWORD.test(password)
}

export const validatePhone = async (phone) => {
  return ConstantRegex.PHONE.test(phone)
}

export const validateAddress = async (address) => {
  return ConstantRegex.ADDRESS.test(address)
}

export default {
  validateUsername,
  validateName,
  validateEmail,
  validatePassword,
  validatePhone,
  validateAddress,
}
Enter fullscreen mode Exit fullscreen mode

Authentication schemas

user.schema.js

import mongoose from 'mongoose'
import ConstantNumber from '../constants/number.constant'

const UserSchema = new mongoose.Schema(
  {
    username: {
      type: String,
      required: true,
      unique: true,
      min: ConstantNumber.USERNAME_MIN_LENGTH,
      max: ConstantNumber.USERNAME_MAX_LENGTH,
    },
    name: {
      type: String,
      required: true,
      min: ConstantNumber.NAME_MIN_LENGTH,
      max: ConstantNumber.NAME_MAX_LENGTH,
    },
    email: {
      type: String,
      required: true,
      unique: true,
      max: ConstantNumber.EMAIL_MAX_LENGTH,
    },
    password: {
      type: String,
      required: true,
      min: ConstantNumber.PASSWORD_MIN_LENGTH,
    },
    phone: {
      type: String,
      required: true,
      unique: true,
      min: ConstantNumber.PHONE_MIN_LENGTH,
      max: ConstantNumber.PHONE_MAX_LENGTH,
    },
    address: {
      type: String,
      required: true,
      min: ConstantNumber.ADDRESS_MIN_LENGTH,
      max: ConstantNumber.ADDRESS_MAX_LENGTH,
    },
    isAdmin: {
      type: Boolean,
      default: true,
    },
  },
  {
    versionKey: false,
    timestamps: true,
  },
)

export default UserSchema
Enter fullscreen mode Exit fullscreen mode

Authentication Models

user.model.js

import mongoose from 'mongoose'
import UserSchema from '../schemas/user.schema'

import ConstantModel from '../constants/model.constant'

const UserModel = mongoose.model(ConstantModel.USER_MODEL, UserSchema)

export default UserModel
Enter fullscreen mode Exit fullscreen mode

Authentication Repositories

user.repository.js

import User from '../models/user.model'

export const findAll = async () => {
  const users = await User.find({}).select('-password')
  return users
}

export const findById = async (id) => {
  const user = await User.findById(id).select('-password')
  return user
}

export const findByIdWithPassword = async (id) => {
  const user = await User.findById(id)
  return user
}

export const findByUser = async (username) => {
  const user = await User.findOne({ username }).select('-password')
  return user
}

export const findByEmail = async (email) => {
  const user = await User.findOne({ email }).select('-password')
  return user
}

export const findByEmailWithPassword = async (email) => {
  const user = await User.findOne({ email })
  return user
}

export const findByPhone = async (phone) => {
  const user = await User.findOne({ phone }).select('-password')
  return user
}

export const createUser = async (user) => {
  const newUser = new User({
    username: user.username,
    name: user.name,
    email: user.email,
    password: user.password,
    phone: user.phone,
    address: user.address,
    isAdmin: user.isAdmin,
  })
  const savedUser = await newUser.save()
  return savedUser
}

export const updateUsername = async (id, username) => {
  const user = await User.findByIdAndUpdate(
    id,
    { username },
    { new: true },
  ).select('-password')
  return user
}

export const updateName = async (id, name) => {
  const user = await User.findByIdAndUpdate(id, { name }, { new: true }).select(
    '-password',
  )
  return user
}

export const updateEmail = async (id, email) => {
  const user = await User.findByIdAndUpdate(
    id,
    { email },
    { new: true },
  ).select('-password')
  return user
}

export const updatePassword = async (id, password) => {
  const user = await User.findByIdAndUpdate(
    id,
    { password },
    { new: true },
  ).select('-password')
  return user
}

export const updatePhone = async (id, phone) => {
  const user = await User.findByIdAndUpdate(
    id,
    { phone },
    { new: true },
  ).select('-password')
  return user
}

export const updateAddress = async (id, address) => {
  const user = await User.findByIdAndUpdate(
    id,
    { address },
    { new: true },
  ).select('-password')
  return user
}

export const deleteUser = async (id) => {
  const user = await User.findByIdAndDelete(id)
  return user
}

export const getUsersStats = async (lastYear) => {
  const users = await User.aggregate([
    { $match: { createdAt: { $gte: lastYear } } },
    {
      $project: {
        month: { $month: '$createdAt' },
      },
    },
    {
      $group: {
        _id: '$month',
        total: { $sum: 1 },
      },
    },
  ])
  return users
}

export default {
  findAll,
  findById,
  findByIdWithPassword,
  findByUser,
  findByEmail,
  findByEmailWithPassword,
  findByPhone,
  createUser,
  updateUsername,
  updateName,
  updateEmail,
  updatePassword,
  updatePhone,
  updateAddress,
  deleteUser,
  getUsersStats,
}
Enter fullscreen mode Exit fullscreen mode

Authentication Services

auth.service.js

import UserRepository from '../repositories/user.repository'
import UserSecurity from '../security/user.security'
import UserValidation from '../validations/user.validation'

export const validateUsername = (username) => {
  return UserValidation.validateUsername(username)
}

export const validateName = (name) => {
  return UserValidation.validateName(name)
}

export const validateEmail = (email) => {
  return UserValidation.validateEmail(email)
}

export const validatePassword = (password) => {
  return UserValidation.validatePassword(password)
}

export const comparePassword = (password, encryptedPassword) => {
  return UserSecurity.comparePassword(password, encryptedPassword)
}

export const validatePhone = (phone) => {
  return UserValidation.validatePhone(phone)
}

export const validateAddress = (address) => {
  return UserValidation.validateAddress(address)
}

export const findByUser = async (username) => {
  const user = await UserRepository.findByUser(username)
  return user
}

export const findByEmail = async (email) => {
  const user = await UserRepository.findByEmailWithPassword(email)
  return user
}

export const findByPhone = async (phone) => {
  const user = await UserRepository.findByPhone(phone)
  return user
}

export const createUser = async (user) => {
  const encryptedPassword = UserSecurity.encryptedPassword(user.password)
  const newUser = {
    username: user.username,
    name: user.name,
    email: user.email,
    password: encryptedPassword,
    phone: user.phone,
    address: user.address,
    isAdmin: user.isAdmin,
  }
  const savedUser = await UserRepository.createUser(newUser)
  return savedUser
}

export const generateAccessToken = async (user) => {
  return `Bearer ${UserSecurity.generateAccessToken(user.id, user.isAdmin)}`
}

export default {
  validateUsername,
  validateName,
  validateEmail,
  validatePassword,
  comparePassword,
  validatePhone,
  validateAddress,
  findByUser,
  findByEmail,
  findByPhone,
  createUser,
  generateAccessToken,
}
Enter fullscreen mode Exit fullscreen mode

user.service.js

import UserRepository from '../repositories/user.repository'
import UserSecurity from '../security/user.security'
import UserValidation from '../validations/user.validation'

export const validateUsername = (username) => {
  return UserValidation.validateUsername(username)
}

export const validateName = (name) => {
  return UserValidation.validateName(name)
}

export const validateEmail = (email) => {
  return UserValidation.validateEmail(email)
}

export const validatePassword = (name) => {
  return UserValidation.validatePassword(name)
}

export const comparePassword = (password, encryptedPassword) => {
  return UserSecurity.comparePassword(password, encryptedPassword)
}

export const validatePhone = (phone) => {
  return UserValidation.validatePhone(phone)
}

export const validateAddress = (address) => {
  return UserValidation.validateAddress(address)
}

export const findAll = async () => {
  const users = await UserRepository.findAll()
  return users
}

export const findById = async (id) => {
  const user = await UserRepository.findByIdWithPassword(id)
  return user
}

export const findByIdWithOutPassword = async (id) => {
  const user = await UserRepository.findById(id)
  return user
}

export const findByEmail = async (email) => {
  const user = await UserRepository.findByEmail(email)
  return user
}

export const findByPhone = async (phone) => {
  const user = await UserRepository.findByPhone(phone)
  return user
}

export const findByUser = async (username) => {
  const user = await UserRepository.findByUser(username)
  return user
}

export const updateUsername = async (id, username) => {
  const user = await UserRepository.updateUsername(id, username)
  return user
}

export const updateName = async (id, name) => {
  const user = await UserRepository.updateName(id, name)
  return user
}

export const updateEmail = async (id, email) => {
  const user = await UserRepository.updateEmail(id, email)
  return user
}

export const updatePassword = async (id, password) => {
  const encryptedPassword = UserSecurity.encryptedPassword(password)
  const user = await UserRepository.updatePassword(id, encryptedPassword)
  return user
}

export const updatePhone = async (id, phone) => {
  const user = await UserRepository.updatePhone(id, phone)
  return user
}

export const updateAddress = async (id, address) => {
  const user = await UserRepository.updateAddress(id, address)
  return user
}

export const deleteUser = async (id) => {
  const user = await UserRepository.deleteUser(id)
  return user
}

export const getUsersStats = async () => {
  const users = await UserRepository.getUsersStats()
  return users
}

export default {
  validateUsername,
  validateName,
  validateEmail,
  validatePassword,
  comparePassword,
  validatePhone,
  validateAddress,
  findAll,
  findById,
  findByIdWithOutPassword,
  findByEmail,
  findByPhone,
  findByUser,
  updateUsername,
  updateName,
  updateEmail,
  updatePassword,
  updatePhone,
  updateAddress,
  deleteUser,
  getUsersStats,
}
Enter fullscreen mode Exit fullscreen mode

Authentication Controllers

auth.controller.js

import ConstantMessage from '../constants/message.constant'
import AuthServices from '../services/auth.service'

// http constant
import ConstantHttpCode from '../constants/http.code.constant'
import ConstantHttpReason from '../constants/http.reason.constant'

// logger
import logger from '../utils/logger.util'

export const register = async (req, res, next) => {
  try {
    const { username, name, email, password, phone, address } = req.body

    const usernameValidated = AuthServices.validateUsername(username)
    if (!usernameValidated) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.USERNAME_NOT_VALID,
      })
    }
    logger.info(`username ${username} is valid`)

    const nameValidated = AuthServices.validateName(name)
    if (!nameValidated) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.NAME_NOT_VALID,
      })
    }
    logger.info(`name ${name} is valid`)

    const emailValidated = AuthServices.validateEmail(email)
    if (!emailValidated) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.EMAIL_NOT_VALID,
      })
    }
    logger.info(`email ${email} is valid`)

    const passwordValidated = AuthServices.validatePassword(password)
    if (!passwordValidated) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PASSWORD_NOT_VALID,
      })
    }

    const phoneValidated = AuthServices.validatePhone(phone)
    if (!phoneValidated) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PHONE_NOT_VALID,
      })
    }

    const addressValidated = AuthServices.validateAddress(address)
    if (!addressValidated) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.ADDRESS_NOT_VALID,
      })
    }

    const usernameCheck = await AuthServices.findByUser(username)
    if (usernameCheck) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.USERNAME_EXIST,
      })
    }

    const emailCheck = await AuthServices.findByEmail(email)
    if (emailCheck) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.EMAIL_EXIST,
      })
    }

    const phoneCheck = await AuthServices.findByPhone(phone)
    if (phoneCheck) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PHONE_EXIST,
      })
    }

    const newUserData = {
      username,
      name,
      email,
      password,
      phone,
      address,
    }

    const user = await AuthServices.createUser(newUserData)
    if (!user) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.USER_NOT_CREATE,
      })
    }

    const newUser = { ...user }._doc

    logger.info({ newUserpassword: newUser.password })

    delete newUser.password

    logger.info({ newUserpassword: newUser.password })

    return res.status(ConstantHttpCode.CREATED).json({
      status: {
        code: ConstantHttpCode.CREATED,
        msg: ConstantHttpReason.CREATED,
      },
      msg: ConstantMessage.USER_CREATE_SUCCESS,
      data: user,
    })
  } catch (err) {
    return next(err)
  }
}

export const login = async (req, res, next) => {
  try {
    const { email, password } = req.body

    const emailValidated = AuthServices.validateEmail(email)
    if (!emailValidated) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.EMAIL_NOT_VALID,
      })
    }

    const passwordValidated = AuthServices.validatePassword(password)
    if (!passwordValidated) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PASSWORD_NOT_VALID,
      })
    }

    const user = await AuthServices.findByEmail(email)
    if (!user) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.USER_NOT_FOUND,
      })
    }

    const isMatch = AuthServices.comparePassword(password, user.password)
    if (!isMatch) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PASSWORD_NOT_MATCH,
      })
    }

    const accessToken = await AuthServices.generateAccessToken(user)
    logger.info(`accessToken: ${accessToken}`)

    const newUser = { ...user }._doc

    logger.info({ newUserpassword: newUser.password })

    delete newUser.password

    logger.info({ newUserpassword: newUser.password })

    return res.status(ConstantHttpCode.OK).json({
      status: {
        code: ConstantHttpCode.OK,
        msg: ConstantHttpReason.OK,
      },
      msg: ConstantMessage.LOGIN_SUCCESS,
      data: {
        user,
        accessToken,
      },
    })
  } catch (err) {
    return next(err)
  }
}

export default {
  register,
  login,
}
Enter fullscreen mode Exit fullscreen mode

user.controller.js

import ConstantMessage from '../constants/message.constant'
import UserService from '../services/user.service'

// http constant
import ConstantHttpCode from '../constants/http.code.constant'
import ConstantHttpReason from '../constants/http.reason.constant'

// logger
import logger from '../utils/logger.util'

export const updateUsername = async (req, res, next) => {
  try {
    const { username, password } = req.body
    const { id } = req.params

    const user = await UserService.findById(id)
    if (!user) {
      return res.status(ConstantHttpCode.NOT_FOUND).json({
        status: {
          code: ConstantHttpCode.NOT_FOUND,
          msg: ConstantHttpReason.NOT_FOUND,
        },
        msg: ConstantMessage.USER_NOT_FOUND,
      })
    }
    logger.info(`user ${user.username} found`)

    const usernameValidated = UserService.validateUsername(username)
    if (!usernameValidated) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.USERNAME_NOT_VALID,
      })
    }
    logger.info(`username ${username} is valid`)

    const passwordValidated = UserService.validatePassword(password)
    if (!passwordValidated) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PASSWORD_NOT_VALID,
      })
    }
    logger.info(`password ${password} is valid`)

    const isMatch = UserService.comparePassword(password, user.password)
    if (!isMatch) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PASSWORD_NOT_MATCH,
      })
    }

    const usernameCheck = await UserService.findByUser(username)
    if (usernameCheck) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.USERNAME_EXIST,
      })
    }

    if (user.username === username) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.USERNAME_NOT_CHANGE,
      })
    }

    const updatedUser = await UserService.updateUsername(id, username)
    if (!updatedUser) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.USERNAME_NOT_CHANGE,
      })
    }
    logger.info(`user ${user.username} updated`)

    return res.status(ConstantHttpCode.OK).json({
      status: {
        code: ConstantHttpCode.OK,
        msg: ConstantHttpReason.OK,
      },
      msg: ConstantMessage.USERNAME_CHANGE_SUCCESS,
      data: {
        user: updatedUser,
      },
    })
  } catch (err) {
    return next(err)
  }
}

export const updateName = async (req, res, next) => {
  try {
    const { name, password } = req.body
    const { id } = req.params

    const user = await UserService.findById(id)
    if (!user) {
      return res.status(ConstantHttpCode.NOT_FOUND).json({
        status: {
          code: ConstantHttpCode.NOT_FOUND,
          msg: ConstantHttpReason.NOT_FOUND,
        },
        msg: ConstantMessage.USER_NOT_FOUND,
      })
    }
    logger.info(`user ${user.username} found`)

    const nameValidated = UserService.validateName(name)
    if (!nameValidated) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.NAME_NOT_VALID,
      })
    }

    const passwordValidated = UserService.validatePassword(password)
    if (!passwordValidated) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PASSWORD_NOT_VALID,
      })
    }

    const isMatch = UserService.comparePassword(password, user.password)
    if (!isMatch) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PASSWORD_NOT_MATCH,
      })
    }
    logger.info(`password ${password} is valid`)

    if (user.name === name) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.NAME_NOT_CHANGE,
      })
    }
    logger.info(`name ${name} is valid`)

    const updatedUser = await UserService.updateName(id, name)
    if (!updatedUser) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.NAME_NOT_CHANGE,
      })
    }
    logger.info(`user ${user.username} updated`)

    return res.status(ConstantHttpCode.OK).json({
      status: {
        code: ConstantHttpCode.OK,
        msg: ConstantHttpReason.OK,
      },
      msg: ConstantMessage.NAME_CHANGE_SUCCESS,
      data: {
        user: updatedUser,
      },
    })
  } catch (err) {
    return next(err)
  }
}

export const updateEmail = async (req, res, next) => {
  try {
    const { email, password } = req.body
    const { id } = req.params

    const user = await UserService.findById(id)
    if (!user) {
      return res.status(ConstantHttpCode.NOT_FOUND).json({
        status: {
          code: ConstantHttpCode.NOT_FOUND,
          msg: ConstantHttpReason.NOT_FOUND,
        },
        msg: ConstantMessage.USER_NOT_FOUND,
      })
    }
    logger.info(`user ${user.username} found`)

    const emailValidated = UserService.validateEmail(email)
    if (!emailValidated) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.EMAIL_NOT_VALID,
      })
    }
    logger.info(`email ${email} is valid`)

    const passwordValidated = UserService.validatePassword(password)
    if (!passwordValidated) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PASSWORD_NOT_VALID,
      })
    }
    logger.info(`password ${password} is valid`)

    const isMatch = UserService.comparePassword(password, user.password)
    if (!isMatch) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PASSWORD_NOT_MATCH,
      })
    }
    logger.info(`password ${password} is valid`)

    if (user.email === email) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.EMAIL_NOT_CHANGE,
      })
    }
    logger.info(`email ${email} is valid`)

    const emailCheck = await UserService.findByEmail(email)
    if (emailCheck) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.EMAIL_EXIST,
      })
    }

    const updatedUser = await UserService.updateEmail(id, email)
    if (!updatedUser) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.EMAIL_NOT_CHANGE,
      })
    }
    logger.info(`user ${user.username} updated`)

    return res.status(ConstantHttpCode.OK).json({
      status: {
        code: ConstantHttpCode.OK,
        msg: ConstantHttpReason.OK,
      },
      msg: ConstantMessage.EMAIL_CHANGE_SUCCESS,
      data: {
        user: updatedUser,
      },
    })
  } catch (err) {
    return next(err)
  }
}

export const updatePassword = async (req, res, next) => {
  try {
    const { oldPassword, newPassword, confirmPassword } = req.body
    const { id } = req.params

    if (newPassword !== confirmPassword) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PASSWORD_NOT_MATCH,
      })
    }

    const user = await UserService.findById(id)
    if (!user) {
      return res.status(ConstantHttpCode.NOT_FOUND).json({
        status: {
          code: ConstantHttpCode.NOT_FOUND,
          msg: ConstantHttpReason.NOT_FOUND,
        },
        msg: ConstantMessage.USER_NOT_FOUND,
      })
    }
    logger.info(`user ${user.username} found`)

    const oldPasswordValidated = UserService.validatePassword(oldPassword)
    if (!oldPasswordValidated) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PASSWORD_NOT_VALID,
      })
    }
    logger.info(`password ${oldPassword} is valid`)

    const newPasswordValidated = UserService.validatePassword(newPassword)
    if (!newPasswordValidated) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PASSWORD_NOT_VALID,
      })
    }
    logger.info(`password ${newPassword} is valid`)

    const confirmPasswordValidated =
      UserService.validatePassword(confirmPassword)
    if (!confirmPasswordValidated) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PASSWORD_NOT_VALID,
      })
    }
    logger.info(`password ${confirmPassword} is valid`)

    if (oldPassword === newPassword) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PASSWORD_NOT_CHANGE,
      })
    }

    const isMatch = UserService.comparePassword(oldPassword, user.password)
    if (!isMatch) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PASSWORD_NOT_MATCH,
      })
    }

    const updatedUser = await UserService.updatePassword(id, newPassword)
    if (!updatedUser) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PASSWORD_NOT_CHANGE,
      })
    }
    logger.info(`user ${user.username} updated`)

    return res.status(ConstantHttpCode.OK).json({
      status: {
        code: ConstantHttpCode.OK,
        msg: ConstantHttpReason.OK,
      },
      msg: ConstantMessage.PASSWORD_CHANGE_SUCCESS,
      data: {
        user: updatedUser,
      },
    })
  } catch (err) {
    return next(err)
  }
}

export const updatePhone = async (req, res, next) => {
  try {
    const { phone, password } = req.body
    const { id } = req.params

    const user = await UserService.findById(id)
    if (!user) {
      return res.status(ConstantHttpCode.NOT_FOUND).json({
        status: {
          code: ConstantHttpCode.NOT_FOUND,
          msg: ConstantHttpReason.NOT_FOUND,
        },
        msg: ConstantMessage.USER_NOT_FOUND,
      })
    }
    logger.info(`user ${user.username} found`)

    const phoneValidated = UserService.validatePhone(phone)
    if (!phoneValidated) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PHONE_NOT_VALID,
      })
    }

    const passwordValidated = UserService.validatePassword(password)
    if (!passwordValidated) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PASSWORD_NOT_VALID,
      })
    }
    logger.info(`password ${password} is valid`)

    const isMatch = UserService.comparePassword(password, user.password)
    if (!isMatch) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PASSWORD_NOT_MATCH,
      })
    }
    logger.info(`password ${password} is valid`)

    if (user.phone === phone) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PHONE_NOT_CHANGE,
      })
    }

    const phoneCheck = await UserService.findByPhone(phone)
    if (phoneCheck) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PHONE_EXIST,
      })
    }

    const updatedUser = await UserService.updatePhone(id, phone)
    if (!updatedUser) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PHONE_NOT_CHANGE,
      })
    }
    logger.info(`user ${user.username} updated`)

    return res.status(ConstantHttpCode.OK).json({
      status: {
        code: ConstantHttpCode.OK,
        msg: ConstantHttpReason.OK,
      },
      msg: ConstantMessage.PHONE_CHANGE_SUCCESS,
      data: {
        user: updatedUser,
      },
    })
  } catch (err) {
    return next(err)
  }
}

export const updateAddress = async (req, res, next) => {
  try {
    const { address, password } = req.body
    const { id } = req.params

    const user = await UserService.findById(id)
    if (!user) {
      return res.status(ConstantHttpCode.NOT_FOUND).json({
        status: {
          code: ConstantHttpCode.NOT_FOUND,
          msg: ConstantHttpReason.NOT_FOUND,
        },
        msg: ConstantMessage.USER_NOT_FOUND,
      })
    }
    logger.info(`user ${user.username} found`)

    const addressValidated = UserService.validateAddress(address)
    if (!addressValidated) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.ADDRESS_NOT_VALID,
      })
    }

    const isMatch = UserService.comparePassword(password, user.password)
    if (!isMatch) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.PASSWORD_NOT_MATCH,
      })
    }
    logger.info(`password ${password} is valid`)

    if (user.address === address) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.ADDRESS_NOT_CHANGE,
      })
    }

    const updatedUser = await UserService.updateAddress(id, address)
    if (!updatedUser) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.ADDRESS_NOT_CHANGE,
      })
    }
    logger.info(`user ${user.username} updated`)

    return res.status(ConstantHttpCode.OK).json({
      status: {
        code: ConstantHttpCode.OK,
        msg: ConstantHttpReason.OK,
      },
      msg: ConstantMessage.ADDRESS_CHANGE_SUCCESS,
      data: {
        user: updatedUser,
      },
    })
  } catch (err) {
    return next(err)
  }
}

export const deleteUser = async (req, res, next) => {
  try {
    const { id } = req.params

    const user = await UserService.findById(id)
    if (!user) {
      return res.status(ConstantHttpCode.NOT_FOUND).json({
        status: {
          code: ConstantHttpCode.NOT_FOUND,
          msg: ConstantHttpReason.NOT_FOUND,
        },
        msg: ConstantMessage.USER_NOT_FOUND,
      })
    }
    logger.info(`user ${user.username} found`)

    const deletedUser = await UserService.deleteUser(id)
    if (!deletedUser) {
      return res.status(ConstantHttpCode.BAD_REQUEST).json({
        status: {
          code: ConstantHttpCode.BAD_REQUEST,
          msg: ConstantHttpReason.BAD_REQUEST,
        },
        msg: ConstantMessage.USER_NOT_DELETE,
      })
    }
    logger.info(`user ${user.username} deleted`)

    return res.status(ConstantHttpCode.OK).json({
      status: {
        code: ConstantHttpCode.OK,
        msg: ConstantHttpReason.OK,
      },
      msg: ConstantMessage.USER_DELETE_SUCCESS,
    })
  } catch (err) {
    return next(err)
  }
}

export const getUser = async (req, res, next) => {
  try {
    const { id } = req.params
    logger.info(`user ${id} found`)

    const user = await UserService.findByIdWithOutPassword(id)
    logger.info(`user ${user} found`)
    if (!user) {
      return res.status(ConstantHttpCode.NOT_FOUND).json({
        status: {
          code: ConstantHttpCode.NOT_FOUND,
          msg: ConstantHttpReason.NOT_FOUND,
        },
        msg: ConstantMessage.USER_NOT_FOUND,
      })
    }
    logger.info(`user ${user.username} found`)

    return res.status(ConstantHttpCode.OK).json({
      status: {
        code: ConstantHttpCode.OK,
        msg: ConstantHttpReason.OK,
      },
      msg: ConstantMessage.USER_FOUND,
      data: {
        user,
      },
    })
  } catch (err) {
    return next(err)
  }
}

export const getUsers = async (req, res, next) => {
  try {
    const users = await UserService.findAll()
    if (!users) {
      return res.status(ConstantHttpCode.NOT_FOUND).json({
        status: {
          code: ConstantHttpCode.NOT_FOUND,
          msg: ConstantHttpReason.NOT_FOUND,
        },
        msg: ConstantMessage.USER_NOT_FOUND,
      })
    }
    logger.info(`users found`)

    return res.status(ConstantHttpCode.OK).json({
      status: {
        code: ConstantHttpCode.OK,
        msg: ConstantHttpReason.OK,
      },
      msg: ConstantMessage.USER_FOUND,
      data: {
        users,
      },
    })
  } catch (err) {
    return next(err)
  }
}

export const getUsersStats = async (req, res, next) => {
  try {
    const usersStats = await UserService.getUsersStats()
    if (!usersStats) {
      return res.status(ConstantHttpCode.NOT_FOUND).json({
        status: {
          code: ConstantHttpCode.NOT_FOUND,
          msg: ConstantHttpReason.NOT_FOUND,
        },
        msg: ConstantMessage.USER_NOT_FOUND,
      })
    }
    logger.info(`users stats found`)

    return res.status(ConstantHttpCode.OK).json({
      status: {
        code: ConstantHttpCode.OK,
        msg: ConstantHttpReason.OK,
      },
      msg: ConstantMessage.USER_FOUND,
      data: {
        users: usersStats,
      },
    })
  } catch (err) {
    return next(err)
  }
}

export default {
  updateUsername,
  updateName,
  updateEmail,
  updatePassword,
  updatePhone,
  updateAddress,
  deleteUser,
  getUser,
  getUsers,
  getUsersStats,
}
Enter fullscreen mode Exit fullscreen mode

Authentication Routes

auth.router.js

import express from 'express'

import ConstantAPI from '../constants/api.constant'
import AuthController from '../controllers/auth.controller'

const router = express.Router()

router.post(ConstantAPI.AUTH_REGISTER, AuthController.register)
router.post(ConstantAPI.AUTH_LOGIN, AuthController.login)

export default router
Enter fullscreen mode Exit fullscreen mode

user.router.js

import express from 'express'

import ConstantAPI from '../constants/api.constant'
import UserController from '../controllers/user.controller'
import TokenMiddleware from '../middlewares/token.middleware'

const router = express.Router()

router.post(
  ConstantAPI.USER_UPDATE_USERNAME,
  TokenMiddleware.verifyTokenAndAuthorization,
  UserController.updateUsername,
)
router.post(
  ConstantAPI.USER_UPDATE_NAME,
  TokenMiddleware.verifyTokenAndAuthorization,
  UserController.updateName,
)
router.post(
  ConstantAPI.USER_UPDATE_EMAIL,
  TokenMiddleware.verifyTokenAndAuthorization,
  UserController.updateEmail,
)
router.post(
  ConstantAPI.USER_UPDATE_PASSWORD,
  TokenMiddleware.verifyTokenAndAuthorization,
  UserController.updatePassword,
)
router.post(
  ConstantAPI.USER_UPDATE_PHONE,
  TokenMiddleware.verifyTokenAndAuthorization,
  UserController.updatePhone,
)
router.post(
  ConstantAPI.USER_UPDATE_ADDRESS,
  TokenMiddleware.verifyTokenAndAuthorization,
  UserController.updateAddress,
)
router.post(
  ConstantAPI.USER_DELETE,
  TokenMiddleware.verifyTokenAndAuthorization,
  UserController.deleteUser,
)
router.get(
  ConstantAPI.USER_GET,
  TokenMiddleware.verifyTokenAndAuthorization,
  UserController.getUser,
)
router.get(
  ConstantAPI.USER_GET_ALL,
  TokenMiddleware.verifyTokenAndAdmin,
  UserController.getUsers,
)
router.get(
  ConstantAPI.USER_GET_ALL_STATS,
  TokenMiddleware.verifyTokenAndAdmin,
  UserController.getUsersStats,
)

export default router
Enter fullscreen mode Exit fullscreen mode

edit index.js

import compression from 'compression'
import cookieParser from 'cookie-parser'
import cors from 'cors'
import express from 'express'
import helmet from 'helmet'

import connectDb from './config/db.config'
import { DATABASE_URL } from './env/variable.env'

// http constant
import ConstantHttpCode from './constants/http.code.constant'
import ConstantHttpReason from './constants/http.reason.constant'

// api constant
import ConstantAPI from './constants/api.constant'

// routers
import AuthRouter from './routers/auth.router'
import UserRouter from './routers/user.router'

connectDb(DATABASE_URL)

const app = express()

// helmet
app.use(helmet())

app.use(express.urlencoded({ extended: true }))
app.use(express.json())
app.use(compression())
app.use(cors())
app.use(cookieParser())

app.get('/', (req, res, next) => {
  try {
    return res.status(ConstantHttpCode.OK).json({
      status: {
        code: ConstantHttpCode.OK,
        msg: ConstantHttpReason.OK,
      },
      API: 'Work',
    })
  } catch (err) {
    return next(err)
  }
})

app.use(ConstantAPI.API_AUTH, AuthRouter)
app.use(ConstantAPI.API_USERS, UserRouter)

export default app
Enter fullscreen mode Exit fullscreen mode

Summary

Finally, after compilation, we can now need to deploy the compiled version in the NodeJS production server.

All code from this tutorial as a complete package is available in this repository.

Top comments (0)