Skip to content

Commit

Permalink
rate limiting class, refactor and improve conditions
Browse files Browse the repository at this point in the history
  • Loading branch information
SAINIAbhishek committed Nov 12, 2024
1 parent 6691529 commit f98db0e
Show file tree
Hide file tree
Showing 19 changed files with 207 additions and 122 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ jobs:

# Lint the server code
- name: Lint Server Code
run: npm run eslint
run: npm run lint
working-directory: ./server

# Step: Formatting Frontend job
Expand Down
24 changes: 14 additions & 10 deletions server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,30 @@ DATE_FULL_FORMAT='yyyy-MM-dd HH:mm:ss.SSS'
# Database
# YOUR_MONGO_URI
MONGO_URI=mongodb://localhost:27017/

# YOUR_MONGO_DB_NAME
MONGO_DB_DATABASE_NAME=tasks-db-v1

#YOUR_MONGO_DB_USER_NAME
MONGO_DB_USERNAME=taskApplicationUser

#YOUR_MONGO_DB_USER_PWD
MONGO_DB_PWD=123456789

# Maintain up to x socket connections
DB_MIN_POOL_SIZE=2
DB_MAX_POOL_SIZE=5
MONGO_DB_MIN_POOL_SIZE=2
MONGO_DB_MAX_POOL_SIZE=5
# Give up initial connection after 10 seconds
DB_CONNECT_TIMEOUT_MS=60000
MONGO_DB_CONNECT_TIMEOUT_MS=60000
# Close sockets after 45 seconds of inactivity
DB_SOCKET_TIMEOUT_MS=45000
MONGO_DB_SOCKET_TIMEOUT_MS=45000

#localhost or IP of the server
# If using the docker installation then use 'mongo' for host name else localhost or ip or db server
#YOUR_MONGO_DB_HOST_NAME
MONGO_DB_HOST=127.0.0.1
MONGO_DB_PORT=27017

#YOUR_MONGO_DB_USER_NAME
MONGO_DB_USERNAME=taskApplicationUser

#YOUR_MONGO_DB_USER_PWD
MONGO_DB_PWD=123456789

# Limiter
# The time window for which login attempts are counted
# 2 minutes = 120000
Expand Down Expand Up @@ -74,6 +75,9 @@ PASSWORD_RESET_TOKEN_VALIDITY_SEC=3600000
TOKEN_ISSUER=api.dev.saini.com
TOKEN_AUDIENCE=dev.saini.com

# make the value production for prod env
MAILTRAP_EMAIL_ENV=testing

# Mailtrap(Email service) Info
MAILTRAP_USERNAME=
MAILTRAP_PASSWORD=
Expand Down
1 change: 1 addition & 0 deletions server/dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ RUN rm -rf \
.eslintignore \
.prettierignore \
tsconfig.json \
tsconfig.prod.json \
.prettierrc \
.eslintrc.json

Expand Down
7 changes: 5 additions & 2 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
"watch": "npx concurrently -k -p \"[{name}]\" -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold,green.bold,red.bold\" \"npm run watch-ts\" \"npm run watch-node\"",
"watch-node": "nodemon -r dotenv/config build/server.js",
"clean": "npx rimraf ./build",
"build-ts": "npx tsc",
"build-ts": "npx tsc --project tsconfig.prod.json",
"watch-ts": "npx tsc -w",
"eslint": "npx eslint . --ext .ts",
"lint": "npx eslint . --ext ts --report-unused-disable-directives --max-warnings 0",
"lint:fix": "npm run lint --fix",
"prettier:write": "npx prettier . --write",
"prettier": "npx prettier . --check",
"test": "npx jest --forceExit --detectOpenHandles --coverage --verbose",
"install:packages": "npm i",
"upgrade:packages": "npm update --save-dev && npm update --save",
Expand Down
12 changes: 11 additions & 1 deletion server/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import express, { NextFunction, Request, Response } from 'express';
import Logger from './middleware/Logger';
import { API_VERSION, CORS_URL, ENVIRONMENT } from './config';
import { API_VERSION, CORS_URL, ENVIRONMENT, LIMITER } from './config';
import cors from 'cors';
import helmet from 'helmet';
import './config/DatabaseConfig'; // initialize database
Expand All @@ -12,13 +12,23 @@ import {
NotFoundError,
} from './middleware/ApiError';
import routes from './routes/v1';
import LimiterHelper from './helpers/LimiterHelper';

process.on('uncaughtException', (e) => {
Logger.error(e);
});

const app = express();

// Apply rate limiting to all requests
app.use(
LimiterHelper.createRateLimiter({
windowMs: LIMITER.ipWS, // 15 minutes
max: LIMITER.ipMaxAttempt, // limit each IP to 100 requests per windowMs
message: 'Too many requests, please try again later.',
})
);

