Files
photo-editor/src/http/middleware/rate-limit.js
Coder 401969cfdb
Some checks failed
test / test (push) Failing after 24s
v2: модульный монолит — очередь, персистентность, безопасность, SSE, тесты
Реализация целевого дизайна (docs/architecture/target-design.md):
- src/: модульная структура (config, logger, errors, http, core: auth/history/jobs/engines/comfy/codex/images)
- Очередь заданий: FIFO, кониурентность codex=1 / comfy-upscale=2, MAX_QUEUED_JOBS=5, JSONL-журнал и восстановление после рестарта (running → interrupted, upscale requeue)
- Персистентные сессии (sha256-хеши токенов), scrypt-хеш пароля, rate-limit входа, Origin-проверка, magic-byte валидация аплоадов
- Атомарная запись манифеста истории, каскадное удаление, URL с фактическим расширением + 302-алиас /image.jpg, миграция history/ → data/history
- Даунскейл sharp до 1024px перед Codex + масштабирование области референса
- SSE /api/events с фолбэком на polling, фронтенд переведён на ES-модули (public/js/)
- Тесты node:test + supertest (27), CI workflow test.yml, Docker/деплой: том kadr-data
- Документация: README, docs/architecture/
2026-08-27 12:52:27 +07:00

100 lines
2.6 KiB
JavaScript

"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<string, number[]>} 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,
};