DEV Community

Cover image for Setting up JWT Authentication in Typescript with Express, MongoDB, Babel, Prettier, ESLint, and Husky: Part 2
Mohammad Abu Mattar
Mohammad Abu Mattar

Posted on • Originally published at dev.to

Setting up JWT Authentication in Typescript with Express, MongoDB, Babel, Prettier, ESLint, and Husky: Part 2

Table of Contents

Introduction

Why do we even need an authentication mechanism in an application? in my opinion, it doesn't need to be explained. The phrases authentication and authorization have likely crossed your lips, but I must emphasize that they have two distinct meanings.

  • Authentication: Any security approach must start with authentication, verifying that users are who they claim to be.
  • Authorization: Authorization in the context of system security describes the procedure for authorizing user access to a certain resource or function. The words "access control" and "client privilege" are commonly used interchangeably.

At the same time, the words "authentication" and "authorization" are used in the context of network security. In this context, authentication is the process of verifying that a user is who they claim to be. Authorization is the process of verifying that a user has the necessary rights to access a certain resource or function.

We will learn how to create an authentication system using JWT in this tutorial. We will also learn how to create an authorization system using JWT with Typescript, Express. and the tutorial is a continuation of the previous tutorial.

Directory and File Structure

We'll start by creating a directory structure for our application, and then we'll create a file structure for our application.

├── src
│   ├── bin
│   │   └── www.ts
│   ├── config
│   │   └── db.config.ts
│   ├── constants
│   │   ├── api.constant.ts
│   │   ├── dateformat.constant.ts
│   │   ├── http.code.constant.ts
│   │   ├── http.reason.constant.ts
│   │   ├── message.constant.ts
│   │   ├── model.constant.ts
│   │   ├── number.constant.ts
│   │   ├── path.constant.ts
│   │   └── regex.constant.ts
│   ├── controllers
│   │   ├── auth.controller.ts
│   │   └── user.controller.ts
│   ├── env
│   │   └── variable.env.ts
│   ├── interfaces
│   │   ├── controller.interface.ts
│   │   └── user.interface.ts
│   ├── middlewares
│   │   ├── authenticated.middleware.ts
│   │   ├── error.middleware.ts
│   │   └── validation.middleware.ts
│   ├── models
│   │   └── user.model.ts
│   ├── repositories
│   │   └── user.repository.ts
│   ├── schemas
│   │   └── user.schema.ts
│   ├── security
│   │   └── user.security.ts
│   ├── services
│   │   ├── auth.service.ts
│   │   └── user.service.ts
│   ├── types
│   │   └── express
│   │       └── index.d.ts
│   ├── utils
│   │   └── exceptions
│   │   │   └── http.exception.ts
│   │   └── logger.util.ts
│   └── validations
│       ├── token.validation.ts
│       ├── user.validation.ts
│       └── variable.validation.ts
├── .babelrc
├── .env
├── .env.example
├── .eslintignore
├── .eslintrc
├── .gitattributes
├── .gitignore
├── .npmrc
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── commitlint.config.js
├── package.json
├── README.md
├── tsconfig.json
└── yarn.lock
Enter fullscreen mode Exit fullscreen mode

Don't be overwhelmed; this structure will be helpful after the program is finished and you start expanding the file structure for the business logic. This is just my opinion; perhaps you'll organize the directory and files differently.

We'll be continuing build-up in the last tutorial repository.

To better arrange the file structure and identify the key files, certain adjustments will be made to tsconfig.json.

tsconfig.json

{
  ...,
  "paths": {
    "@/bin/*": [
      "bin/*"
    ],
    "@/config/*": [
      "config/*"
    ],
    "@/constants/*": [
      "constants/*"
    ],
    "@/controllers/*": [
      "controllers/*"
    ],
    "@/env/*": [
      "env/*"
    ],
    "@/interfaces/*": [
      "interfaces/*"
    ],
    "@/middlewares/*": [
      "middlewares/*"
    ],
    "@/models/*": [
      "models/*"
    ],
    "@/repositories/*": [
      "repositories/*"
    ],
    "@/routers/*": [
      "routers/*"
    ],
    "@/schemas/*": [
      "schemas/*"
    ],
    "@/security/*": [
      "security/*"
    ],
    "@/services/*": [
      "services/*"
    ],
    "@/utils/*": [
      "utils/*"
    ],
    "@/validations/*": [
      "validations/*"
    ],
  },
}
Enter fullscreen mode Exit fullscreen mode

nevertheless, in order to use the file structure, we must install a package called module-alias. To install the package, use the following command after generating the project:

yarn add module-alias
Enter fullscreen mode Exit fullscreen mode
yarn add -D @types/module-alias
Enter fullscreen mode Exit fullscreen mode

and we need to do some change to package.json and add _moduleAliases:

{
  ...,
    "_moduleAliases": {
    "@/bin": "build/bin",
    "@/config": "build/config",
    "@/constants": "build/constants",
    "@/controllers": "build/controllers",
    "@/env": "build/env",
    "@/interfaces": "build/interfaces",
    "@/middlewares": "build/middlewares",
    "@/models": "build/models",
    "@/repositories": "build/repositories",
    "@/routers": "build/routers",
    "@/schemas": "build/schemas",
    "@/security": "build/security",
    "@/services": "build/services",
    "@/utils": "build/utils",
    "@/validations": "build/validations"
  }
}
Enter fullscreen mode Exit fullscreen mode

Environment Variables

A basic text configuration file called a .env file or dotenv file is used to manage the environment constants for your applications. The vast bulk of your application will remain the same throughout the Local, Staging, and Production environments. However, there are times when some configurations need to be changed between environments in various applications. Typical setup adjustments across contexts might be, but are not restricted to:

  • URL’s and API keys
  • Domain names
  • Public and private authentication keys
  • Service account names

An environment constant is a variable whose value is set outside to the application, generally via operating system capability. Any number of environment variables may be generated and made accessible for use at one time; each environment variable consists of a name/value pair.

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.
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

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

.env.example

NODE_ENV=development
# NODE_ENV=production
PORT=3030
DATABASE_URL=mongodb://

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

They will now be loaded and used using the library dotenv, and environment variables will be verified by a different library called envalid.

yarn add dotenv envalid
Enter fullscreen mode Exit fullscreen mode

variable.validation.ts

import { cleanEnv, str, port } from 'envalid'

const validate = (): void => {
  cleanEnv(process.env, {
    NODE_ENV: str({
      choices: ['development', 'production'],
    }),
    PORT: port({ default: 3030 }),
    DATABASE_URL: str(),
    JWT_SECRET: str(),
    PASS_SECRET: str(),
  })
}

export default validate
Enter fullscreen mode Exit fullscreen mode

variable.env.ts

import 'dotenv/config'

import VariableValidate from '@/validations/variable.validation'

class Variable {
  public static readonly NODE_ENV: string = process.env.NODE_ENV!

  public static readonly PORT: number = Number(process.env.PORT)!

  public static readonly DATABASE_URL: string = process.env.DATABASE_URL!

  public static readonly JWT_SECRET: string = process.env.JWT_SECRET!

  public static readonly PASS_SECRET: string = process.env.PASS_SECRET!

  constructor() {
    this.initialise()
  }

  private initialise(): void {
    VariableValidate()
  }
}

export default Variable
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

We'll begin by introducing the constants, which will be applied as follows:

dateformat.constant.ts

