"use strict"; const { TooManyRequestsError } = require("../../errors"); function getClientIp(req) { return ( req.ip || req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket?.remoteAddress || "127.0.0.1" ); } class RateLimiter { /** * @param {Object} options * @param {number} [options.windowMs=900000] - 15 минут * @param {number} [options.max=5] - Максимум попыток * @param {import('../../logger').Logger} [options.logger] */ constructor({ windowMs = 15 * 60 * 1000, max = 5, logger = null }) { this.windowMs = windowMs; this.max = max; this.logger = logger; /** @type {Map} IP -> array of failure timestamps */ this.failures = new Map(); this._cleanupInterval = setInterval(() => this.cleanup(), this.windowMs); if (this._cleanupInterval.unref) { this._cleanupInterval.unref(); } } middleware() { return (req, res, next) => { const ip = getClientIp(req); const now = Date.now(); const timestamps = this.failures.get(ip) || []; // Filter timestamps within current window const recent = timestamps.filter((t) => now - t < this.windowMs); this.failures.set(ip, recent); if (recent.length >= this.max) { if (this.logger) { this.logger.warn("Превышен лимит попыток входа (rate limit)", { ip, attempts: recent.length, }); } return next( new TooManyRequestsError( "Слишком много неудачных попыток входа. Подождите 15 минут перед следующей попыткой." ) ); } next(); }; } recordFailure(req) { const ip = getClientIp(req); const now = Date.now(); const timestamps = this.failures.get(ip) || []; const recent = timestamps.filter((t) => now - t < this.windowMs); recent.push(now); this.failures.set(ip, recent); } recordSuccess(req) { const ip = getClientIp(req); this.failures.delete(ip); } cleanup() { const now = Date.now(); for (const [ip, timestamps] of this.failures) { const recent = timestamps.filter((t) => now - t < this.windowMs); if (recent.length === 0) { this.failures.delete(ip); } else { this.failures.set(ip, recent); } } } close() { if (this._cleanupInterval) { clearInterval(this._cleanupInterval); this._cleanupInterval = null; } } } module.exports = { RateLimiter, getClientIp, };