Type something to search...
Setting up JWT Authentication in Typescript with Express, MongoDB, Babel, Prettier, ESLint, and Husky: Part 2

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

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.

1
├── src
2
│   ├── bin
3
│ │   └── www.ts
4
│ ├── config
5
│ │   └── db.config.ts
6
│   ├── constants
7
│ │   ├── api.constant.ts
8
│ │   ├── dateformat.constant.ts
9
│ │   ├── http.code.constant.ts
10
│ │   ├── http.reason.constant.ts
11
│ │   ├── message.constant.ts
12
│ │   ├── model.constant.ts
13
│ │   ├── number.constant.ts
14
│ │   ├── path.constant.ts
15
│ │   └── regex.constant.ts
16
│   ├── controllers
17
│ │   ├── auth.controller.ts
18
│ │   └── user.controller.ts
19
│   ├── env
20
│ │   └── variable.env.ts
21
│   ├── interfaces
22
│ │   ├── controller.interface.ts
23
│ │   └── user.interface.ts
24
│   ├── middlewares
25
│ │   ├── authenticated.middleware.ts
26
│ │   ├── error.middleware.ts
27
│ │   └── validation.middleware.ts
28
│   ├── models
29
│ │   └── user.model.ts
30
│   ├── repositories
31
│ │   └── user.repository.ts
32
│   ├── schemas
33
│ │   └── user.schema.ts
34
│   ├── security
35
│ │   └── user.security.ts
36
│   ├── services
37
│ │   ├── auth.service.ts
38
│ │   └── user.service.ts
39
│   ├── types
40
│ │   └── express
41
│ │   └── index.d.ts
42
│   ├── utils
43
│ │   └── exceptions
44
│ │ │ └── http.exception.ts
45
│ │   └── logger.util.ts
46
│   └── validations
47
│ ├── token.validation.ts
48
│ ├── user.validation.ts
49
│ └── variable.validation.ts
50
├── .babelrc
51
├── .env
52
├── .env.example
53
├── .eslintignore
54
├── .eslintrc
55
├── .gitattributes
56
├── .gitignore
57
├── .npmrc
58
├── .nvmrc
59
├── .prettierignore
60
├── .prettierrc
61
├── commitlint.config.js
62
├── package.json
63
├── README.md
64
├── tsconfig.json
65
└── yarn.lock

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

1
{
2
...,
3
"paths": {
4
"@/bin/*": [
5
"bin/*"
6
],
7
"@/config/*": [
8
"config/*"
9
],
10
"@/constants/*": [
11
"constants/*"
12
],
13
"@/controllers/*": [
14
"controllers/*"
15
],
16
"@/env/*": [
17
"env/*"
18
],
19
"@/interfaces/*": [
20
"interfaces/*"
21
],
22
"@/middlewares/*": [
23
"middlewares/*"
24
],
25
"@/models/*": [
26
"models/*"
27
],
28
"@/repositories/*": [
29
"repositories/*"
30
],
31
"@/routers/*": [
32
"routers/*"
33
],
34
"@/schemas/*": [
35
"schemas/*"
36
],
37
"@/security/*": [
38
"security/*"
39
],
40
"@/services/*": [
41
"services/*"
42
],
43
"@/utils/*": [
44
"utils/*"
45
],
46
"@/validations/*": [
47
"validations/*"
48
],
49
},
50
}

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:

Terminal window
yarn add module-alias
Terminal window
yarn add -D @types/module-alias

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

1
{
2
...,
3
"_moduleAliases": {
4
"@/bin": "build/bin",
5
"@/config": "build/config",
6
"@/constants": "build/constants",
7
"@/controllers": "build/controllers",
8
"@/env": "build/env",
9
"@/interfaces": "build/interfaces",
10
"@/middlewares": "build/middlewares",
11
"@/models": "build/models",
12
"@/repositories": "build/repositories",
13
"@/routers": "build/routers",
14
"@/schemas": "build/schemas",
15
"@/security": "build/security",
16
"@/services": "build/services",
17
"@/utils": "build/utils",
18
"@/validations": "build/validations"
19
}
20
}

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.
Terminal window
touch .env .env.example

Now we add a new variable to .env:

.env

Terminal window
NODE_ENV=development
# NODE_ENV=production
PORT=3030
DATABASE_URL=mongodb://127.0.0.1:27017/example
JWT_SECRET=secret
PASS_SECRET=secret

.env.example

Terminal window
NODE_ENV=development
# NODE_ENV=production
PORT=3030
DATABASE_URL=mongodb://
JWT_SECRET=secret
PASS_SECRET=secret

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

Terminal window
yarn add dotenv envalid

variable.validation.ts

1
import {cleanEnv, str, port} from 'envalid';
2
3
const validate = (): void => {
4
cleanEnv(process.env, {
5
NODE_ENV: str({
6
choices: ['development', 'production'],
7
}),
8
PORT: port({default: 3030}),
9
DATABASE_URL: str(),
10
JWT_SECRET: str(),
11
PASS_SECRET: str(),
12
});
13
};
14
15
export default validate;

variable.env.ts

1
import 'dotenv/config';
2
3
import VariableValidate from '@/validations/variable.validation';
4
5
class Variable {
6
public static readonly NODE_ENV: string = process.env.NODE_ENV!;
7
8
public static readonly PORT: number = Number(process.env.PORT)!;
9
10
public static readonly DATABASE_URL: string = process.env.DATABASE_URL!;
11
12
public static readonly JWT_SECRET: string = process.env.JWT_SECRET!;
13
14
public static readonly PASS_SECRET: string = process.env.PASS_SECRET!;
15
16
constructor() {
17
this.initialise();
18
}
19
20
private initialise(): void {
21
VariableValidate();
22
}
23
}
24
25
export default Variable;

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:

Terminal window
yarn add winston

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

dateformat.constant.ts

1
class Dateformat {
2
public static readonly YYYY_MM_DD_HH_MM_SS_MS: string =
3
'YYYY-MM-DD HH:mm:ss:ms';
4
}
5
6
export default Dateformat;

path.constant.ts

1
class Path {
2
public static readonly LOGS_ALL: string = 'logs/all.log';
3
4
public static readonly LOGS_ERROR: string = 'logs/error.log';
5
}
6
export default Path;

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

logger.util.ts

1
import winston from 'winston';
2
import Variable from '@/env/variable.env';
3
4
import ConstantDateFormat from '@/constants/dateformat.constant';
5
import ConstantPath from '@/constants/path.constant';
6
7
const levels = {
8
error: 0,
9
warn: 1,
10
info: 2,
11
http: 3,
12
debug: 4,
13
};
14
15
const level = () => {
16
const env = Variable.NODE_ENV || 'development';
17
const isDevelopment = env === 'development';
18
return isDevelopment ? 'debug' : 'warn';
19
};
20
21
const colors = {
22
error: 'red',
23
warn: 'yellow',
24
info: 'green',
25
http: 'magenta',
26
debug: 'white',
27
};
28
29
winston.addColors(colors);
30
31
const format = winston.format.combine(
32
winston.format.timestamp({
33
format: ConstantDateFormat.YYYY_MM_DD_HH_MM_SS_MS,
34
}),
35
winston.format.colorize({all: true}),
36
winston.format.printf(
37
(info) => `${info.timestamp} ${info.level}: ${info.message}`,
38
),
39
);
40
41
const transports = [
42
new winston.transports.Console(),
43
new winston.transports.File({
44
filename: ConstantPath.LOGS_ERROR,
45
level: 'error',
46
}),
47
new winston.transports.File({filename: ConstantPath.LOGS_ALL}),
48
];
49
50
const logger = winston.createLogger({
51
level: level(),
52
levels,
53
format,
54
transports,
55
});
56
57
export default logger;

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:

Terminal window
yarn add mongoose

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

db.config.ts

1
import {connect} from 'mongoose';
2
import logger from '@/utils/logger.util';
3
4
const connectDb = async (URL: string) => {
5
try {
6
const connection: any = await connect(URL);
7
logger.info(`Mongo DB is connected to: ${connection.connection.host}`);
8
} catch (err) {
9
logger.error(`An error ocurred\n\r\n\r${err}`);
10
}
11
};
12
13
export default connectDb;

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

index.ts