class Dateformat {
  public static readonly YYYY_MM_DD_HH_MM_SS_MS: string =
    'YYYY-MM-DD HH:mm:ss:ms'
}

export default Dateformat
Enter fullscreen mode Exit fullscreen mode

path.constant.ts

class Path {
  public static readonly LOGS_ALL: string = 'logs/all.log'

  public static readonly LOGS_ERROR: string = 'logs/error.log'
}
export default Path
Enter fullscreen mode Exit fullscreen mode

We'll now create the winston as a function to make it simpler to use:

logger.util.ts

import winston from 'winston'
import Variable from '@/env/variable.env'

import ConstantDateFormat from '@/constants/dateformat.constant'
import ConstantPath from '@/constants/path.constant'

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

const level = () => {
  const env = Variable.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: ConstantDateFormat.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: ConstantPath.LOGS_ERROR,
    level: 'error',
  }),
  new winston.transports.File({ filename: ConstantPath.LOGS_ALL }),
]

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

export default logger
Enter fullscreen mode Exit fullscreen mode

Setup MongoDB using Mongoose

What is MongoDB?

MongoDB is a NoSQL database is used to store structured data. It is a document-oriented database made to operate with documents that resemble JSON.

What is Mongoose?

Mongoose is a MongoDB object modeling library. It is a MongoDB driver for Node.js.

first I'll install the package:

yarn add mongoose
Enter fullscreen mode Exit fullscreen mode

We'll begin setting up the mongoose to connect to the database right away:

db.config.ts

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