// This middleware is responsible to enable cookie parsing
// commonly used to parse cookies from the incoming HTTP request headers.
app.use(cookieParser());
Expand Down
19 changes: 15 additions & 4 deletions server/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const PORT = process.env.PORT;
export const CORS_URL = process.env.CORS_URL?.split(',') || [];
export const API_VERSION = process.env.API_VERSION;
export const FRONTEND_RESET_URL = process.env.FRONTEND_RESET_URL;
export const MAILTRAP_EMAIL_ENV = process.env.MAILTRAP_EMAIL_ENV || 'testing';

export const DATE_FORMAT = process.env.DATE_FORMAT || 'yyyy-MM-dd';
export const DATE_FULL_FORMAT =
Expand All @@ -15,14 +16,22 @@ export const MAILTRAP_EMAIL = {
host: process.env.MAILTRAP_TESTING_HOST || '',
port: parseInt(process.env.MAILTRAP_TESTING_PORT || ''),
},
prod: {
username: process.env.MAILTRAP_USERNAME || '',
password: process.env.MAILTRAP_PASSWORD || '',
host: process.env.MAILTRAP_HOST || '',
port: parseInt(process.env.MAILTRAP_PORT || ''),
},
};

export const LIMITER = {
loginWS: parseInt(process.env.LIMITER_LOGIN_WS || '120000'),
ipWS: parseInt(process.env.LIMITER_IP_WS || '900000'),
forgotPasswordWS: parseInt(
process.env.LIMITER_FORGOT_PASSWORD_WS || '120000'
),
loginMaxAttempt: parseInt(process.env.LIMITER_LOGIN_ATTEMPT || '5'),
ipMaxAttempt: parseInt(process.env.LIMITER_IP_ATTEMPT || '100'),
forgotPasswordMaxAttempt: parseInt(
process.env.LIMITER_FORGOT_PASSWORD_ATTEMPT || '2'
),
Expand All @@ -40,10 +49,12 @@ export const DB = {
username: process.env.MONGO_DB_USERNAME || '',
pwd: process.env.MONGO_DB_PWD || '',
port: process.env.MONGO_DB_PORT || '',
minPoolSize: parseInt(process.env.DB_MIN_POOL_SIZE || '5'),
maxPoolSize: parseInt(process.env.DB_MAX_POOL_SIZE || '10'),
connectTimeoutMS: parseInt(process.env.DB_CONNECT_TIMEOUT_MS || '60000'),
socketTimeoutMS: parseInt(process.env.DB_SOCKET_TIMEOUT_MS || '45000'),
minPoolSize: parseInt(process.env.MONGO_DB_MIN_POOL_SIZE || '5'),
maxPoolSize: parseInt(process.env.MONGO_DB_MAX_POOL_SIZE || '10'),
connectTimeoutMS: parseInt(
process.env.MONGO_DB_CONNECT_TIMEOUT_MS || '60000'
),
socketTimeoutMS: parseInt(process.env.MONGO_DB_SOCKET_TIMEOUT_MS || '45000'),
};

export const TOKEN_INFO = {
Expand Down
46 changes: 21 additions & 25 deletions server/src/controllers/AuthController.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import asyncHandler from 'express-async-handler';
import Logger from '../middleware/Logger';
import {
ManyRequestResponse,
SuccessMsgResponse,
SuccessResponse,
TokenRefreshResponse,
Expand All @@ -16,23 +15,17 @@ import bcrypt from 'bcrypt';
import AuthHelper from '../helpers/AuthHelper';
import { COOKIE, LIMITER, TOKEN_INFO } from '../config';
import jwt, { JwtPayload } from 'jsonwebtoken';
import rateLimit from 'express-rate-limit';
import { ProtectedRequest } from 'app-request';
import { UserModel } from '../models/UserModel';
import { RoleNameEnum, RoleStatusEnum } from '../models/RoleModel';
import RoleHelper from '../helpers/RoleHelper';
import LimiterHelper from '../helpers/LimiterHelper';

class AuthController {
forgotPasswordLimiter = rateLimit({
forgotPasswordLimiter = LimiterHelper.createRateLimiter({
windowMs: LIMITER.forgotPasswordWS,
max: LIMITER.forgotPasswordMaxAttempt,
message: 'Too many reset passwords attempts, please try again later.',
handler: (req, res, _, options) => {
Logger.info(`${options.message}, Method: ${req.method}, Url: ${req.url}`);
new ManyRequestResponse(options.message).send(res);
},
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
message: 'Too many reset password attempts, please try again later.',
});

forgotPassword = asyncHandler(async (req: ProtectedRequest, res, next) => {
Expand Down Expand Up @@ -73,14 +66,25 @@ class AuthController {

resetPassword = asyncHandler(async (req: ProtectedRequest, res, next) => {
const { password, email } = req.body;
const { token } = req.params ?? '';

const filter = {
passwordResetTokenRaw: req.params.token,
passwordResetToken: AuthHelper.generateHashTokenKey(req.params.token),
let filter = {
passwordResetTokenRaw: token,
passwordResetToken: token,
email: email,
passwordResetTokenExpires: { $gt: Date.now() },
};

if (!token) {
Logger.info(`Attempted password reset, ${JSON.stringify(filter)}`);
throw new BadRequestError('Token is invalid or has been expired.');
}

filter = {
...filter,
passwordResetToken: AuthHelper.generateHashTokenKey(token),
};

const user = await UserModel.findOne(filter);

if (!user) {
Expand All @@ -104,20 +108,14 @@ class AuthController {
next();
});

loginLimiter = rateLimit({
loginLimiter = LimiterHelper.createRateLimiter({
windowMs: LIMITER.loginWS,
max: LIMITER.loginMaxAttempt,
message: 'Too many login attempts, please try again later.',
handler: (req, res, _, options) => {
Logger.info(`${options.message}, Method: ${req.method}, Url: ${req.url}`);
new ManyRequestResponse(options.message).send(res);
},
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});

isAuthorized = asyncHandler(async (req: ProtectedRequest, _, next) => {
const token = AuthHelper.getAccessToken(req.headers.authorization);
const token = AuthHelper.getAccessToken(req.headers.authorization) || '';
const accessTokenPayload = jwt.verify(
token,
TOKEN_INFO.accessTokenSecret
Expand Down Expand Up @@ -177,10 +175,8 @@ class AuthController {
},
]);

if (!user || !user.password) {
throw new BadRequestError(
'Your email address or your password is incorrect'
);
if (!user?.password) {
throw new BadRequestError('Your email address or password is incorrect');
}

const isMatched = await bcrypt.compare(req.body.password, user.password);
Expand Down
8 changes: 4 additions & 4 deletions server/src/controllers/EmailController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ class EmailController {
'Your password has been successfully updated. If you did not initiate this change, please contact your administrator for assistance.';

const email: Email = {
to: user.email,
to: user.email || '',
subject: 'Password update successfully',
content: EmailHelper.emailFormatter(message, user.firstname),
};

try {
await EmailHelper.testingEmailTransporter({
await EmailHelper.emailTransporter({
to: email.to,
subject: email.subject,
html: email.content,
Expand Down Expand Up @@ -72,14 +72,14 @@ class EmailController {
If you didn't initiate this request or have any concerns, please ignore this message. Your account remains secure.`;

const email: Email = {
to: user.email,
to: user.email || '',
subject: 'Password change request received',
content: EmailHelper.emailFormatter(message, user.firstname),
url: resetUrl,
};

try {
await EmailHelper.testingEmailTransporter({
await EmailHelper.emailTransporter({
to: email.to,
subject: email.subject,
html: email.content,
Expand Down
10 changes: 5 additions & 5 deletions server/src/controllers/HealthCheckController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import asyncHandler from 'express-async-handler';
import { SuccessResponse } from '../middleware/ApiResponse';

class HealthCheckController {

checkHealth = asyncHandler(async (req, res) => {
new SuccessResponse('The API is up and running. Health check is passed.', {}).send(res);
new SuccessResponse(
'The API is up and running. Health check is passed.',
{}
).send(res);
});

}


export default new HealthCheckController();
export default new HealthCheckController();
35 changes: 16 additions & 19 deletions server/src/controllers/UserController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ class UserController {
});

getUser = asyncHandler(async (req, res) => {
const user = await UserHelper.findById(req.params.id);
const { id } = req.params;
const user = (!!id && (await UserHelper.findById(id))) || null;
if (!user) throw new NotFoundError('User not found');

new SuccessResponse('User fetched successfully', {
Expand All @@ -59,24 +60,17 @@ class UserController {

updateUser = asyncHandler(async (req, res) => {
const { email, firstname, lastname } = req.body;
const updateFields: any = {};
const updateFields: { [key: string]: any; } = { email, firstname, lastname };

if (email) {
updateFields.email = email;
}
if (firstname) {
updateFields.firstname = firstname;
}
if (lastname) {
updateFields.lastname = lastname;
}

const updatedUser = await UserModel.findOneAndUpdate(
{ _id: req.params.id },
{ $set: updateFields },
{ new: true }
// Remove undefined fields
Object.keys(updateFields).forEach(
(key) => updateFields[key] === undefined && delete updateFields[key]
);

const { id } = req.params;

const updatedUser = await UserHelper.findByIdAndUpdate(id, updateFields);

if (!updatedUser) {
throw new NotFoundError('User not found');
}
Expand All @@ -87,13 +81,16 @@ class UserController {
});

deleteUser = asyncHandler(async (req, res) => {
const { id } = req.params;

const result: DeleteResult = await UserModel.deleteOne({
_id: req.params.id,
_id: id,
});
if (!result.deletedCount) throw new NotFoundError('User not found');

if (result.deletedCount === 0) throw new NotFoundError('User not found');

new SuccessResponse('User deleted successfully', {
userId: req.params.id,
userId: id,
}).send(res);
});
}
Expand Down
Loading

0 comments on commit f98db0e

Please sign in to comment.