1
import express, {Application, Request, Response, NextFunction} from 'express';
2
3
import compression from 'compression';
4
import cookieParser from 'cookie-parser';
5
import cors from 'cors';
6
import helmet from 'helmet';
7
8
import ErrorMiddleware from '@/middlewares/error.middleware';
9
import HttpException from '@/utils/exceptions/http.exception';
10
import Controller from '@/interfaces/controller.interface';
11
12
import connectDb from '@/config/db.config';
13
14
// variable
15
import Variable from '@/env/variable.env';
16
17
// api constant
18
import ConstantAPI from '@/constants/api.constant';
19
20
// message constant
21
import ConstantMessage from '@/constants/message.constant';
22
23
// http constant
24
import ConstantHttpCode from '@/constants/http.code.constant';
25
import ConstantHttpReason from '@/constants/http.reason.constant';
26
27
class App {
28
public app: Application;
29
private DATABASE_URL: string;
30
31
constructor(controllers: Controller[]) {
32
this.app = express();
33
this.DATABASE_URL = Variable.DATABASE_URL;
34
35
this.initialiseDatabaseConnection(this.DATABASE_URL);
36
this.initialiseConfig();
37
this.initialiseRoutes();
38
this.initialiseControllers(controllers);
39
this.initialiseErrorHandling();
40
}
41
42
private initialiseConfig(): void {
43
this.app.use(express.json());
44
this.app.use(express.urlencoded({extended: true}));
45
this.app.use(cookieParser());
46
this.app.use(compression());
47
this.app.use(cors());
48
this.app.use(helmet());
49
}
50
51
private initialiseRoutes(): void {
52
this.app.get(
53
ConstantAPI.ROOT,
54
(_req: Request, res: Response, next: NextFunction) => {
55
try {
56
return res.status(ConstantHttpCode.OK).json({
57
status: {
58
code: ConstantHttpCode.OK,
59
msg: ConstantHttpReason.OK,
60
},
61
msg: ConstantMessage.API_WORKING,
62
});
63
} catch (err: any) {
64
return next(
65
new HttpException(
66
ConstantHttpCode.INTERNAL_SERVER_ERROR,
67
ConstantHttpReason.INTERNAL_SERVER_ERROR,
68
err.message,
69
),
70
);
71
}
72
},
73
);
74
}
75
76
private initialiseControllers(controllers: Controller[]): void {
77
controllers.forEach((controller: Controller) => {
78
this.app.use(ConstantAPI.API, controller.router);
79
});
80
}
81
82
private initialiseErrorHandling(): void {
83
this.app.use(ErrorMiddleware);
84
}
85
86
private initialiseDatabaseConnection(url: string): void {
87
connectDb(url);
88
}
89
}
90
91
export default App;

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

number.constant.ts

1
class Number {
2
// user
3
public static readonly USERNAME_MIN_LENGTH: number = 3;
4
public static readonly USERNAME_MAX_LENGTH: number = 20;
5
public static readonly NAME_MIN_LENGTH: number = 3;
6
public static readonly NAME_MAX_LENGTH: number = 80;
7
public static readonly EMAIL_MAX_LENGTH: number = 50;
8
public static readonly PASSWORD_MIN_LENGTH: number = 8;
9
public static readonly PHONE_MIN_LENGTH: number = 10;
10
public static readonly PHONE_MAX_LENGTH: number = 20;
11
public static readonly ADDRESS_MIN_LENGTH: number = 10;
12
public static readonly ADDRESS_MAX_LENGTH: number = 200;
13
}
14
15
export default Number;

user.schema.ts

1
import mongoose from 'mongoose';
2
import ConstantNumber from '@/constants/number.constant';
3
4
const UserSchema = new mongoose.Schema(
5
{
6
username: {
7
type: String,
8
required: true,
9
unique: true,
10
min: ConstantNumber.USERNAME_MIN_LENGTH,
11
max: ConstantNumber.USERNAME_MAX_LENGTH,
12
},
13
name: {
14
type: String,
15
required: true,
16
min: ConstantNumber.NAME_MIN_LENGTH,
17
max: ConstantNumber.NAME_MAX_LENGTH,
18
},
19
email: {
20
type: String,
21
required: true,
22
unique: true,
23
max: ConstantNumber.EMAIL_MAX_LENGTH,
24
},
25
password: {
26
type: String,
27
required: true,
28
min: ConstantNumber.PASSWORD_MIN_LENGTH,
29
},
30
phone: {
31
type: String,
32
required: true,
33
unique: true,
34
min: ConstantNumber.PHONE_MIN_LENGTH,
35
max: ConstantNumber.PHONE_MAX_LENGTH,
36
},
37
address: {
38
type: String,
39
required: true,
40
min: ConstantNumber.ADDRESS_MIN_LENGTH,
41
max: ConstantNumber.ADDRESS_MAX_LENGTH,
42
},
43
isAdmin: {
44
type: Boolean,
45
default: true,
46
},
47
},
48
{
49
versionKey: false,
50
timestamps: true,
51
},
52
);
53
54
export default UserSchema;

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

model.constant.ts

1
class Model {
2
public static readonly USER_MODEL: string = 'UserModel';
3
}
4
5
export default Model;

user.interface.ts

1
import {Document} from 'mongoose';
2
3
export default interface User extends Document {
4
username: string;
5
name: string;
6
email: string;
7
password: string;
8
phone: string;
9
address: string;
10
isAdmin: boolean;
11
}

user.model.ts

1
import mongoose from 'mongoose';
2
3
import UserSchema from '@/schemas/user.schema';
4
import UserInterface from '@/interfaces/user.interface';
5
6
import ConstantModel from '@/constants/model.constant';
7
8
const UserModel = mongoose.model<UserInterface>(
9
ConstantModel.USER_MODEL,
10
UserSchema,
11
);
12
13
export default UserModel;

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

user.repository.ts

1
import User from '@/models/user.model';
2
import UserInterface from '@/interfaces/user.interface';
3
4
class UserRepository {
5
public async findAll(): Promise<UserInterface[]> {
6
const users = await User.find({}).select('-password');
7
return users;
8
}
9
10
public async findById(id: string): Promise<UserInterface | null> {
11
const user = await User.findById(id).select('-password');
12
return user;
13
}
14
15
public async findByUsername(username: string): Promise<UserInterface | null> {
16
const user = await User.findOne({username}).select('-password');
17
return user;
18
}
19
20
public async findByEmail(email: string): Promise<UserInterface | null> {
21
const user = await User.findOne({email}).select('-password');
22
return user;
23
}
24
25
public async findByPhone(phone: string): Promise<UserInterface | null> {
26
const user = await User.findOne({phone}).select('-password');
27
return user;
28
}
29
30
public async findByIdWithPassword(id: string): Promise<UserInterface | null> {
31
const user = await User.findById(id);
32
return user;
33
}
34
35
public async findByUsernameWithPassword(
36
username: string,
37
): Promise<UserInterface | null> {
38
const user = await User.findOne({username});
39
return user;
40
}
41
42
public async findByEmailWithPassword(
43
email: string,
44
): Promise<UserInterface | null> {
45
const user = await User.findOne({email});
46
return user;
47
}
48
49
public async findByPhoneWithPassword(
50
phone: string,
51
): Promise<UserInterface | null> {
52
const user = await User.findOne({phone});
53
return user;
54
}
55
56
public async createUser(user: any): Promise<UserInterface | null> {
57
const newUser = new User({
58
username: user.username,
59
name: user.name,
60
email: user.email,
61
password: user.password,
62
phone: user.phone,
63
address: user.address,
64
isAdmin: user.isAdmin,
65
});
66
const savedUser = await newUser.save();
67
return savedUser;
68
}
69
70
public async updateUsername(
71
id: string,
72
username: string,
73
): Promise<UserInterface | null> {
74
const user = await User.findByIdAndUpdate(
75
id,
76
{username},
77
{new: true},
78
).select('-password');
79
return user;
80
}
81
82
public async updateName(
83
id: string,
84
name: string,
85
): Promise<UserInterface | null> {
86
const user = await User.findByIdAndUpdate(id, {name}, {new: true}).select(
87
'-password',
88
);
89
return user;
90
}
91
92
public async updateEmail(
93
id: string,
94
email: string,
95
): Promise<UserInterface | null> {
96
const user = await User.findByIdAndUpdate(id, {email}, {new: true}).select(
97
'-password',
98
);
99
return user;
100
}
101
102
public async updatePassword(
103
id: string,
104
password: string,
105
): Promise<UserInterface | null> {
106
const user = await User.findByIdAndUpdate(
107
id,
108
{password},
109
{new: true},
110
).select('-password');
111
return user;
112
}
113
114
public async updatePhone(
115
id: string,
116
phone: string,
117
): Promise<UserInterface | null> {
118
const user = await User.findByIdAndUpdate(id, {phone}, {new: true}).select(
119
'-password',
120
);
121
return user;
122
}
123
124
public async updateAddress(
125
id: string,
126
address: string,
127
): Promise<UserInterface | null> {
128
const user = await User.findByIdAndUpdate(
129
id,
130
{address},
131
{new: true},
132
).select('-password');
133
return user;
134
}
135
136
public async deleteUser(id: string): Promise<UserInterface | null> {
137
const user = await User.findByIdAndDelete(id);
138
return user;
139
}
140
141
public async getUsersStats(lastYear: Date): Promise<UserInterface[] | null> {
142
const users = await User.aggregate([
143
{$match: {createdAt: {$gte: lastYear}}},
144
{
145
$project: {
146
month: {$month: '$createdAt'},
147
},
148
},
149
{
150
$group: {
151
_id: '$month',
152
total: {$sum: 1},
153
},
154
},
155
]);
156
return users;
157
}
158
}
159
160
export default UserRepository;

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:

Terminal window
yarn add joi

regex.constant.ts

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

user.validation.ts