const connectDb = async (URL: string) => {
  try {
    const connection: any = await connect(URL)
    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

after that, we'll do some changes to index.ts, which is the entry point of the application:

index.ts

import express, { Application, Request, Response, NextFunction } from 'express'

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

import ErrorMiddleware from '@/middlewares/error.middleware'
import HttpException from '@/utils/exceptions/http.exception'
import Controller from '@/interfaces/controller.interface'

import connectDb from '@/config/db.config'

// variable
import Variable from '@/env/variable.env'

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

// message constant
import ConstantMessage from '@/constants/message.constant'

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

class App {
  public app: Application
  private DATABASE_URL: string

  constructor(controllers: Controller[]) {
    this.app = express()
    this.DATABASE_URL = Variable.DATABASE_URL

    this.initialiseDatabaseConnection(this.DATABASE_URL)
    this.initialiseConfig()
    this.initialiseRoutes()
    this.initialiseControllers(controllers)
    this.initialiseErrorHandling()
  }

  private initialiseConfig(): void {
    this.app.use(express.json())
    this.app.use(express.urlencoded({ extended: true }))
    this.app.use(cookieParser())
    this.app.use(compression())
    this.app.use(cors())
    this.app.use(helmet())
  }

  private initialiseRoutes(): void {
    this.app.get(
      ConstantAPI.ROOT,
      (_req: Request, res: Response, next: NextFunction) => {
        try {
          return res.status(ConstantHttpCode.OK).json({
            status: {
              code: ConstantHttpCode.OK,
              msg: ConstantHttpReason.OK,
            },
            msg: ConstantMessage.API_WORKING,
          })
        } catch (err: any) {
          return next(
            new HttpException(
              ConstantHttpCode.INTERNAL_SERVER_ERROR,
              ConstantHttpReason.INTERNAL_SERVER_ERROR,
              err.message,
            ),
          )
        }
      },
    )
  }

  private initialiseControllers(controllers: Controller[]): void {
    controllers.forEach((controller: Controller) => {
      this.app.use(ConstantAPI.API, controller.router)
    })
  }

  private initialiseErrorHandling(): void {
    this.app.use(ErrorMiddleware)
  }

  private initialiseDatabaseConnection(url: string): void {
    connectDb(url)
  }
}

export default App
Enter fullscreen mode Exit fullscreen mode

We will now begin to construct the user schema, but before we do, we must include constants for numbers:

number.constant.ts

class Number {
  // user
  public static readonly USERNAME_MIN_LENGTH: number = 3
  public static readonly USERNAME_MAX_LENGTH: number = 20
  public static readonly NAME_MIN_LENGTH: number = 3
  public static readonly NAME_MAX_LENGTH: number = 80
  public static readonly EMAIL_MAX_LENGTH: number = 50
  public static readonly PASSWORD_MIN_LENGTH: number = 8
  public static readonly PHONE_MIN_LENGTH: number = 10
  public static readonly PHONE_MAX_LENGTH: number = 20
  public static readonly ADDRESS_MIN_LENGTH: number = 10
  public static readonly ADDRESS_MAX_LENGTH: number = 200
}

export default Number
Enter fullscreen mode Exit fullscreen mode

user.schema.ts

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

Now, we must create a model in order to interact with this schema:

model.constant.ts

class Model {
  public static readonly USER_MODEL: string = 'UserModel'
}

export default Model
Enter fullscreen mode Exit fullscreen mode

user.interface.ts

import { Document } from 'mongoose'

export default interface User extends Document {
  username: string
  name: string
  email: string
  password: string
  phone: string
  address: string
  isAdmin: boolean
}
Enter fullscreen mode Exit fullscreen mode

user.model.ts

import mongoose from 'mongoose'

import UserSchema from '@/schemas/user.schema'
import UserInterface from '@/interfaces/user.interface'

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

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

export default UserModel
Enter fullscreen mode Exit fullscreen mode

In my opinion, I build a file to handle all queries in the database through a specific cluster.

user.repository.ts

import User from '@/models/user.model'
import UserInterface from '@/interfaces/user.interface'

class UserRepository {
  public async findAll(): Promise<UserInterface[]> {
    const users = await User.find({}).select('-password')
    return users
  }

  public async findById(id: string): Promise<UserInterface | null> {
    const user = await User.findById(id).select('-password')
    return user
  }

  public async findByUsername(username: string): Promise<UserInterface | null> {
    const user = await User.findOne({ username }).select('-password')
    return user
  }

  public async findByEmail(email: string): Promise<UserInterface | null> {
    const user = await User.findOne({ email }).select('-password')
    return user
  }

  public async findByPhone(phone: string): Promise<UserInterface | null> {
    const user = await User.findOne({ phone }).select('-password')
    return user
  }

  public async findByIdWithPassword(id: string): Promise<UserInterface | null> {
    const user = await User.findById(id)
    return user
  }

  public async findByUsernameWithPassword(
    username: string,
  ): Promise<UserInterface | null> {
    const user = await User.findOne({ username })
    return user
  }

  public async findByEmailWithPassword(
    email: string,
  ): Promise<UserInterface | null> {
    const user = await User.findOne({ email })
    return user
  }

  public async findByPhoneWithPassword(
    phone: string,
  ): Promise<UserInterface | null> {
    const user = await User.findOne({ phone })
    return user
  }

  public async createUser(user: any): Promise<UserInterface | null> {
    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
  }

  public async updateUsername(
    id: string,
    username: string,
  ): Promise<UserInterface | null> {
    const user = await User.findByIdAndUpdate(
      id,
      { username },
      { new: true },
    ).select('-password')
    return user
  }

  public async updateName(
    id: string,
    name: string,
  ): Promise<UserInterface | null> {
    const user = await User.findByIdAndUpdate(
      id,
      { name },
      { new: true },
    ).select('-password')
    return user
  }

  public async updateEmail(
    id: string,
    email: string,
  ): Promise<UserInterface | null> {
    const user = await User.findByIdAndUpdate(
      id,
      { email },
      { new: true },
    ).select('-password')
    return user
  }

  public async updatePassword(
    id: string,
    password: string,
  ): Promise<UserInterface | null> {
    const user = await User.findByIdAndUpdate(
      id,
      { password },
      { new: true },
    ).select('-password')
    return user
  }

  public async updatePhone(
    id: string,
    phone: string,
  ): Promise<UserInterface | null> {
    const user = await User.findByIdAndUpdate(
      id,
      { phone },
      { new: true },
    ).select('-password')
    return user
  }

  public async updateAddress(
    id: string,
    address: string,
  ): Promise<UserInterface | null> {
    const user = await User.findByIdAndUpdate(
      id,
      { address },
      { new: true },
    ).select('-password')
    return user
  }

  public async deleteUser(id: string): Promise<UserInterface | null> {
    const user = await User.findByIdAndDelete(id)
    return user
  }

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

export default UserRepository
Enter fullscreen mode Exit fullscreen mode

Setup Validation using Joi

What is Joi?

Joi is a library that helps you validate data. It is a great tool to validate data before you save it to the database.

first I'll install the package:

yarn add joi
Enter fullscreen mode Exit fullscreen mode

regex.constant.ts

class Regex {
  public static readonly USERNAME = /^(?!.*\.\.)(?!.*\.$)[^\W][\w.]{3,32}$/
  public static readonly 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,}))$/
  public static readonly PASSWORD =
    /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/
  public static readonly NAME = /^[a-zA-Z ]{2,35}$/
  public static readonly PHONE =
    /^\s*(?:\+?(\d{1,3}))?([-. (]*(\d{3})[-. )]*)?((\d{3})[-. ]*(\d{2,4})(?:[-.x ]*(\d+))?)\s*$/
  public static readonly ADDRESS = /^[a-zA-Z0-9\s,'-]{10,200}$/
}

export default Regex
Enter fullscreen mode Exit fullscreen mode

user.validation.ts

import Joi from 'joi'
import ConstantRegex from '@/constants/regex.constant'

class UserValidation {
  public register = Joi.object({
    username: Joi.string().max(30).required(),
    name: Joi.string().max(30).required(),
    email: Joi.string().email().required(),
    password: Joi.string().min(6).max(30).required(),
    phone: Joi.string().min(10).max(15).required(),
    address: Joi.string().max(100).required(),
  })

  public login = Joi.object({
    email: Joi.string().email().required(),
    password: Joi.string().min(6).max(30).required(),
  })

  public updateUsername = Joi.object({
    username: Joi.string().max(30).required(),
    password: Joi.string().min(6).max(30).required(),
  })

  public updateName = Joi.object({
    name: Joi.string().max(30).required(),
    password: Joi.string().min(6).max(30).required(),
  })

  public updateEmail = Joi.object({
    email: Joi.string().email().required(),
    password: Joi.string().min(6).max(30).required(),
  })

  public updatePassword = Joi.object({
    oldPassword: Joi.string().min(6).max(30).required(),
    newPassword: Joi.string().min(6).max(30).required(),
    confirmPassword: Joi.string().min(6).max(30).required(),
  })

  public updatePhone = Joi.object({
    phone: Joi.string().min(10).max(15).required(),
    password: Joi.string().min(6).max(30).required(),
  })

  public updateAddress = Joi.object({
    address: Joi.string().max(100).required(),
    password: Joi.string().min(6).max(30).required(),
  })

  public deleteUser = Joi.object({
    password: Joi.string().min(6).max(30).required(),
  })

  public validateUsername(username: string): boolean {
    return ConstantRegex.USERNAME.test(username)
  }

  public validateName(name: string): boolean {
    return ConstantRegex.NAME.test(name)
  }

  public validateEmail(email: string): boolean {
    return ConstantRegex.EMAIL.test(email)
  }

  public validatePassword(password: string): boolean {
    return ConstantRegex.PASSWORD.test(password)
  }

  public validatePhone(phone: string): boolean {
    return ConstantRegex.PHONE.test(phone)
  }

  public validateAddress(address: string): boolean {
    return ConstantRegex.ADDRESS.test(address)
  }
}

export default UserValidation
Enter fullscreen mode Exit fullscreen mode

Setup JWT Authentication

What is JWT?

JWT is a JSON Web Token. It is a standard for representing claims to be transferred between parties in a secure way.

Alternatively explanation as a book's:

An open industry standard called JSON Web Token is used to exchange data between two entities, often a client (like the front end of your app) and a server (like the back end of your app). They include JSON objects that include the necessary information to be communicated. To ensure that the JSON contents, also known as JWT claims, cannot be changed by the client or an unintentional party, each JWT is additionally signed using cryptography (hashing).

we need to install two libraries:

yarn add jsonwebtoken crypto-js
Enter fullscreen mode Exit fullscreen mode
  • jsonwebtoken is a library that helps you create, sign, and verify JSON Web Tokens.
  • crypto-js is a library that helps you encrypt and decrypt data.

we need to install the types for the library:

yarn add @types/jsonwebtoken @types/crypto-js
Enter fullscreen mode Exit fullscreen mode

to encrypt, decode, and produce an access token, create a file:

user.security.ts

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

import Variable from '@/env/variable.env'

class UserSecurity {
  public encrypt(password: string): string {
    return CryptoJS.AES.encrypt(password, Variable.PASS_SECRET).toString()
  }

  public decrypt(password: string): string {
    return CryptoJS.AES.decrypt(password, Variable.PASS_SECRET).toString(
      CryptoJS.enc.Utf8,
    )
  }

  public comparePassword(password: string, decryptedPassword: string): boolean {
    return password === this.decrypt(decryptedPassword)
  }

  public generateAccessToken(id: string, isAdmin: boolean): string {
    const token = jwt.sign({ id, isAdmin }, Variable.JWT_SECRET, {
      expiresIn: '3d',
    })

    return `Bearer ${token}`
  }
}

export default UserSecurity
Enter fullscreen mode Exit fullscreen mode

message.constant.ts

class Message {
  ...

  // auth
  public static readonly USERNAME_NOT_VALID: string = 'username is not valid'
  public static readonly NAME_NOT_VALID: string = 'name is not valid'
  public static readonly EMAIL_NOT_VALID: string = 'email is not valid'
  public static readonly PASSWORD_NOT_VALID: string = 'password is not valid'
  public static readonly PHONE_NOT_VALID: string = 'phone is not valid'
  public static readonly ADDRESS_NOT_VALID: string = 'address is not valid'
  public static readonly USERNAME_EXIST: string = 'username is exist'
  public static readonly EMAIL_EXIST: string = 'email is exist'
  public static readonly PHONE_EXIST: string = 'phone is exist'
  public static readonly USER_NOT_CREATE: string =
    'user is not create, please try again'
  public static readonly USER_CREATE_SUCCESS: string =
    'user is create success, please login'
  public static readonly USER_NOT_FOUND: string = 'user is not found'
  public static readonly PASSWORD_NOT_MATCH: string = 'password is not match'
  public static readonly USER_LOGIN_SUCCESS: string = 'user is login success'
}
export default Message
Enter fullscreen mode Exit fullscreen mode

using the user validation form joi as input, construct middleware:

validation.middleware.ts

import { Request, Response, NextFunction, RequestHandler } from 'express'
import Joi from 'joi'

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

const validationMiddleware = (schema: Joi.Schema): RequestHandler => {
  return async (
    req: Request,
    res: Response,
    next: NextFunction,
  ): Promise<void> => {
    const validationOptions = {
      abortEarly: false,
      allowUnknown: true,
      stripUnknown: true,
    }

    try {
      const value = await schema.validateAsync(req.body, validationOptions)
      req.body = value
      next()
    } catch (e: any) {
      const errors: string[] = []
      e.details.forEach((error: Joi.ValidationErrorItem) => {
        errors.push(error.message)
      })

      res.status(ConstantHttpCode.NOT_FOUND).send({
        status: {
          code: ConstantHttpCode.NOT_FOUND,
          msg: ConstantHttpReason.NOT_FOUND,
        },
        msg: errors,
      })
    }
  }
}

export default validationMiddleware
Enter fullscreen mode Exit fullscreen mode

We'll now link the repository, security, and a service for authentication:

auth.service.ts

import UserRepository from '@/repositories/user.repository'
import UserSecurity from '@/security/user.security'

class AuthService {
  private userRepository: UserRepository
  private userSecurity: UserSecurity

  constructor() {
    this.userRepository = new UserRepository()
    this.userSecurity = new UserSecurity()
  }

  public async findByUsername(username: string): Promise<any> {
    const user = await this.userRepository.findByUsername(username)
    return user
  }

  public async findByEmail(email: string): Promise<any> {
    const user = await this.userRepository.findByEmail(email)
    return user
  }

  public async findByPhone(phone: string): Promise<any> {
    const user = await this.userRepository.findByPhone(phone)
    return user
  }

  public async findByEmailWithPassword(email: string): Promise<any> {
    const user = await this.userRepository.findByEmailWithPassword(email)
    return user
  }

  public comparePassword(password: string, decryptedPassword: string): boolean {
    return this.userSecurity.comparePassword(password, decryptedPassword)
  }

  public async createUser(user: any): Promise<any> {
    const encryptedPassword = this.userSecurity.encrypt(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 this.userRepository.createUser(newUser)
    return savedUser
  }

  public async generateAccessToken(
    id: string,
    isAdmin: boolean,
  ): Promise<string> {
    const token = this.userSecurity.generateAccessToken(id, isAdmin)
    return token
  }
}

export default AuthService
Enter fullscreen mode Exit fullscreen mode

api.constant.ts

class Api {
  ...

  // auth
  public static readonly AUTH_REGISTER: string = '/register'
  public static readonly AUTH_LOGIN: string = '/login'
}
export default Api
Enter fullscreen mode Exit fullscreen mode

auth.controller.ts

import { Router, Request, Response, NextFunction } from 'express'

import AuthService from '@/services/auth.service'
import Controller from '@/interfaces/controller.interface'

import Validate from '@/validations/user.validation'
import validationMiddleware from '@/middlewares/validation.middleware'

import HttpException from '@/utils/exceptions/http.exception'

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

// message constant
import ConstantMessage from '@/constants/message.constant'

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

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

class AuthController implements Controller {
  public path: string
  public router: Router
  private authService: AuthService
  private validate: Validate

  constructor() {
    this.path = ConstantAPI.AUTH
    this.router = Router()
    this.authService = new AuthService()
    this.validate = new Validate()

    this.initialiseRoutes()
  }

  private initialiseRoutes(): void {
    this.router.post(
      `${this.path}${ConstantAPI.AUTH_REGISTER}`,
      validationMiddleware(this.validate.register),
      this.register,
    )

    this.router.post(
      `${this.path}${ConstantAPI.AUTH_LOGIN}`,
      validationMiddleware(this.validate.login),
      this.login,
    )
  }

  private register = async (
    req: Request,
    res: Response,
    next: NextFunction,
  ): Promise<Response | void> => {
    try {
      const { username, name, email, password, phone, address } = req.body

      const usernameValidated = this.validate.validateUsername(username)
      if (!usernameValidated) {
        return next(
          new HttpException(
            ConstantHttpCode.CONFLICT,
            ConstantHttpReason.CONFLICT,
            ConstantMessage.USERNAME_NOT_VALID,
          ),
        )
      }
      logger.info(`username ${username} is valid`)

      const nameValidated = this.validate.validateName(name)
      if (!nameValidated) {
        return next(
          new HttpException(
            ConstantHttpCode.CONFLICT,
            ConstantHttpReason.CONFLICT,
            ConstantMessage.NAME_NOT_VALID,
          ),
        )
      }
      logger.info(`name ${name} is valid`)

      const emailValidated = this.validate.validateEmail(email)
      if (!emailValidated) {
        return next(
          new HttpException(
            ConstantHttpCode.CONFLICT,
            ConstantHttpReason.CONFLICT,
            ConstantMessage.EMAIL_NOT_VALID,
          ),
        )
      }
      logger.info(`email ${email} is valid`)

      const passwordValidated = this.validate.validatePassword(password)
      if (!passwordValidated) {
        return next(
          new HttpException(
            ConstantHttpCode.CONFLICT,
            ConstantHttpReason.CONFLICT,
            ConstantMessage.PASSWORD_NOT_VALID,
          ),
        )
      }
      logger.info(`password ${password} is valid`)

      const phoneValidated = this.validate.validatePhone(phone)
      if (!phoneValidated) {
        return next(
          new HttpException(
            ConstantHttpCode.CONFLICT,
            ConstantHttpReason.CONFLICT,
            ConstantMessage.PHONE_NOT_VALID,
          ),
        )
      }
      logger.info(`phone ${phone} is valid`)

      const addressValidated = this.validate.validateAddress(address)
      if (!addressValidated) {
        return next(
          new HttpException(
            ConstantHttpCode.CONFLICT,
            ConstantHttpReason.CONFLICT,
            ConstantMessage.ADDRESS_NOT_VALID,
          ),
        )
      }
      logger.info(`address ${address} is valid`)

      const usernameCheck = await this.authService.findByUsername(username)
      if (usernameCheck) {
        return next(
          new HttpException(
            ConstantHttpCode.CONFLICT,
            ConstantHttpReason.CONFLICT,
            ConstantMessage.USERNAME_EXIST,
          ),
        )
      }

      const emailCheck = await this.authService.findByEmail(email)
      if (emailCheck) {
        return next(
          new HttpException(
            ConstantHttpCode.CONFLICT,
            ConstantHttpReason.CONFLICT,
            ConstantMessage.EMAIL_EXIST,
          ),
        )
      }

      const phoneCheck = await this.authService.findByPhone(phone)
      if (phoneCheck) {
        return next(
          new HttpException(
            ConstantHttpCode.CONFLICT,
            ConstantHttpReason.CONFLICT,
            ConstantMessage.PHONE_EXIST,
          ),
        )
      }

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

      const user = await this.authService.createUser(newUserData)
      if (!user) {
        return next(
          new HttpException(
            ConstantHttpCode.CONFLICT,
            ConstantHttpReason.CONFLICT,
            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: newUser,
      })
    } catch (err: any) {
      return next(
        new HttpException(
          ConstantHttpCode.INTERNAL_SERVER_ERROR,
          ConstantHttpReason.INTERNAL_SERVER_ERROR,
          err.message,
        ),
      )
    }
  }

  private login = async (
    req: Request,
    res: Response,
    next: NextFunction,
  ): Promise<Response | void> => {
    try {
      const { email, password } = req.body

      const emailValidated = this.validate.validateEmail(email)
      if (!emailValidated) {
        return next(
          new HttpException(
            ConstantHttpCode.INTERNAL_SERVER_ERROR,
            ConstantHttpReason.INTERNAL_SERVER_ERROR,
            ConstantMessage.EMAIL_NOT_VALID,
          ),
        )
      }
      logger.info(`email ${email} is valid`)

      const passwordValidated = this.validate.validatePassword(password)
      if (!passwordValidated) {
        return next(
          new HttpException(
            ConstantHttpCode.INTERNAL_SERVER_ERROR,
            ConstantHttpReason.INTERNAL_SERVER_ERROR,
            ConstantMessage.PASSWORD_NOT_VALID,
          ),
        )
      }
      logger.info(`password ${password} is valid`)

      const user = await this.authService.findByEmailWithPassword(email)
      if (!user) {
        return next(
          new HttpException(
            ConstantHttpCode.INTERNAL_SERVER_ERROR,
            ConstantHttpReason.INTERNAL_SERVER_ERROR,
            ConstantMessage.USER_NOT_FOUND,
          ),
        )
      }

      const isMatch = this.authService.comparePassword(password, user.password)
      if (!isMatch) {
        return next(
          new HttpException(
            ConstantHttpCode.INTERNAL_SERVER_ERROR,
            ConstantHttpReason.INTERNAL_SERVER_ERROR,
            ConstantMessage.PASSWORD_NOT_MATCH,
          ),
        )
      }

      const accessToken = await this.authService.generateAccessToken(
        user.id,
        user.isAdmin,
      )
      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.USER_LOGIN_SUCCESS,
        data: {
          user: newUser,
          accessToken,
        },
      })
    } catch (err: any) {
      return next(
        new HttpException(
          ConstantHttpCode.INTERNAL_SERVER_ERROR,
          ConstantHttpReason.INTERNAL_SERVER_ERROR,
          err.message,
        ),
      )
    }
  }
}

export default AuthController
Enter fullscreen mode Exit fullscreen mode

We will now develop a validation for each JWT that we receive:

token.validation.ts

import jwt from 'jsonwebtoken'
import { Request, Response, NextFunction } from 'express'
import HttpException from '@/utils/exceptions/http.exception'

// variable
import Variable from '@/env/variable.env'

// message constant
import ConstantMessage from '@/constants/message.constant'

// 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 = async (
  req: Request,
  res: Response,
  next: NextFunction,
) => {
  const bearer = req.headers.authorization
  logger.info(`bearer: ${bearer}`)

  if (!bearer) {
    return next(
      new HttpException(
        ConstantHttpCode.UNAUTHORIZED,
        ConstantHttpReason.UNAUTHORIZED,
        ConstantMessage.TOKEN_NOT_VALID,
      ),
    )
  }

  if (!bearer || !bearer.startsWith('Bearer ')) {
    return next(
      new HttpException(
        ConstantHttpCode.UNAUTHORIZED,
        ConstantHttpReason.UNAUTHORIZED,
        ConstantMessage.UNAUTHORIZED,
      ),
    )
  }

  const accessToken = bearer.split('Bearer ')[1].trim()

  return jwt.verify(accessToken, Variable.JWT_SECRET, (err, user: any) => {
    if (err) {
      res.status(ConstantHttpCode.FORBIDDEN).json({
        status: {
          code: ConstantHttpCode.FORBIDDEN,
          msg: ConstantHttpReason.FORBIDDEN,
        },
        msg: ConstantMessage.TOKEN_NOT_VALID,
      })
    }
    req.user = user
    return next()
  })
}

