From a94b15ff1403e208df7e32bfac52444be0457711 Mon Sep 17 00:00:00 2001 From: Dmytro Doronin <138795636+Dmytro-Doronin@users.noreply.github.com> Date: Mon, 9 Feb 2026 14:16:57 +0100 Subject: [PATCH] feat: rate limit middleware --- server/src/db/schemes/rateLimit.schema.ts | 20 +++++++++++ .../src/db/schemes/types/rateLimit.types.ts | 7 ++++ .../middlewares/accessCounter.middleware.ts | 35 +++++++++++++++++++ server/src/routes/authRoute.ts | 6 ++++ 4 files changed, 68 insertions(+) create mode 100644 server/src/db/schemes/rateLimit.schema.ts create mode 100644 server/src/db/schemes/types/rateLimit.types.ts create mode 100644 server/src/middlewares/accessCounter.middleware.ts diff --git a/server/src/db/schemes/rateLimit.schema.ts b/server/src/db/schemes/rateLimit.schema.ts new file mode 100644 index 0000000..81a82e1 --- /dev/null +++ b/server/src/db/schemes/rateLimit.schema.ts @@ -0,0 +1,20 @@ +import mongoose from "mongoose"; +import { RateLimitTypes } from "./types/rateLimit.types.js"; + +const RateSchema = new mongoose.Schema( + { + ip: { type: String, required: true }, + url: { type: String, required: true }, + bucket: { type: Number, required: true }, + count: { type: Number, required: true, default: 0 }, + createdAt: { type: Date, required: true, default: Date.now }, + }, + { versionKey: false }, +); + +RateSchema.index({ ip: 1, url: 1, bucket: 1 }, { unique: true }); + +RateSchema.index({ createdAt: 1 }, { expireAfterSeconds: 120 }); + +export const RateModel = + mongoose.models.Rate || mongoose.model("Rate", RateSchema); diff --git a/server/src/db/schemes/types/rateLimit.types.ts b/server/src/db/schemes/types/rateLimit.types.ts new file mode 100644 index 0000000..e2d8ca1 --- /dev/null +++ b/server/src/db/schemes/types/rateLimit.types.ts @@ -0,0 +1,7 @@ +export type RateLimitTypes = { + ip: string; + url: string; + bucket: number; + count: number; + createdAt: Date; +}; diff --git a/server/src/middlewares/accessCounter.middleware.ts b/server/src/middlewares/accessCounter.middleware.ts new file mode 100644 index 0000000..39f4989 --- /dev/null +++ b/server/src/middlewares/accessCounter.middleware.ts @@ -0,0 +1,35 @@ +import { NextFunction, Request, Response } from "express"; +import { RateModel } from "../db/schemes/rateLimit.schema.js"; + +const ms = 10_000; +const maxRequests = 5; + +export const accessCounterMiddleware = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const now = Date.now(); + const bucket = Math.floor(now / ms); + const ip = req.ip; + const url = req.originalUrl; + + const doc = await RateModel.findOneAndUpdate( + { ip, url, bucket }, + { + $inc: { count: 1 }, + $setOnInsert: { createdAt: new Date(now) }, + }, + { upsert: true, new: true, lean: true }, + ); + + if (doc.count > maxRequests) { + return res.sendStatus(429); + } + + return next(); + } catch { + return res.sendStatus(500); + } +}; diff --git a/server/src/routes/authRoute.ts b/server/src/routes/authRoute.ts index 5ed7b0a..cf05ed6 100644 --- a/server/src/routes/authRoute.ts +++ b/server/src/routes/authRoute.ts @@ -10,6 +10,7 @@ import { AuthController } from "../controllers/auth.controller.js"; import { TYPES } from "../composition/composition.types.js"; import { AuthMiddleware } from "../middlewares/authMiddlewareWithBearer.js"; import { RefreshTokenMiddleware } from "../middlewares/refreshToken.middleware.js"; +import { accessCounterMiddleware } from "../middlewares/accessCounter.middleware.js"; export const authRouter = Router(); const authController = container.get(TYPES.AuthController); @@ -20,6 +21,7 @@ const refreshTokenMiddleware = container.get( authRouter.post( "/registration-student", + accessCounterMiddleware, autStudentValidationMiddleware(), errorMiddleware, authController.registrationStudentController.bind(authController), @@ -27,6 +29,7 @@ authRouter.post( authRouter.post( "/registration-teacher", + accessCounterMiddleware, autTeacherValidationMiddleware(), errorMiddleware, authController.registrationTeacherController.bind(authController), @@ -34,6 +37,7 @@ authRouter.post( authRouter.post( "/login-student", + accessCounterMiddleware, authStudentLoginValidationMiddleware(), errorMiddleware, authController.loginStudentController.bind(authController), @@ -41,6 +45,7 @@ authRouter.post( authRouter.post( "/login-teacher", + accessCounterMiddleware, authStudentLoginValidationMiddleware(), errorMiddleware, authController.loginTeacherController.bind(authController), @@ -54,6 +59,7 @@ authRouter.get( authRouter.post( "/refresh-token", + accessCounterMiddleware, refreshTokenMiddleware.handle, authController.refreshController.bind(authController), );