1
import Joi from 'joi';
2
import ConstantRegex from '@/constants/regex.constant';
3
4
class UserValidation {
5
public register = Joi.object({
6
username: Joi.string().max(30).required(),
7
name: Joi.string().max(30).required(),
8
email: Joi.string().email().required(),
9
password: Joi.string().min(6).max(30).required(),
10
phone: Joi.string().min(10).max(15).required(),
11
address: Joi.string().max(100).required(),
12
});
13
14
public login = Joi.object({
15
email: Joi.string().email().required(),
16
password: Joi.string().min(6).max(30).required(),
17
});
18
19
public updateUsername = Joi.object({
20
username: Joi.string().max(30).required(),
21
password: Joi.string().min(6).max(30).required(),
22
});
23
24
public updateName = Joi.object({
25
name: Joi.string().max(30).required(),
26
password: Joi.string().min(6).max(30).required(),
27
});
28
29
public updateEmail = Joi.object({
30
email: Joi.string().email().required(),
31
password: Joi.string().min(6).max(30).required(),
32
});
33
34
public updatePassword = Joi.object({
35
oldPassword: Joi.string().min(6).max(30).required(),
36
newPassword: Joi.string().min(6).max(30).required(),
37
confirmPassword: Joi.string().min(6).max(30).required(),
38
});
39
40
public updatePhone = Joi.object({
41
phone: Joi.string().min(10).max(15).required(),
42
password: Joi.string().min(6).max(30).required(),
43
});
44
45
public updateAddress = Joi.object({
46
address: Joi.string().max(100).required(),
47
password: Joi.string().min(6).max(30).required(),
48
});
49
50
public deleteUser = Joi.object({
51
password: Joi.string().min(6).max(30).required(),
52
});
53
54
public validateUsername(username: string): boolean {
55
return ConstantRegex.USERNAME.test(username);
56
}
57
58
public validateName(name: string): boolean {
59
return ConstantRegex.NAME.test(name);
60
}
61
62
public validateEmail(email: string): boolean {
63
return ConstantRegex.EMAIL.test(email);
64
}
65
66
public validatePassword(password: string): boolean {
67
return ConstantRegex.PASSWORD.test(password);
68
}
69
70
public validatePhone(phone: string): boolean {
71
return ConstantRegex.PHONE.test(phone);
72
}
73
74
public validateAddress(address: string): boolean {
75
return ConstantRegex.ADDRESS.test(address);
76
}
77
}
78
79
export default UserValidation;

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:

Terminal window
yarn add jsonwebtoken crypto-js
  • 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:

Terminal window
yarn add @types/jsonwebtoken @types/crypto-js

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

user.security.ts

1
import CryptoJS from 'crypto-js';
2
import jwt from 'jsonwebtoken';
3
4
import Variable from '@/env/variable.env';
5
6
class UserSecurity {
7
public encrypt(password: string): string {
8
return CryptoJS.AES.encrypt(password, Variable.PASS_SECRET).toString();
9
}
10
11
public decrypt(password: string): string {
12
return CryptoJS.AES.decrypt(password, Variable.PASS_SECRET).toString(
13
CryptoJS.enc.Utf8,
14
);
15
}
16
17
public comparePassword(password: string, decryptedPassword: string): boolean {
18
return password === this.decrypt(decryptedPassword);
19
}
20
21
public generateAccessToken(id: string, isAdmin: boolean): string {
22
const token = jwt.sign({id, isAdmin}, Variable.JWT_SECRET, {
23
expiresIn: '3d',
24
});
25
26
return `Bearer ${token}`;
27
}
28
}
29
30
export default UserSecurity;

message.constant.ts

1
class Message {
2
...
3
4
// auth
5
public static readonly USERNAME_NOT_VALID: string = 'username is not valid'
6
public static readonly NAME_NOT_VALID: string = 'name is not valid'
7
public static readonly EMAIL_NOT_VALID: string = 'email is not valid'
8
public static readonly PASSWORD_NOT_VALID: string = 'password is not valid'
9
public static readonly PHONE_NOT_VALID: string = 'phone is not valid'
10
public static readonly ADDRESS_NOT_VALID: string = 'address is not valid'
11
public static readonly USERNAME_EXIST: string = 'username is exist'
12
public static readonly EMAIL_EXIST: string = 'email is exist'
13
public static readonly PHONE_EXIST: string = 'phone is exist'
14
public static readonly USER_NOT_CREATE: string =
15
'user is not create, please try again'
16
public static readonly USER_CREATE_SUCCESS: string =
17
'user is create success, please login'
18
public static readonly USER_NOT_FOUND: string = 'user is not found'
19
public static readonly PASSWORD_NOT_MATCH: string = 'password is not match'
20
public static readonly USER_LOGIN_SUCCESS: string = 'user is login success'
21
}
22
export default Message

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

validation.middleware.ts

1
import {Request, Response, NextFunction, RequestHandler} from 'express';
2
import Joi from 'joi';
3
4
// http constant
5
import ConstantHttpCode from '@/constants/http.code.constant';
6
import ConstantHttpReason from '@/constants/http.reason.constant';
7
8
const validationMiddleware = (schema: Joi.Schema): RequestHandler => {
9
return async (
10
req: Request,
11
res: Response,
12
next: NextFunction,
13
): Promise<void> => {
14
const validationOptions = {
15
abortEarly: false,
16
allowUnknown: true,
17
stripUnknown: true,
18
};
19
20
try {
21
const value = await schema.validateAsync(req.body, validationOptions);
22
req.body = value;
23
next();
24
} catch (e: any) {
25
const errors: string[] = [];
26
e.details.forEach((error: Joi.ValidationErrorItem) => {
27
errors.push(error.message);
28
});
29
30
res.status(ConstantHttpCode.NOT_FOUND).send({
31
status: {
32
code: ConstantHttpCode.NOT_FOUND,
33
msg: ConstantHttpReason.NOT_FOUND,
34
},
35
msg: errors,
36
});
37
}
38
};
39
};
40
41
export default validationMiddleware;

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

auth.service.ts

1
import UserRepository from '@/repositories/user.repository';
2
import UserSecurity from '@/security/user.security';
3
4
class AuthService {
5
private userRepository: UserRepository;
6
private userSecurity: UserSecurity;
7
8
constructor() {
9
this.userRepository = new UserRepository();
10
this.userSecurity = new UserSecurity();
11
}
12
13
public async findByUsername(username: string): Promise<any> {
14
const user = await this.userRepository.findByUsername(username);
15
return user;
16
}
17
18
public async findByEmail(email: string): Promise<any> {
19
const user = await this.userRepository.findByEmail(email);
20
return user;
21
}
22
23
public async findByPhone(phone: string): Promise<any> {
24
const user = await this.userRepository.findByPhone(phone);
25
return user;
26
}
27
28
public async findByEmailWithPassword(email: string): Promise<any> {
29
const user = await this.userRepository.findByEmailWithPassword(email);
30
return user;
31
}
32
33
public comparePassword(password: string, decryptedPassword: string): boolean {
34
return this.userSecurity.comparePassword(password, decryptedPassword);
35
}
36
37
public async createUser(user: any): Promise<any> {
38
const encryptedPassword = this.userSecurity.encrypt(user.password);
39
const newUser = {
40
username: user.username,
41
name: user.name,
42
email: user.email,
43
password: encryptedPassword,
44
phone: user.phone,
45
address: user.address,
46
isAdmin: user.isAdmin,
47
};
48
const savedUser = await this.userRepository.createUser(newUser);
49
return savedUser;
50
}
51
52
public async generateAccessToken(
53
id: string,
54
isAdmin: boolean,
55
): Promise<string> {
56
const token = this.userSecurity.generateAccessToken(id, isAdmin);
57
return token;
58
}
59
}
60
61
export default AuthService;

api.constant.ts

1
class Api {
2
...
3
4
// auth
5
public static readonly AUTH_REGISTER: string = '/register'
6
public static readonly AUTH_LOGIN: string = '/login'
7
}
8
export default Api

auth.controller.ts