export default { verifyToken }
Enter fullscreen mode Exit fullscreen mode

Before moving on, we must make a few adjustments to the configuration file and type in the typescript request:

index.d.ts

import User from '@/interfaces/user.interface'

declare global {
  namespace Express {
    export interface Request {
      user: User
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

tsconfig.json

{
  ...,
  "typeRoots": [
    "./src/types",
    "./node_modules/@types"
  ],
  ...
}
Enter fullscreen mode Exit fullscreen mode

message.constant.ts

class Message {
  ...

  // token
  public static readonly TOKEN_NOT_VALID: string = 'Token not valid'
  public static readonly NOT_AUTHENTICATED: string = 'Not authenticated'
  public static readonly UNAUTHORIZED: string = 'Unauthorized'
  public static readonly NOT_ALLOWED: string = 'Not allowed'
}
export default Message
Enter fullscreen mode Exit fullscreen mode

After that, we can develop middleware to check if the request has authorization for the same end point:

authenticated.middleware.ts

import { Request, Response, NextFunction } from 'express'
import HttpException from '@/utils/exceptions/http.exception'

import { verifyToken } from '@/validations/token.validation'

// message constant
import ConstantMessage from '@/constants/message.constant'

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

class AuthenticatedMiddleware {
  public async verifyTokenAndAuthorization(
    req: Request,
    res: Response,
    next: NextFunction,
  ) {
    verifyToken(req, res, () => {
      if (req?.user?.id === req?.params?.id || req?.user?.isAdmin) {
        return next()
      }

      return next(
        new HttpException(
          ConstantHttpCode.FORBIDDEN,
          ConstantHttpReason.FORBIDDEN,
          ConstantMessage.NOT_ALLOWED,
        ),
      )
    })
  }

  public async verifyTokenAndAdmin(
    req: Request,
    res: Response,
    next: NextFunction,
  ) {
    verifyToken(req, res, () => {
      if (req?.user?.isAdmin) {
        return next()
      }

      return next(
        new HttpException(
          ConstantHttpCode.FORBIDDEN,
          ConstantHttpReason.FORBIDDEN,
          ConstantMessage.NOT_ALLOWED,
        ),
      )
    })
  }
}

export default AuthenticatedMiddleware
Enter fullscreen mode Exit fullscreen mode

We’ll now link the repository, security, and service for user:

user.service.ts

import UserRepository from '@/repositories/user.repository'
import UserSecurity from '@/security/user.security'

class UserService {
  private userRepository: UserRepository
  private userSecurity: UserSecurity

  constructor() {
    this.userRepository = new UserRepository()
    this.userSecurity = new UserSecurity()
  }

  public comparePassword(password: string, encryptedPassword: string): boolean {
    return this.userSecurity.comparePassword(password, encryptedPassword)
  }

  public async findAll(): Promise<any> {
    const users = await this.userRepository.findAll()
    return users
  }

  public async findById(id: string): Promise<any> {
    const user = await this.userRepository.findById(id)
    return user
  }

  public async findByUsername(username: string): Promise<any> {
    const user = await this.userRepository.findByUsername(username)
    return user
  }

  public async findByEmail(email: string): Promise<any> {
    const user = await this.userRepository.findByEmail(email)
    return user
  }

  public async findByPhone(phone: string): Promise<any> {
    const user = await this.userRepository.findByPhone(phone)
    return user
  }

  public async findByIdWithPassword(id: string): Promise<any> {
    const user = await this.userRepository.findByIdWithPassword(id)
    return user
  }

  public async updateUsername(id: string, username: string): Promise<any> {
    const user = await this.userRepository.updateUsername(id, username)
    return user
  }

  public async updateName(id: string, name: string): Promise<any> {
    const user = await this.userRepository.updateName(id, name)
    return user
  }

  public async updateEmail(id: string, email: string): Promise<any> {
    const user = await this.userRepository.updateEmail(id, email)
    return user
  }

  public async updatePassword(id: string, password: string): Promise<any> {
    const encryptedPassword = this.userSecurity.encrypt(password)
    const user = await this.userRepository.updatePassword(id, encryptedPassword)
    return user
  }

  public async updatePhone(id: string, phone: string): Promise<any> {
    const user = await this.userRepository.updatePhone(id, phone)
    return user
  }

  public async updateAddress(id: string, address: string): Promise<any> {
    const user = await this.userRepository.updateAddress(id, address)
    return user
  }

  public async deleteUser(id: string): Promise<any> {
    const user = await this.userRepository.deleteUser(id)
    return user
  }

  public async getUsersStats(): Promise<any> {
    const date = new Date()
    const lastYear = new Date(date.setFullYear(date.getFullYear() - 1))
    const usersStats = await this.userRepository.getUsersStats(lastYear)
    return usersStats
  }
}

export default UserService
Enter fullscreen mode Exit fullscreen mode

api.constant.ts

class Api {
  ...

  // users
  public static readonly USER_UPDATE_USERNAME: string = '/update-username/:id'
  public static readonly USER_UPDATE_NAME: string = '/update-name/:id'
  public static readonly USER_UPDATE_EMAIL: string = '/update-email/:id'
  public static readonly USER_UPDATE_PASSWORD: string = '/update-password/:id'
  public static readonly USER_UPDATE_PHONE: string = '/update-phone/:id'
  public static readonly USER_UPDATE_ADDRESS: string = '/update-address/:id'
  public static readonly USER_DELETE: string = '/delete/:id'
  public static readonly USER_GET: string = '/find/:id'
  public static readonly USER_GET_ALL: string = '/'
  public static readonly USER_GET_ALL_STATS: string = '/stats'
}
export default Api
Enter fullscreen mode Exit fullscreen mode

message.constant.ts

class Message {
  ...

  // user
  public static readonly USERNAME_NOT_CHANGE: string = 'username is not change'
  public static readonly USERNAME_CHANGE_SUCCESS: string =
    'username is change success'
  public static readonly NAME_NOT_CHANGE: string = 'name is not change'
  public static readonly NAME_CHANGE_SUCCESS: string = 'name is change success'
  public static readonly EMAIL_NOT_CHANGE: string = 'email is not change'
  public static readonly EMAIL_CHANGE_SUCCESS: string =
    'email is change success'
  public static readonly PASSWORD_NOT_CHANGE: string = 'password is not change'
  public static readonly PASSWORD_CHANGE_SUCCESS: string =
    'password is change success'
  public static readonly PHONE_NOT_CHANGE: string = 'phone is not change'
  public static readonly PHONE_CHANGE_SUCCESS: string =
    'phone is change success'
  public static readonly ADDRESS_NOT_CHANGE: string = 'address is not change'
  public static readonly ADDRESS_CHANGE_SUCCESS: string =
    'address is change success'
  public static readonly USER_NOT_DELETE: string =
    'user is not delete, please try again'
  public static readonly USER_DELETE_SUCCESS: string = 'user is delete success'
  public static readonly USER_FOUND: string = 'user is found'
}
export default Message
Enter fullscreen mode Exit fullscreen mode

To access the service for what we need to build the methods, we'll make a new contact with the middleware for each authorized user and input validation:

user.controller.ts

import { Router, Request, Response, NextFunction } from 'express'

import Controller from '@/interfaces/controller.interface'

import UserService from '@/services/user.service'
import Validate from '@/validations/user.validation'

import Authenticated from '@/middlewares/authenticated.middleware'
import validationMiddleware from '@/middlewares/validation.middleware'

import HttpException from '@/utils/exceptions/http.exception'

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

// message constant
import ConstantMessage from '@/constants/message.constant'

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

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

class UserController implements Controller {
  public path: string
  public router: Router
  private userService: UserService
  private authenticated: Authenticated
  private validate: Validate

  constructor() {
    this.path = ConstantAPI.USERS
    this.router = Router()
    this.userService = new UserService()
    this.authenticated = new Authenticated()
    this.validate = new Validate()

    this.initialiseRoutes()
  }

  private initialiseRoutes(): void {
    this.router.post(
      `${this.path}${ConstantAPI.USER_UPDATE_USERNAME}`,
      this.authenticated.verifyTokenAndAuthorization,
      validationMiddleware(this.validate.updateUsername),
      this.updateUsername,
    )

    this.router.post(
      `${this.path}${ConstantAPI.USER_UPDATE_NAME}`,
      this.authenticated.verifyTokenAndAuthorization,
      validationMiddleware(this.validate.updateName),
      this.updateName,
    )

    this.router.post(
      `${this.path}${ConstantAPI.USER_UPDATE_EMAIL}`,
      this.authenticated.verifyTokenAndAuthorization,
      validationMiddleware(this.validate.updateEmail),
      this.updateEmail,
    )

    this.router.post(
      `${this.path}${ConstantAPI.USER_UPDATE_PASSWORD}`,
      this.authenticated.verifyTokenAndAuthorization,
      validationMiddleware(this.validate.updatePassword),
      this.updatePassword,
    )

    this.router.post(
      `${this.path}${ConstantAPI.USER_UPDATE_PHONE}`,
      this.authenticated.verifyTokenAndAuthorization,
      validationMiddleware(this.validate.updatePhone),
      this.updatePhone,
    )

    this.router.post(
      `${this.path}${ConstantAPI.USER_UPDATE_ADDRESS}`,
      this.authenticated.verifyTokenAndAuthorization,
      validationMiddleware(this.validate.updateAddress),
      this.updateAddress,
    )

    this.router.post(
      `${this.path}${ConstantAPI.USER_DELETE}`,
      this.authenticated.verifyTokenAndAuthorization,
      validationMiddleware(this.validate.deleteUser),
      this.deleteUser,
    )

    this.router.get(
      `${this.path}${ConstantAPI.USER_GET}`,
      this.authenticated.verifyTokenAndAuthorization,
      this.getUser,
    )

    this.router.get(
      `${this.path}${ConstantAPI.USER_GET_ALL}`,
      this.authenticated.verifyTokenAndAdmin,
      this.getAllUsers,
    )

    this.router.get(
      `${this.path}${ConstantAPI.USER_GET_ALL_STATS}`,
      this.authenticated.verifyTokenAndAdmin,
      this.getUsersStats,
    )
  }

  private updateUsername = async (
    req: Request,
    res: Response,
    next: NextFunction,
  ): Promise<Response | void> => {
    try {
      const { username, password } = req.body
      const { id } = req.params

      const user = await this.userService.findByIdWithPassword(id)
      if (!user) {
        return next(
          new HttpException(
            ConstantHttpCode.NOT_FOUND,
            ConstantHttpReason.NOT_FOUND,
            ConstantMessage.USER_NOT_FOUND,
          ),
        )
      }
      logger.info(`user ${user.username} found`)

      const isUsernameValid = this.validate.validateUsername(username)
      if (!isUsernameValid) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.USERNAME_NOT_VALID,
          ),
        )
      }
      logger.info(`username ${username} is valid`)

      const isPasswordValid = this.validate.validatePassword(password)
      if (!isPasswordValid) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.PASSWORD_NOT_VALID,
          ),
        )
      }
      logger.info(`password ${password} is valid`)

      const isMatch = this.userService.comparePassword(password, user.password)
      if (!isMatch) {
        return next(
          new HttpException(
            ConstantHttpCode.UNAUTHORIZED,
            ConstantHttpReason.UNAUTHORIZED,
            ConstantMessage.PASSWORD_NOT_MATCH,
          ),
        )
      }
      logger.info(`password ${password} match`)

      const usernameCheck = await this.userService.findByUsername(username)
      if (usernameCheck) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.USERNAME_EXIST,
          ),
        )
      }

      if (user.username === username) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.USERNAME_NOT_CHANGE,
          ),
        )
      }

      const updatedUser = await this.userService.updateUsername(id, username)
      if (!updatedUser) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            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: any) {
      next(
        new HttpException(
          ConstantHttpCode.INTERNAL_SERVER_ERROR,
          ConstantHttpReason.INTERNAL_SERVER_ERROR,
          err?.message,
        ),
      )
    }
  }

  private updateName = async (
    req: Request,
    res: Response,
    next: NextFunction,
  ): Promise<Response | void> => {
    try {
      const { name, password } = req.body
      const { id } = req.params

      const user = await this.userService.findByIdWithPassword(id)
      if (!user) {
        return next(
          new HttpException(
            ConstantHttpCode.NOT_FOUND,
            ConstantHttpReason.NOT_FOUND,
            ConstantMessage.USER_NOT_FOUND,
          ),
        )
      }
      logger.info(`user ${user.username} found`)

      const isNameValid = this.validate.validateName(name)
      if (!isNameValid) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.NAME_NOT_VALID,
          ),
        )
      }
      logger.info(`name ${name} is valid`)

      const isPasswordValid = this.validate.validatePassword(password)
      if (!isPasswordValid) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.PASSWORD_NOT_VALID,
          ),
        )
      }
      logger.info(`password ${password} is valid`)

      const isMatch = this.userService.comparePassword(password, user.password)
      if (!isMatch) {
        return next(
          new HttpException(
            ConstantHttpCode.UNAUTHORIZED,
            ConstantHttpReason.UNAUTHORIZED,
            ConstantMessage.PASSWORD_NOT_MATCH,
          ),
        )
      }
      logger.info(`password ${password} match`)

      if (user.name === name) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.NAME_NOT_CHANGE,
          ),
        )
      }

      const updatedUser = await this.userService.updateName(id, name)
      if (!updatedUser) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            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: any) {
      next(
        new HttpException(
          ConstantHttpCode.INTERNAL_SERVER_ERROR,
          ConstantHttpReason.INTERNAL_SERVER_ERROR,
          err?.message,
        ),
      )
    }
  }

  private updateEmail = async (
    req: Request,
    res: Response,
    next: NextFunction,
  ): Promise<Response | void> => {
    try {
      const { email, password } = req.body
      const { id } = req.params

      const user = await this.userService.findByIdWithPassword(id)
      if (!user) {
        return next(
          new HttpException(
            ConstantHttpCode.NOT_FOUND,
            ConstantHttpReason.NOT_FOUND,
            ConstantMessage.USER_NOT_FOUND,
          ),
        )
      }

      const isEmailValid = this.validate.validateEmail(email)
      if (!isEmailValid) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.EMAIL_NOT_VALID,
          ),
        )
      }

      const isPasswordValid = this.validate.validatePassword(password)
      if (!isPasswordValid) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.PASSWORD_NOT_VALID,
          ),
        )
      }

      if (user.email === email) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.EMAIL_NOT_CHANGE,
          ),
        )
      }

      const emailCheck = await this.userService.findByEmail(email)
      if (emailCheck) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.EMAIL_EXIST,
          ),
        )
      }

      const isMatch = this.userService.comparePassword(password, user.password)
      if (!isMatch) {
        return next(
          new HttpException(
            ConstantHttpCode.UNAUTHORIZED,
            ConstantHttpReason.UNAUTHORIZED,
            ConstantMessage.PASSWORD_NOT_MATCH,
          ),
        )
      }

      const updatedUser = await this.userService.updateEmail(id, email)
      if (!updatedUser) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.EMAIL_NOT_CHANGE,
          ),
        )
      }

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

  private updatePassword = async (
    req: Request,
    res: Response,
    next: NextFunction,
  ): Promise<Response | void> => {
    try {
      const { oldPassword, newPassword, confirmPassword } = req.body
      const { id } = req.params

      if (newPassword !== confirmPassword) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.PASSWORD_NOT_MATCH,
          ),
        )
      }

      const user = await this.userService.findByIdWithPassword(id)
      if (!user) {
        return next(
          new HttpException(
            ConstantHttpCode.NOT_FOUND,
            ConstantHttpReason.NOT_FOUND,
            ConstantMessage.USER_NOT_FOUND,
          ),
        )
      }

      const isOldPasswordValid = this.validate.validatePassword(oldPassword)
      if (!isOldPasswordValid) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.PASSWORD_NOT_VALID,
          ),
        )
      }

      const isNewPasswordValid = this.validate.validatePassword(newPassword)
      if (!isNewPasswordValid) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.PASSWORD_NOT_VALID,
          ),
        )
      }

      const isConfirmPasswordValid =
        this.validate.validatePassword(confirmPassword)
      if (!isConfirmPasswordValid) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.PASSWORD_NOT_VALID,
          ),
        )
      }

      const isMatch = this.userService.comparePassword(
        oldPassword,
        user.password,
      )
      if (!isMatch) {
        return next(
          new HttpException(
            ConstantHttpCode.UNAUTHORIZED,
            ConstantHttpReason.UNAUTHORIZED,
            ConstantMessage.PASSWORD_NOT_MATCH,
          ),
        )
      }

      if (oldPassword === newPassword) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.PASSWORD_NOT_CHANGE,
          ),
        )
      }

      const updatedUser = await this.userService.updatePassword(id, newPassword)
      if (!updatedUser) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.PASSWORD_NOT_CHANGE,
          ),
        )
      }

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

  private updatePhone = async (
    req: Request,
    res: Response,
    next: NextFunction,
  ): Promise<Response | void> => {
    try {
      const { phone, password } = req.body
      const { id } = req.params

      const user = await this.userService.findByIdWithPassword(id)
      if (!user) {
        return next(
          new HttpException(
            ConstantHttpCode.NOT_FOUND,
            ConstantHttpReason.NOT_FOUND,
            ConstantMessage.USER_NOT_FOUND,
          ),
        )
      }

      const isPhoneValid = this.validate.validatePhone(phone)
      logger.info({ isPhoneValid })
      if (!isPhoneValid) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.PHONE_NOT_VALID,
          ),
        )
      }

      const isPasswordValid = this.validate.validatePassword(password)
      if (!isPasswordValid) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.PASSWORD_NOT_VALID,
          ),
        )
      }

      const phoneCheck = await this.userService.findByPhone(phone)
      if (phoneCheck) {
        return next(
          new HttpException(
            ConstantHttpCode.NOT_FOUND,
            ConstantHttpReason.NOT_FOUND,
            ConstantMessage.PHONE_EXIST,
          ),
        )
      }

      const isMatch = this.userService.comparePassword(password, user.password)
      if (!isMatch) {
        return next(
          new HttpException(
            ConstantHttpCode.UNAUTHORIZED,
            ConstantHttpReason.UNAUTHORIZED,
            ConstantMessage.PASSWORD_NOT_MATCH,
          ),
        )
      }

      if (user.phone === phone) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.PHONE_NOT_CHANGE,
          ),
        )
      }

      const updatedUser = await this.userService.updatePhone(id, phone)
      if (!updatedUser) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.PHONE_NOT_CHANGE,
          ),
        )
      }

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

  private updateAddress = async (
    req: Request,
    res: Response,
    next: NextFunction,
  ): Promise<Response | void> => {
    try {
      const { address, password } = req.body
      const { id } = req.params

      const user = await this.userService.findByIdWithPassword(id)
      if (!user) {
        return next(
          new HttpException(
            ConstantHttpCode.NOT_FOUND,
            ConstantHttpReason.NOT_FOUND,
            ConstantMessage.USER_NOT_FOUND,
          ),
        )
      }

      const isAddressValid = this.validate.validateAddress(address)
      if (!isAddressValid) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.ADDRESS_NOT_VALID,
          ),
        )
      }

      const isPasswordValid = this.validate.validatePassword(password)
      if (!isPasswordValid) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.PASSWORD_NOT_VALID,
          ),
        )
      }

      const isMatch = this.userService.comparePassword(password, user.password)
      if (!isMatch) {
        return next(
          new HttpException(
            ConstantHttpCode.UNAUTHORIZED,
            ConstantHttpReason.UNAUTHORIZED,
            ConstantMessage.PASSWORD_NOT_MATCH,
          ),
        )
      }

      if (user.address === address) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.ADDRESS_NOT_CHANGE,
          ),
        )
      }

      const updatedUser = await this.userService.updateAddress(id, address)
      if (!updatedUser) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.ADDRESS_NOT_CHANGE,
          ),
        )
      }

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

  private deleteUser = async (
    req: Request,
    res: Response,
    next: NextFunction,
  ): Promise<Response | void> => {
    try {
      const { password } = req.body
      const { id } = req.params

      const user = await this.userService.findByIdWithPassword(id)
      if (!user) {
        return next(
          new HttpException(
            ConstantHttpCode.NOT_FOUND,
            ConstantHttpReason.NOT_FOUND,
            ConstantMessage.USER_NOT_FOUND,
          ),
        )
      }

      const isPasswordValid = this.validate.validatePassword(password)
      if (!isPasswordValid) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.PASSWORD_NOT_VALID,
          ),
        )
      }

      const isMatch = this.userService.comparePassword(password, user.password)
      if (!isMatch) {
        return next(
          new HttpException(
            ConstantHttpCode.UNAUTHORIZED,
            ConstantHttpReason.UNAUTHORIZED,
            ConstantMessage.PASSWORD_NOT_MATCH,
          ),
        )
      }

      const deletedUser = await this.userService.deleteUser(id)
      if (!deletedUser) {
        return next(
          new HttpException(
            ConstantHttpCode.BAD_REQUEST,
            ConstantHttpReason.BAD_REQUEST,
            ConstantMessage.USER_NOT_DELETE,
          ),
        )
      }

      return res.status(ConstantHttpCode.OK).json({
        status: {
          code: ConstantHttpCode.OK,
          msg: ConstantHttpReason.OK,
        },
        msg: ConstantMessage.USER_DELETE_SUCCESS,
      })
    } catch (err: any) {
      next(
        new HttpException(
          ConstantHttpCode.INTERNAL_SERVER_ERROR,
          ConstantHttpReason.INTERNAL_SERVER_ERROR,
          err?.message,
        ),
      )
    }
  }

  private getUser = async (
    req: Request,
    res: Response,
    next: NextFunction,
  ): Promise<Response | void> => {
    try {
      const { id } = req.params

      const user = await this.userService.findById(id)
      if (!user) {
        return next(
          new HttpException(
            ConstantHttpCode.NOT_FOUND,
            ConstantHttpReason.NOT_FOUND,
            ConstantMessage.USER_NOT_FOUND,
          ),
        )
      }

      return res.status(ConstantHttpCode.OK).json({
        status: {
          code: ConstantHttpCode.OK,
          msg: ConstantHttpReason.OK,
        },
        msg: ConstantMessage.USER_FOUND,
        data: {
          user,
        },
      })
    } catch (err: any) {
      next(
        new HttpException(
          ConstantHttpCode.INTERNAL_SERVER_ERROR,
          ConstantHttpReason.INTERNAL_SERVER_ERROR,
          err?.message,
        ),
      )
    }
  }

  private getAllUsers = async (
    _req: Request,
    res: Response,
    next: NextFunction,
  ): Promise<Response | void> => {
    try {
      const users = await this.userService.findAll()
      if (!users) {
        return next(
          new HttpException(
            ConstantHttpCode.NOT_FOUND,
            ConstantHttpReason.NOT_FOUND,
            ConstantMessage.USER_NOT_FOUND,
          ),
        )
      }

      return res.status(ConstantHttpCode.OK).json({
        status: {
          code: ConstantHttpCode.OK,
          msg: ConstantHttpReason.OK,
        },
        msg: ConstantMessage.USER_FOUND,
        data: {
          users,
        },
      })
    } catch (err: any) {
      next(
        new HttpException(
          ConstantHttpCode.INTERNAL_SERVER_ERROR,
          ConstantHttpReason.INTERNAL_SERVER_ERROR,
          err?.message,
        ),
      )
    }
  }

  private getUsersStats = async (
    _req: Request,
    res: Response,
    next: NextFunction,
  ): Promise<Response | void> => {
    try {
      const usersStats = await this.userService.getUsersStats()
      if (!usersStats) {
        return next(
          new HttpException(
            ConstantHttpCode.NOT_FOUND,
            ConstantHttpReason.NOT_FOUND,
            ConstantMessage.USER_NOT_FOUND,
          ),
        )
      }

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

export default UserController
Enter fullscreen mode Exit fullscreen mode

api.constant.ts

class Api {
  ...

  public static readonly AUTH: string = `/auth`
  public static readonly USERS: string = '/users'
}
export default Api
Enter fullscreen mode Exit fullscreen mode

after that, we'll do some changes to the www.ts file:

www.ts

#!/usr/bin/env ts-node

import 'module-alias/register'
import 'core-js/stable'
import 'regenerator-runtime/runtime'

import http from 'http'
import App from '..'

import Variable from '@/env/variable.env'
import logger from '@/utils/logger.util'

// controllers
import AuthController from '@/controllers/auth.controller'
import UserController from '@/controllers/user.controller'

const { app } = new App([new AuthController(), new UserController()])

/**
 * Normalize a port into a number, string, or false.
 */
const normalizePort = (val: any) => {
  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(Variable.PORT || '3030')
app.set('port', port)

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

/**
 * Event listener for HTTP server "error" event.
 */
const onError = (error: any) => {
  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

Summary

All in all, you learnt about JWTs and how to develop a router-level middleware for JWT authentication in Node.js and Express.js using TypeScript. If we wanted to authenticate all incoming requests to our API, we could also utilize it as an application-level middleware.

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.

Top comments (1)

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

Hi. You might want to dump dotenv in favor of wj-config. It is far more flexible, it allows the use of almost anything you can think of as configuration source (yes, including environment variables), is hierarchical and can dynamically build URL's for you if you need to configure them in your configuration. Next-generation stuff, I swear. :-)