v2: модульный монолит — очередь, персистентность, безопасность, SSE, тесты
Some checks failed
test / test (push) Failing after 24s
Some checks failed
test / test (push) Failing after 24s
Реализация целевого дизайна (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/
This commit is contained in:
99
src/http/middleware/rate-limit.js
Normal file
99
src/http/middleware/rate-limit.js
Normal file
@@ -0,0 +1,99 @@
|
||||
"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,
|
||||
};
|
||||
Reference in New Issue
Block a user