1
import {Router, Request, Response, NextFunction} from 'express';
2
3
import AuthService from '@/services/auth.service';
4
import Controller from '@/interfaces/controller.interface';
5
6
import Validate from '@/validations/user.validation';
7
import validationMiddleware from '@/middlewares/validation.middleware';
8
9
import HttpException from '@/utils/exceptions/http.exception';
10
11
// api constant
12
import ConstantAPI from '@/constants/api.constant';
13
14
// message constant
15
import ConstantMessage from '@/constants/message.constant';
16
17
// http constant
18
import ConstantHttpCode from '@/constants/http.code.constant';
19
import ConstantHttpReason from '@/constants/http.reason.constant';
20
21
// logger
22
import logger from '@/utils/logger.util';
23
24
class AuthController implements Controller {
25
public path: string;
26
public router: Router;
27
private authService: AuthService;
28
private validate: Validate;
29
30
constructor() {
31
this.path = ConstantAPI.AUTH;
32
this.router = Router();
33
this.authService = new AuthService();
34
this.validate = new Validate();
35
36
this.initialiseRoutes();
37
}
38
39
private initialiseRoutes(): void {
40
this.router.post(
41
`${this.path}${ConstantAPI.AUTH_REGISTER}`,
42
validationMiddleware(this.validate.register),
43
this.register,
44
);
45
46
this.router.post(
47
`${this.path}${ConstantAPI.AUTH_LOGIN}`,
48
validationMiddleware(this.validate.login),
49
this.login,
50
);
51
}
52
53
private register = async (
54
req: Request,
55
res: Response,
56
next: NextFunction,
57
): Promise<Response | void> => {
58
try {
59
const {username, name, email, password, phone, address} = req.body;
60
61
const usernameValidated = this.validate.validateUsername(username);
62
if (!usernameValidated) {
63
return next(
64
new HttpException(
65
ConstantHttpCode.CONFLICT,
66
ConstantHttpReason.CONFLICT,
67
ConstantMessage.USERNAME_NOT_VALID,
68
),
69
);
70
}
71
logger.info(`username ${username} is valid`);
72
73
const nameValidated = this.validate.validateName(name);
74
if (!nameValidated) {
75
return next(
76
new HttpException(
77
ConstantHttpCode.CONFLICT,
78
ConstantHttpReason.CONFLICT,
79
ConstantMessage.NAME_NOT_VALID,
80
),
81
);
82
}
83
logger.info(`name ${name} is valid`);
84
85
const emailValidated = this.validate.validateEmail(email);
86
if (!emailValidated) {
87
return next(
88
new HttpException(
89
ConstantHttpCode.CONFLICT,
90
ConstantHttpReason.CONFLICT,
91
ConstantMessage.EMAIL_NOT_VALID,
92
),
93
);
94
}
95
logger.info(`email ${email} is valid`);
96
97
const passwordValidated = this.validate.validatePassword(password);
98
if (!passwordValidated) {
99
return next(
100
new HttpException(
101
ConstantHttpCode.CONFLICT,
102
ConstantHttpReason.CONFLICT,
103
ConstantMessage.PASSWORD_NOT_VALID,
104
),
105
);
106
}
107
logger.info(`password ${password} is valid`);
108
109
const phoneValidated = this.validate.validatePhone(phone);
110
if (!phoneValidated) {
111
return next(
112
new HttpException(
113
ConstantHttpCode.CONFLICT,
114
ConstantHttpReason.CONFLICT,
115
ConstantMessage.PHONE_NOT_VALID,
116
),
117
);
118
}
119
logger.info(`phone ${phone} is valid`);
120
121
const addressValidated = this.validate.validateAddress(address);
122
if (!addressValidated) {
123
return next(
124
new HttpException(
125
ConstantHttpCode.CONFLICT,
126
ConstantHttpReason.CONFLICT,
127
ConstantMessage.ADDRESS_NOT_VALID,
128
),
129
);
130
}
131
logger.info(`address ${address} is valid`);
132
133
const usernameCheck = await this.authService.findByUsername(username);
134
if (usernameCheck) {
135
return next(
136
new HttpException(
137
ConstantHttpCode.CONFLICT,
138
ConstantHttpReason.CONFLICT,
139
ConstantMessage.USERNAME_EXIST,
140
),
141
);
142
}
143
144
const emailCheck = await this.authService.findByEmail(email);
145
if (emailCheck) {
146
return next(
147
new HttpException(
148
ConstantHttpCode.CONFLICT,
149
ConstantHttpReason.CONFLICT,
150
ConstantMessage.EMAIL_EXIST,
151
),
152
);
153
}
154
155
const phoneCheck = await this.authService.findByPhone(phone);
156
if (phoneCheck) {
157
return next(
158
new HttpException(
159
ConstantHttpCode.CONFLICT,
160
ConstantHttpReason.CONFLICT,
161
ConstantMessage.PHONE_EXIST,
162
),
163
);
164
}
165
166
const newUserData = {
167
username,
168
name,
169
email,
170
password,
171
phone,
172
address,
173
};
174
175
const user = await this.authService.createUser(newUserData);
176
if (!user) {
177
return next(
178
new HttpException(
179
ConstantHttpCode.CONFLICT,
180
ConstantHttpReason.CONFLICT,
181
ConstantMessage.USER_NOT_CREATE,
182
),
183
);
184
}
185
186
const newUser = {...user}._doc;
187
188
logger.info({newUserpassword: newUser.password});
189
190
delete newUser.password;
191
192
logger.info({newUserpassword: newUser.password});
193
194
return res.status(ConstantHttpCode.CREATED).json({
195
status: {
196
code: ConstantHttpCode.CREATED,
197
msg: ConstantHttpReason.CREATED,
198
},
199
msg: ConstantMessage.USER_CREATE_SUCCESS,
200
data: newUser,
201
});
202
} catch (err: any) {
203
return next(
204
new HttpException(
205
ConstantHttpCode.INTERNAL_SERVER_ERROR,
206
ConstantHttpReason.INTERNAL_SERVER_ERROR,
207
err.message,
208
),
209
);
210
}
211
};
212
213
private login = async (
214
req: Request,
215
res: Response,
216
next: NextFunction,
217
): Promise<Response | void> => {
218
try {
219
const {email, password} = req.body;
220
221
const emailValidated = this.validate.validateEmail(email);
222
if (!emailValidated) {
223
return next(
224
new HttpException(
225
ConstantHttpCode.INTERNAL_SERVER_ERROR,
226
ConstantHttpReason.INTERNAL_SERVER_ERROR,
227
ConstantMessage.EMAIL_NOT_VALID,
228
),
229
);
230
}
231
logger.info(`email ${email} is valid`);
232
233
const passwordValidated = this.validate.validatePassword(password);
234
if (!passwordValidated) {
235
return next(
236
new HttpException(
237
ConstantHttpCode.INTERNAL_SERVER_ERROR,
238
ConstantHttpReason.INTERNAL_SERVER_ERROR,
239
ConstantMessage.PASSWORD_NOT_VALID,
240
),
241
);
242
}
243
logger.info(`password ${password} is valid`);
244
245
const user = await this.authService.findByEmailWithPassword(email);
246
if (!user) {
247
return next(
248
new HttpException(
249
ConstantHttpCode.INTERNAL_SERVER_ERROR,
250
ConstantHttpReason.INTERNAL_SERVER_ERROR,
251
ConstantMessage.USER_NOT_FOUND,
252
),
253
);
254
}
255
256
const isMatch = this.authService.comparePassword(password, user.password);
257
if (!isMatch) {
258
return next(
259
new HttpException(
260
ConstantHttpCode.INTERNAL_SERVER_ERROR,
261
ConstantHttpReason.INTERNAL_SERVER_ERROR,
262
ConstantMessage.PASSWORD_NOT_MATCH,
263
),
264
);
265
}
266
267
const accessToken = await this.authService.generateAccessToken(
268
user.id,
269
user.isAdmin,
270
);
271
logger.info(`accessToken: ${accessToken}`);
272
273
const newUser = {...user}._doc;
274
275
logger.info({newUserpassword: newUser.password});
276
277
delete newUser.password;
278
279
logger.info({newUserpassword: newUser.password});
280
281
return res.status(ConstantHttpCode.OK).json({
282
status: {
283
code: ConstantHttpCode.OK,
284
msg: ConstantHttpReason.OK,
285
},
286
msg: ConstantMessage.USER_LOGIN_SUCCESS,
287
data: {
288
user: newUser,
289
accessToken,
290
},
291
});
292
} catch (err: any) {
293
return next(
294
new HttpException(
295
ConstantHttpCode.INTERNAL_SERVER_ERROR,
296
ConstantHttpReason.INTERNAL_SERVER_ERROR,
297
err.message,
298
),
299
);
300
}
301
};
302
}
303
304
export default AuthController;

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

token.validation.ts

1
import jwt from 'jsonwebtoken';
2
import {Request, Response, NextFunction} from 'express';
3
import HttpException from '@/utils/exceptions/http.exception';
4
5
// variable
6
import Variable from '@/env/variable.env';
7
8
// message constant
9
import ConstantMessage from '@/constants/message.constant';
10
11
// http constant
12
import ConstantHttpCode from '@/constants/http.code.constant';
13
import ConstantHttpReason from '@/constants/http.reason.constant';
14
15
// logger
16
import logger from '@/utils/logger.util';
17
18
export const verifyToken = async (
19
req: Request,
20
res: Response,
21
next: NextFunction,
22
) => {
23
const bearer = req.headers.authorization;
24
logger.info(`bearer: ${bearer}`);
25
26
if (!bearer) {
27
return next(
28
new HttpException(
29
ConstantHttpCode.UNAUTHORIZED,
30
ConstantHttpReason.UNAUTHORIZED,
31
ConstantMessage.TOKEN_NOT_VALID,
32
),
33
);
34
}
35
36
if (!bearer || !bearer.startsWith('Bearer ')) {
37
return next(
38
new HttpException(
39
ConstantHttpCode.UNAUTHORIZED,
40
ConstantHttpReason.UNAUTHORIZED,
41
ConstantMessage.UNAUTHORIZED,
42
),
43
);
44
}
45
46
const accessToken = bearer.split('Bearer ')[1].trim();
47
48
return jwt.verify(accessToken, Variable.JWT_SECRET, (err, user: any) => {
49
if (err) {
50
res.status(ConstantHttpCode.FORBIDDEN).json({
51
status: {
52
code: ConstantHttpCode.FORBIDDEN,
53
msg: ConstantHttpReason.FORBIDDEN,
54
},
55
msg: ConstantMessage.TOKEN_NOT_VALID,
56
});
57
}
58
req.user = user;
59
return next();
60
});
61
};
62
63
export default {verifyToken};

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

index.d.ts

1
import User from '@/interfaces/user.interface';
2
3
declare global {
4
namespace Express {
5
export interface Request {
6
user: User;
7
}
8
}
9
}

tsconfig.json

1
{
2
...,
3
"typeRoots": [
4
"./src/types",
5
"./node_modules/@types"
6
],
7
...
8
}

message.constant.ts

1
class Message {
2
...
3
4
// token
5
public static readonly TOKEN_NOT_VALID: string = 'Token not valid'
6
public static readonly NOT_AUTHENTICATED: string = 'Not authenticated'
7
public static readonly UNAUTHORIZED: string = 'Unauthorized'
8
public static readonly NOT_ALLOWED: string = 'Not allowed'
9
}
10
export default Message

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

authenticated.middleware.ts

1
import {Request, Response, NextFunction} from 'express';
2
import HttpException from '@/utils/exceptions/http.exception';
3
4
import {verifyToken} from '@/validations/token.validation';
5
6
// message constant
7
import ConstantMessage from '@/constants/message.constant';
8
9
// http constant
10
import ConstantHttpCode from '@/constants/http.code.constant';
11
import ConstantHttpReason from '@/constants/http.reason.constant';
12
13
class AuthenticatedMiddleware {
14
public async verifyTokenAndAuthorization(
15
req: Request,
16
res: Response,
17
next: NextFunction,
18
) {
19
verifyToken(req, res, () => {
20
if (req?.user?.id === req?.params?.id || req?.user?.isAdmin) {
21
return next();
22
}
23
24
return next(
25
new HttpException(
26
ConstantHttpCode.FORBIDDEN,
27
ConstantHttpReason.FORBIDDEN,
28
ConstantMessage.NOT_ALLOWED,
29
),
30
);
31
});
32
}
33
34
public async verifyTokenAndAdmin(
35
req: Request,
36
res: Response,
37
next: NextFunction,
38
) {
39
verifyToken(req, res, () => {
40
if (req?.user?.isAdmin) {
41
return next();
42
}
43
44
return next(
45
new HttpException(
46
ConstantHttpCode.FORBIDDEN,
47
ConstantHttpReason.FORBIDDEN,
48
ConstantMessage.NOT_ALLOWED,
49
),
50
);
51
});
52
}
53
}
54
55
export default AuthenticatedMiddleware;

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

user.service.ts

1
import UserRepository from '@/repositories/user.repository';
2
import UserSecurity from '@/security/user.security';
3
4
class UserService {
5
private userRepository: UserRepository;
6
private userSecurity: UserSecurity;
7
8
constructor() {
9
this.userRepository = new UserRepository();
10
this.userSecurity = new UserSecurity();
11
}
12
13
public comparePassword(password: string, encryptedPassword: string): boolean {
14
return this.userSecurity.comparePassword(password, encryptedPassword);
15
}
16
17
public async findAll(): Promise<any> {
18
const users = await this.userRepository.findAll();
19
return users;
20
}
21
22
public async findById(id: string): Promise<any> {
23
const user = await this.userRepository.findById(id);
24
return user;
25
}
26
27
public async findByUsername(username: string): Promise<any> {
28
const user = await this.userRepository.findByUsername(username);
29
return user;
30
}
31
32
public async findByEmail(email: string): Promise<any> {
33
const user = await this.userRepository.findByEmail(email);
34
return user;
35
}
36
37
public async findByPhone(phone: string): Promise<any> {
38
const user = await this.userRepository.findByPhone(phone);
39
return user;
40
}
41
42
public async findByIdWithPassword(id: string): Promise<any> {
43
const user = await this.userRepository.findByIdWithPassword(id);
44
return user;
45
}
46
47
public async updateUsername(id: string, username: string): Promise<any> {
48
const user = await this.userRepository.updateUsername(id, username);
49
return user;
50
}
51
52
public async updateName(id: string, name: string): Promise<any> {
53
const user = await this.userRepository.updateName(id, name);
54
return user;
55
}
56
57
public async updateEmail(id: string, email: string): Promise<any> {
58
const user = await this.userRepository.updateEmail(id, email);
59
return user;
60
}
61
62
public async updatePassword(id: string, password: string): Promise<any> {
63
const encryptedPassword = this.userSecurity.encrypt(password);
64
const user = await this.userRepository.updatePassword(
65
id,
66
encryptedPassword,
67
);
68
return user;
69
}
70
71
public async updatePhone(id: string, phone: string): Promise<any> {
72
const user = await this.userRepository.updatePhone(id, phone);
73
return user;
74
}
75
76
public async updateAddress(id: string, address: string): Promise<any> {
77
const user = await this.userRepository.updateAddress(id, address);
78
return user;
79
}
80
81
public async deleteUser(id: string): Promise<any> {
82
const user = await this.userRepository.deleteUser(id);
83
return user;
84
}
85
86
public async getUsersStats(): Promise<any> {
87
const date = new Date();
88
const lastYear = new Date(date.setFullYear(date.getFullYear() - 1));
89
const usersStats = await this.userRepository.getUsersStats(lastYear);
90
return usersStats;
91
}
92
}
93
94
export default UserService;

api.constant.ts

1
class Api {
2
...
3
4
// users
5
public static readonly USER_UPDATE_USERNAME: string = '/update-username/:id'
6
public static readonly USER_UPDATE_NAME: string = '/update-name/:id'
7
public static readonly USER_UPDATE_EMAIL: string = '/update-email/:id'
8
public static readonly USER_UPDATE_PASSWORD: string = '/update-password/:id'
9
public static readonly USER_UPDATE_PHONE: string = '/update-phone/:id'
10
public static readonly USER_UPDATE_ADDRESS: string = '/update-address/:id'
11
public static readonly USER_DELETE: string = '/delete/:id'
12
public static readonly USER_GET: string = '/find/:id'
13
public static readonly USER_GET_ALL: string = '/'
14
public static readonly USER_GET_ALL_STATS: string = '/stats'
15
}
16
export default Api

message.constant.ts

1
class Message {
2
...
3
4
// user
5
public static readonly USERNAME_NOT_CHANGE: string = 'username is not change'
6
public static readonly USERNAME_CHANGE_SUCCESS: string =
7
'username is change success'
8
public static readonly NAME_NOT_CHANGE: string = 'name is not change'
9
public static readonly NAME_CHANGE_SUCCESS: string = 'name is change success'
10
public static readonly EMAIL_NOT_CHANGE: string = 'email is not change'
11
public static readonly EMAIL_CHANGE_SUCCESS: string =
12
'email is change success'
13
public static readonly PASSWORD_NOT_CHANGE: string = 'password is not change'
14
public static readonly PASSWORD_CHANGE_SUCCESS: string =
15
'password is change success'
16
public static readonly PHONE_NOT_CHANGE: string = 'phone is not change'
17
public static readonly PHONE_CHANGE_SUCCESS: string =
18
'phone is change success'
19
public static readonly ADDRESS_NOT_CHANGE: string = 'address is not change'
20
public static readonly ADDRESS_CHANGE_SUCCESS: string =
21
'address is change success'
22
public static readonly USER_NOT_DELETE: string =
23
'user is not delete, please try again'
24
public static readonly USER_DELETE_SUCCESS: string = 'user is delete success'
25
public static readonly USER_FOUND: string = 'user is found'
26
}
27
export default Message

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

1
import {Router, Request, Response, NextFunction} from 'express';
2
3
import Controller from '@/interfaces/controller.interface';
4
5
import UserService from '@/services/user.service';
6
import Validate from '@/validations/user.validation';
7
8
import Authenticated from '@/middlewares/authenticated.middleware';
9
import validationMiddleware from '@/middlewares/validation.middleware';
10
11
import HttpException from '@/utils/exceptions/http.exception';
12
13
// api constant
14
import ConstantAPI from '@/constants/api.constant';
15
16
// message constant
17
import ConstantMessage from '@/constants/message.constant';
18
19
// http constant
20
import ConstantHttpCode from '@/constants/http.code.constant';
21
import ConstantHttpReason from '@/constants/http.reason.constant';
22
23
// logger
24
import logger from '@/utils/logger.util';
25
26
class UserController implements Controller {
27
public path: string;
28
public router: Router;
29
private userService: UserService;
30
private authenticated: Authenticated;
31
private validate: Validate;
32
33
constructor() {
34
this.path = ConstantAPI.USERS;
35
this.router = Router();
36
this.userService = new UserService();
37
this.authenticated = new Authenticated();
38
this.validate = new Validate();
39
40
this.initialiseRoutes();
41
}
42
43
private initialiseRoutes(): void {
44
this.router.post(
45
`${this.path}${ConstantAPI.USER_UPDATE_USERNAME}`,
46
this.authenticated.verifyTokenAndAuthorization,
47
validationMiddleware(this.validate.updateUsername),
48
this.updateUsername,
49
);
50
51
this.router.post(
52
`${this.path}${ConstantAPI.USER_UPDATE_NAME}`,
53
this.authenticated.verifyTokenAndAuthorization,
54
validationMiddleware(this.validate.updateName),
55
this.updateName,
56
);
57
58
this.router.post(
59
`${this.path}${ConstantAPI.USER_UPDATE_EMAIL}`,
60
this.authenticated.verifyTokenAndAuthorization,
61
validationMiddleware(this.validate.updateEmail),
62
this.updateEmail,
63
);
64
65
this.router.post(
66
`${this.path}${ConstantAPI.USER_UPDATE_PASSWORD}`,
67
this.authenticated.verifyTokenAndAuthorization,
68
validationMiddleware(this.validate.updatePassword),
69
this.updatePassword,
70
);
71
72
this.router.post(
73
`${this.path}${ConstantAPI.USER_UPDATE_PHONE}`,
74
this.authenticated.verifyTokenAndAuthorization,
75
validationMiddleware(this.validate.updatePhone),
76
this.updatePhone,
77
);
78
79
this.router.post(
80
`${this.path}${ConstantAPI.USER_UPDATE_ADDRESS}`,
81
this.authenticated.verifyTokenAndAuthorization,
82
validationMiddleware(this.validate.updateAddress),
83
this.updateAddress,
84
);
85
86
this.router.post(
87
`${this.path}${ConstantAPI.USER_DELETE}`,
88
this.authenticated.verifyTokenAndAuthorization,
89
validationMiddleware(this.validate.deleteUser),
90
this.deleteUser,
91
);
92
93
this.router.get(
94
`${this.path}${ConstantAPI.USER_GET}`,
95
this.authenticated.verifyTokenAndAuthorization,
96
this.getUser,
97
);
98
99
this.router.get(
100
`${this.path}${ConstantAPI.USER_GET_ALL}`,
101
this.authenticated.verifyTokenAndAdmin,
102
this.getAllUsers,
103
);
104
105
this.router.get(
106
`${this.path}${ConstantAPI.USER_GET_ALL_STATS}`,
107
this.authenticated.verifyTokenAndAdmin,
108
this.getUsersStats,
109
);
110
}
111
112
private updateUsername = async (
113
req: Request,
114
res: Response,
115
next: NextFunction,
116
): Promise<Response | void> => {
117
try {
118
const {username, password} = req.body;
119
const {id} = req.params;
120
121
const user = await this.userService.findByIdWithPassword(id);
122
if (!user) {
123
return next(
124
new HttpException(
125
ConstantHttpCode.NOT_FOUND,
126
ConstantHttpReason.NOT_FOUND,
127
ConstantMessage.USER_NOT_FOUND,
128
),
129
);
130
}
131
logger.info(`user ${user.username} found`);
132
133
const isUsernameValid = this.validate.validateUsername(username);
134
if (!isUsernameValid) {
135
return next(
136
new HttpException(
137
ConstantHttpCode.BAD_REQUEST,
138
ConstantHttpReason.BAD_REQUEST,
139
ConstantMessage.USERNAME_NOT_VALID,
140
),
141
);
142
}
143
logger.info(`username ${username} is valid`);
144
145
const isPasswordValid = this.validate.validatePassword(password);
146
if (!isPasswordValid) {
147
return next(
148
new HttpException(
149
ConstantHttpCode.BAD_REQUEST,
150
ConstantHttpReason.BAD_REQUEST,
151
ConstantMessage.PASSWORD_NOT_VALID,
152
),
153
);
154
}
155
logger.info(`password ${password} is valid`);
156
157
const isMatch = this.userService.comparePassword(password, user.password);
158
if (!isMatch) {
159
return next(
160
new HttpException(
161
ConstantHttpCode.UNAUTHORIZED,
162
ConstantHttpReason.UNAUTHORIZED,
163
ConstantMessage.PASSWORD_NOT_MATCH,
164
),
165
);
166
}
167
logger.info(`password ${password} match`);
168
169
const usernameCheck = await this.userService.findByUsername(username);
170
if (usernameCheck) {
171
return next(
172
new HttpException(
173
ConstantHttpCode.BAD_REQUEST,
174
ConstantHttpReason.BAD_REQUEST,
175
ConstantMessage.USERNAME_EXIST,
176
),
177
);
178
}
179
180
if (user.username === username) {
181
return next(
182
new HttpException(
183
ConstantHttpCode.BAD_REQUEST,
184
ConstantHttpReason.BAD_REQUEST,
185
ConstantMessage.USERNAME_NOT_CHANGE,
186
),
187
);
188
}
189
190
const updatedUser = await this.userService.updateUsername(id, username);
191
if (!updatedUser) {
192
return next(
193
new HttpException(
194
ConstantHttpCode.BAD_REQUEST,
195
ConstantHttpReason.BAD_REQUEST,
196
ConstantMessage.USERNAME_NOT_CHANGE,
197
),
198
);
199
}
200
logger.info(`user ${user.username} updated`);
201
202
return res.status(ConstantHttpCode.OK).json({
203
status: {
204
code: ConstantHttpCode.OK,
205
msg: ConstantHttpReason.OK,
206
},
207
msg: ConstantMessage.USERNAME_CHANGE_SUCCESS,
208
data: {
209
user: updatedUser,
210
},
211
});
212
} catch (err: any) {
213
next(
214
new HttpException(
215
ConstantHttpCode.INTERNAL_SERVER_ERROR,
216
ConstantHttpReason.INTERNAL_SERVER_ERROR,
217
err?.message,
218
),
219
);
220
}
221
};
222
223
private updateName = async (
224
req: Request,
225
res: Response,
226
next: NextFunction,
227
): Promise<Response | void> => {
228
try {
229
const {name, password} = req.body;
230
const {id} = req.params;
231
232
const user = await this.userService.findByIdWithPassword(id);
233
if (!user) {
234
return next(
235
new HttpException(
236
ConstantHttpCode.NOT_FOUND,
237
ConstantHttpReason.NOT_FOUND,
238
ConstantMessage.USER_NOT_FOUND,
239
),
240
);
241
}
242
logger.info(`user ${user.username} found`);
243
244
const isNameValid = this.validate.validateName(name);
245
if (!isNameValid) {
246
return next(
247
new HttpException(
248
ConstantHttpCode.BAD_REQUEST,
249
ConstantHttpReason.BAD_REQUEST,
250
ConstantMessage.NAME_NOT_VALID,
251
),
252
);
253
}
254
logger.info(`name ${name} is valid`);
255
256
const isPasswordValid = this.validate.validatePassword(password);
257
if (!isPasswordValid) {
258
return next(
259
new HttpException(
260
ConstantHttpCode.BAD_REQUEST,
261
ConstantHttpReason.BAD_REQUEST,
262
ConstantMessage.PASSWORD_NOT_VALID,
263
),
264
);
265
}
266
logger.info(`password ${password} is valid`);
267
268
const isMatch = this.userService.comparePassword(password, user.password);
269
if (!isMatch) {
270
return next(
271
new HttpException(
272
ConstantHttpCode.UNAUTHORIZED,
273
ConstantHttpReason.UNAUTHORIZED,
274
ConstantMessage.PASSWORD_NOT_MATCH,
275
),
276
);
277
}
278
logger.info(`password ${password} match`);
279
280
if (user.name === name) {
281
return next(
282
new HttpException(
283
ConstantHttpCode.BAD_REQUEST,
284
ConstantHttpReason.BAD_REQUEST,
285
ConstantMessage.NAME_NOT_CHANGE,
286
),
287
);
288
}
289
290
const updatedUser = await this.userService.updateName(id, name);
291
if (!updatedUser) {
292
return next(
293
new HttpException(
294
ConstantHttpCode.BAD_REQUEST,
295
ConstantHttpReason.BAD_REQUEST,
296
ConstantMessage.NAME_NOT_CHANGE,
297
),
298
);
299
}
300
logger.info(`user ${user.username} updated`);
301
302
return res.status(ConstantHttpCode.OK).json({
303
status: {
304
code: ConstantHttpCode.OK,
305
msg: ConstantHttpReason.OK,
306
},
307
msg: ConstantMessage.NAME_CHANGE_SUCCESS,
308
data: {
309
user: updatedUser,
310
},
311
});
312
} catch (err: any) {
313
next(
314
new HttpException(
315
ConstantHttpCode.INTERNAL_SERVER_ERROR,
316
ConstantHttpReason.INTERNAL_SERVER_ERROR,
317
err?.message,
318
),
319
);
320
}
321
};
322
323
private updateEmail = async (
324
req: Request,
325
res: Response,
326
next: NextFunction,
327
): Promise<Response | void> => {
328
try {
329
const {email, password} = req.body;
330
const {id} = req.params;
331
332
const user = await this.userService.findByIdWithPassword(id);
333
if (!user) {
334
return next(
335
new HttpException(
336
ConstantHttpCode.NOT_FOUND,
337
ConstantHttpReason.NOT_FOUND,
338
ConstantMessage.USER_NOT_FOUND,
339
),
340
);
341
}
342
343
const isEmailValid = this.validate.validateEmail(email);
344
if (!isEmailValid) {
345
return next(
346
new HttpException(
347
ConstantHttpCode.BAD_REQUEST,
348
ConstantHttpReason.BAD_REQUEST,
349
ConstantMessage.EMAIL_NOT_VALID,
350
),
351
);
352
}
353
354
const isPasswordValid = this.validate.validatePassword(password);
355
if (!isPasswordValid) {
356
return next(
357
new HttpException(
358
ConstantHttpCode.BAD_REQUEST,
359
ConstantHttpReason.BAD_REQUEST,
360
ConstantMessage.PASSWORD_NOT_VALID,
361
),
362
);
363
}
364
365
if (user.email === email) {
366
return next(
367
new HttpException(
368
ConstantHttpCode.BAD_REQUEST,
369
ConstantHttpReason.BAD_REQUEST,
370
ConstantMessage.EMAIL_NOT_CHANGE,
371
),
372
);
373
}
374
375
const emailCheck = await this.userService.findByEmail(email);
376
if (emailCheck) {
377
return next(
378
new HttpException(
379
ConstantHttpCode.BAD_REQUEST,
380
ConstantHttpReason.BAD_REQUEST,
381
ConstantMessage.EMAIL_EXIST,
382
),
383
);
384
}
385
386
const isMatch = this.userService.comparePassword(password, user.password);
387
if (!isMatch) {
388
return next(
389
new HttpException(
390
ConstantHttpCode.UNAUTHORIZED,
391
ConstantHttpReason.UNAUTHORIZED,
392
ConstantMessage.PASSWORD_NOT_MATCH,
393
),
394
);
395
}
396
397
const updatedUser = await this.userService.updateEmail(id, email);
398
if (!updatedUser) {
399
return next(
400
new HttpException(
401
ConstantHttpCode.BAD_REQUEST,
402
ConstantHttpReason.BAD_REQUEST,
403
ConstantMessage.EMAIL_NOT_CHANGE,
404
),
405
);
406
}
407
408
return res.status(ConstantHttpCode.OK).json({
409
status: {
410
code: ConstantHttpCode.OK,
411
msg: ConstantHttpReason.OK,
412
},
413
msg: ConstantMessage.EMAIL_CHANGE_SUCCESS,
414
data: {
415
user: updatedUser,
416
},
417
});
418
} catch (err: any) {
419
next(
420
new HttpException(
421
ConstantHttpCode.INTERNAL_SERVER_ERROR,
422
ConstantHttpReason.INTERNAL_SERVER_ERROR,
423
err?.message,
424
),
425
);
426
}
427
};
428
429
private updatePassword = async (
430
req: Request,
431
res: Response,
432
next: NextFunction,
433
): Promise<Response | void> => {
434
try {
435
const {oldPassword, newPassword, confirmPassword} = req.body;
436
const {id} = req.params;
437
438
if (newPassword !== confirmPassword) {
439
return next(
440
new HttpException(
441
ConstantHttpCode.BAD_REQUEST,
442
ConstantHttpReason.BAD_REQUEST,
443
ConstantMessage.PASSWORD_NOT_MATCH,
444
),
445
);
446
}
447
448
const user = await this.userService.findByIdWithPassword(id);
449
if (!user) {
450
return next(
451
new HttpException(
452
ConstantHttpCode.NOT_FOUND,
453
ConstantHttpReason.NOT_FOUND,
454
ConstantMessage.USER_NOT_FOUND,
455
),
456
);
457
}
458
459
const isOldPasswordValid = this.validate.validatePassword(oldPassword);
460
if (!isOldPasswordValid) {
461
return next(
462
new HttpException(
463
ConstantHttpCode.BAD_REQUEST,
464
ConstantHttpReason.BAD_REQUEST,
465
ConstantMessage.PASSWORD_NOT_VALID,
466
),
467
);
468
}
469
470
const isNewPasswordValid = this.validate.validatePassword(newPassword);
471
if (!isNewPasswordValid) {
472
return next(
473
new HttpException(
474
ConstantHttpCode.BAD_REQUEST,
475
ConstantHttpReason.BAD_REQUEST,
476
ConstantMessage.PASSWORD_NOT_VALID,
477
),
478
);
479
}
480
481
const isConfirmPasswordValid =
482
this.validate.validatePassword(confirmPassword);
483
if (!isConfirmPasswordValid) {
484
return next(
485
new HttpException(
486
ConstantHttpCode.BAD_REQUEST,
487
ConstantHttpReason.BAD_REQUEST,
488
ConstantMessage.PASSWORD_NOT_VALID,
489
),
490
);
491
}
492
493
const isMatch = this.userService.comparePassword(
494
oldPassword,
495
user.password,
496
);
497
if (!isMatch) {
498
return next(
499
new HttpException(
500
ConstantHttpCode.UNAUTHORIZED,
501
ConstantHttpReason.UNAUTHORIZED,
502
ConstantMessage.PASSWORD_NOT_MATCH,
503
),
504
);
505
}
506
507
if (oldPassword === newPassword) {
508
return next(
509
new HttpException(
510
ConstantHttpCode.BAD_REQUEST,
511
ConstantHttpReason.BAD_REQUEST,
512
ConstantMessage.PASSWORD_NOT_CHANGE,
513
),
514
);
515
}
516
517
const updatedUser = await this.userService.updatePassword(
518
id,
519
newPassword,
520
);
521
if (!updatedUser) {
522
return next(
523
new HttpException(
524
ConstantHttpCode.BAD_REQUEST,
525
ConstantHttpReason.BAD_REQUEST,
526
ConstantMessage.PASSWORD_NOT_CHANGE,
527
),
528
);
529
}
530
531
return res.status(ConstantHttpCode.OK).json({
532
status: {
533
code: ConstantHttpCode.OK,
534
msg: ConstantHttpReason.OK,
535
},
536
msg: ConstantMessage.PASSWORD_CHANGE_SUCCESS,
537
data: {
538
user: updatedUser,
539
},
540
});
541
} catch (err: any) {
542
next(
543
new HttpException(
544
ConstantHttpCode.INTERNAL_SERVER_ERROR,
545
ConstantHttpReason.INTERNAL_SERVER_ERROR,
546
err?.message,
547
),
548
);
549
}
550
};
551
552
private updatePhone = async (
553
req: Request,
554
res: Response,
555
next: NextFunction,
556
): Promise<Response | void> => {
557
try {
558
const {phone, password} = req.body;
559
const {id} = req.params;
560
561
const user = await this.userService.findByIdWithPassword(id);
562
if (!user) {
563
return next(
564
new HttpException(
565
ConstantHttpCode.NOT_FOUND,
566
ConstantHttpReason.NOT_FOUND,
567
ConstantMessage.USER_NOT_FOUND,
568
),
569
);
570
}
571
572
const isPhoneValid = this.validate.validatePhone(phone);
573
logger.info({isPhoneValid});
574
if (!isPhoneValid) {
575
return next(
576
new HttpException(
577
ConstantHttpCode.BAD_REQUEST,
578
ConstantHttpReason.BAD_REQUEST,
579
ConstantMessage.PHONE_NOT_VALID,
580
),
581
);
582
}
583
584
const isPasswordValid = this.validate.validatePassword(password);
585
if (!isPasswordValid) {
586
return next(
587
new HttpException(
588
ConstantHttpCode.BAD_REQUEST,
589
ConstantHttpReason.BAD_REQUEST,
590
ConstantMessage.PASSWORD_NOT_VALID,
591
),
592
);
593
}
594
595
const phoneCheck = await this.userService.findByPhone(phone);
596
if (phoneCheck) {
597
return next(
598
new HttpException(
599
ConstantHttpCode.NOT_FOUND,
600
ConstantHttpReason.NOT_FOUND,
601
ConstantMessage.PHONE_EXIST,
602
),
603
);
604
}
605
606
const isMatch = this.userService.comparePassword(password, user.password);
607
if (!isMatch) {
608
return next(
609
new HttpException(
610
ConstantHttpCode.UNAUTHORIZED,
611
ConstantHttpReason.UNAUTHORIZED,
612
ConstantMessage.PASSWORD_NOT_MATCH,
613
),
614
);
615
}
616
617
if (user.phone === phone) {
618
return next(
619
new HttpException(
620
ConstantHttpCode.BAD_REQUEST,
621
ConstantHttpReason.BAD_REQUEST,
622
ConstantMessage.PHONE_NOT_CHANGE,
623
),
624
);
625
}
626
627
const updatedUser = await this.userService.updatePhone(id, phone);
628
if (!updatedUser) {
629
return next(
630
new HttpException(
631
ConstantHttpCode.BAD_REQUEST,
632
ConstantHttpReason.BAD_REQUEST,
633
ConstantMessage.PHONE_NOT_CHANGE,
634
),
635
);
636
}
637
638
return res.status(ConstantHttpCode.OK).json({
639
status: {
640
code: ConstantHttpCode.OK,
641
msg: ConstantHttpReason.OK,
642
},
643
msg: ConstantMessage.PHONE_CHANGE_SUCCESS,
644
data: {
645
user: updatedUser,
646
},
647
});
648
} catch (err: any) {
649
next(
650
new HttpException(
651
ConstantHttpCode.INTERNAL_SERVER_ERROR,
652
ConstantHttpReason.INTERNAL_SERVER_ERROR,
653
err?.message,
654
),
655
);
656
}
657
};
658
659
private updateAddress = async (
660
req: Request,
661
res: Response,
662
next: NextFunction,
663
): Promise<Response | void> => {
664
try {
665
const {address, password} = req.body;
666
const {id} = req.params;
667
668
const user = await this.userService.findByIdWithPassword(id);
669
if (!user) {
670
return next(
671
new HttpException(
672
ConstantHttpCode.NOT_FOUND,
673
ConstantHttpReason.NOT_FOUND,
674
ConstantMessage.USER_NOT_FOUND,
675
),
676
);
677
}
678
679
const isAddressValid = this.validate.validateAddress(address);
680
if (!isAddressValid) {
681
return next(
682
new HttpException(
683
ConstantHttpCode.BAD_REQUEST,
684
ConstantHttpReason.BAD_REQUEST,
685
ConstantMessage.ADDRESS_NOT_VALID,
686
),
687
);
688
}
689
690
const isPasswordValid = this.validate.validatePassword(password);
691
if (!isPasswordValid) {
692
return next(
693
new HttpException(
694
ConstantHttpCode.BAD_REQUEST,
695
ConstantHttpReason.BAD_REQUEST,
696
ConstantMessage.PASSWORD_NOT_VALID,
697
),
698
);
699
}
700
701
const isMatch = this.userService.comparePassword(password, user.password);
702
if (!isMatch) {
703
return next(
704
new HttpException(
705
ConstantHttpCode.UNAUTHORIZED,
706
ConstantHttpReason.UNAUTHORIZED,
707
ConstantMessage.PASSWORD_NOT_MATCH,
708
),
709
);
710
}
711
712
if (user.address === address) {
713
return next(
714
new HttpException(
715
ConstantHttpCode.BAD_REQUEST,
716
ConstantHttpReason.BAD_REQUEST,
717
ConstantMessage.ADDRESS_NOT_CHANGE,
718
),
719
);
720
}
721
722
const updatedUser = await this.userService.updateAddress(id, address);
723
if (!updatedUser) {
724
return next(
725
new HttpException(
726
ConstantHttpCode.BAD_REQUEST,
727
ConstantHttpReason.BAD_REQUEST,
728
ConstantMessage.ADDRESS_NOT_CHANGE,
729
),
730
);
731
}
732
733
return res.status(ConstantHttpCode.OK).json({
734
status: {
735
code: ConstantHttpCode.OK,
736
msg: ConstantHttpReason.OK,
737
},
738
msg: ConstantMessage.ADDRESS_CHANGE_SUCCESS,
739
data: {
740
user: updatedUser,
741
},
742
});
743
} catch (err: any) {
744
next(
745
new HttpException(
746
ConstantHttpCode.INTERNAL_SERVER_ERROR,
747
ConstantHttpReason.INTERNAL_SERVER_ERROR,
748
err?.message,
749
),
750
);
751
}
752
};
753
754
private deleteUser = async (
755
req: Request,
756
res: Response,
757
next: NextFunction,
758
): Promise<Response | void> => {
759
try {
760
const {password} = req.body;
761
const {id} = req.params;
762
763
const user = await this.userService.findByIdWithPassword(id);
764
if (!user) {
765
return next(
766
new HttpException(
767
ConstantHttpCode.NOT_FOUND,
768
ConstantHttpReason.NOT_FOUND,
769
ConstantMessage.USER_NOT_FOUND,
770
),
771
);
772
}
773
774
const isPasswordValid = this.validate.validatePassword(password);
775
if (!isPasswordValid) {
776
return next(
777
new HttpException(
778
ConstantHttpCode.BAD_REQUEST,
779
ConstantHttpReason.BAD_REQUEST,
780
ConstantMessage.PASSWORD_NOT_VALID,
781
),
782
);
783
}
784
785
const isMatch = this.userService.comparePassword(password, user.password);
786
if (!isMatch) {
787
return next(
788
new HttpException(
789
ConstantHttpCode.UNAUTHORIZED,
790
ConstantHttpReason.UNAUTHORIZED,
791
ConstantMessage.PASSWORD_NOT_MATCH,
792
),
793
);
794
}
795
796
const deletedUser = await this.userService.deleteUser(id);
797
if (!deletedUser) {
798
return next(
799
new HttpException(
800
ConstantHttpCode.BAD_REQUEST,
801
ConstantHttpReason.BAD_REQUEST,
802
ConstantMessage.USER_NOT_DELETE,
803
),
804
);
805
}
806
807
return res.status(ConstantHttpCode.OK).json({
808
status: {
809
code: ConstantHttpCode.OK,
810
msg: ConstantHttpReason.OK,
811
},
812
msg: ConstantMessage.USER_DELETE_SUCCESS,
813
});
814
} catch (err: any) {
815
next(
816
new HttpException(
817
ConstantHttpCode.INTERNAL_SERVER_ERROR,
818
ConstantHttpReason.INTERNAL_SERVER_ERROR,
819
err?.message,
820
),
821
);
822
}
823
};
824
825
private getUser = async (
826
req: Request,
827
res: Response,
828
next: NextFunction,
829
): Promise<Response | void> => {
830
try {
831
const {id} = req.params;
832
833
const user = await this.userService.findById(id);
834
if (!user) {
835
return next(
836
new HttpException(
837
ConstantHttpCode.NOT_FOUND,
838
ConstantHttpReason.NOT_FOUND,
839
ConstantMessage.USER_NOT_FOUND,
840
),
841
);
842
}
843
844
return res.status(ConstantHttpCode.OK).json({
845
status: {
846
code: ConstantHttpCode.OK,
847
msg: ConstantHttpReason.OK,
848
},
849
msg: ConstantMessage.USER_FOUND,
850
data: {
851
user,
852
},
853
});
854
} catch (err: any) {
855
next(
856
new HttpException(
857
ConstantHttpCode.INTERNAL_SERVER_ERROR,
858
ConstantHttpReason.INTERNAL_SERVER_ERROR,
859
err?.message,
860
),
861
);
862
}
863
};
864
865
private getAllUsers = async (
866
_req: Request,
867
res: Response,
868
next: NextFunction,
869
): Promise<Response | void> => {
870
try {
871
const users = await this.userService.findAll();
872
if (!users) {
873
return next(
874
new HttpException(
875
ConstantHttpCode.NOT_FOUND,
876
ConstantHttpReason.NOT_FOUND,
877
ConstantMessage.USER_NOT_FOUND,
878
),
879
);
880
}
881
882
return res.status(ConstantHttpCode.OK).json({
883
status: {
884
code: ConstantHttpCode.OK,
885
msg: ConstantHttpReason.OK,
886
},
887
msg: ConstantMessage.USER_FOUND,
888
data: {
889
users,
890
},
891
});
892
} catch (err: any) {
893
next(
894
new HttpException(
895
ConstantHttpCode.INTERNAL_SERVER_ERROR,
896
ConstantHttpReason.INTERNAL_SERVER_ERROR,
897
err?.message,
898
),
899
);
900
}
901
};
902
903
private getUsersStats = async (
904
_req: Request,
905
res: Response,
906
next: NextFunction,
907
): Promise<Response | void> => {
908
try {
909
const usersStats = await this.userService.getUsersStats();
910
if (!usersStats) {
911
return next(
912
new HttpException(
913
ConstantHttpCode.NOT_FOUND,
914
ConstantHttpReason.NOT_FOUND,
915
ConstantMessage.USER_NOT_FOUND,
916
),
917
);
918
}
919
920
return res.status(ConstantHttpCode.OK).json({
921
status: {
922
code: ConstantHttpCode.OK,
923
msg: ConstantHttpReason.OK,
924
},
925
msg: ConstantMessage.USER_FOUND,
926
data: {
927
users: usersStats,
928
},
929
});
930
} catch (err: any) {
931
next(
932
new HttpException(
933
ConstantHttpCode.INTERNAL_SERVER_ERROR,
934
ConstantHttpReason.INTERNAL_SERVER_ERROR,
935
err?.message,
936
),
937
);
938
}
939
};
940
}
941
942
export default UserController;

api.constant.ts

1
class Api {
2
...
3
4
public static readonly AUTH: string = `/auth`
5
public static readonly USERS: string = '/users'
6
}
7
export default Api

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

www.ts

1
#!/usr/bin/env ts-node
2
3
import 'module-alias/register';
4
import 'core-js/stable';
5
import 'regenerator-runtime/runtime';
6
7
import http from 'http';
8
import App from '..';
9
10
import Variable from '@/env/variable.env';
11
import logger from '@/utils/logger.util';
12
13
// controllers
14
import AuthController from '@/controllers/auth.controller';
15
import UserController from '@/controllers/user.controller';
16
17
const {app} = new App([new AuthController(), new UserController()]);
18
19
/**
20
* Normalize a port into a number, string, or false.
21
*/
22
const normalizePort = (val: any) => {
23
const port = parseInt(val, 10);
24
25
if (Number.isNaN(port)) {
26
// named pipe
27
return val;
28
}
29
30
if (port >= 0) {
31
// port number
32
return port;
33
}
34
35
return false;
36
};
37
38
const port = normalizePort(Variable.PORT || '3030');
39
app.set('port', port);
40
41
/**
42
* Create HTTP server.
43
*/
44
const server = http.createServer(app);
45
46
/**
47
* Event listener for HTTP server "error" event.
48
*/
49
const onError = (error: any) => {
50
if (error.syscall !== 'listen') {
51
throw error;
52
}
53
54
const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`;
55
56
// handle specific listen errors with friendly messages
57
switch (error.code) {
58
case 'EACCES':
59
logger.error(`${bind} requires elevated privileges`);
60
process.exit(1);
61
break;
62
case 'EADDRINUSE':
63
logger.error(`${bind} is already in use`);
64
process.exit(1);
65
break;
66
default:
67
throw error;
68
}
69
};
70
71
/**
72
* Event listener for HTTP server "listening" event.
73
*/
74
const onListening = () => {
75
const addr = server.address();
76
const bind = typeof addr === 'string' ? `pipe ${addr}` : `port ${addr?.port}`;
77
logger.info(`Listening on ${bind}`);
78
};
79
80
server.listen(port);
81
server.on('error', onError);
82
server.on('listening', onListening);

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.

Related Posts

Check out some of our other posts

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

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

Introduction All code from this tutorial as a complete package is available in this repository. If you find this tutorial helpful, please share

read more
Introduction to Spring Boot Framework

Introduction to Spring Boot Framework

Introduction For creating web apps and microservices, many developers utilize the Spring Boot framework. The fact that it is built on top of the Spring Framework and offers a number of advantages

read more
RESTful API vs. GraphQL: Which API is the Right Choice for Your Project?

RESTful API vs. GraphQL: Which API is the Right Choice for Your Project?

TL;DR When deciding between RESTful and GraphQL APIs for a data analysis and display application, it is important to consider the advantages and disadvantages of each. RESTful APIs have been arou

read more
Decoding REST API Architecture: A Comprehensive Guide for Developers

Decoding REST API Architecture: A Comprehensive Guide for Developers

Introduction Hey there, fellow developers! Buckle up because we're about to dive into the crazy world of REST API architecture. Prepare to decode the mysterious differences between REST API and R

read more
Mastering Caching Strategies with Redis Cache: Boosting Performance in Node.js and TypeScript

Mastering Caching Strategies with Redis Cache: Boosting Performance in Node.js and TypeScript

Introduction In the ever-evolving realm of software development, the pursuit of optimizing application performance is a perpetual endeavor. Among the arsenal of strategies to attain this goal, th

read more