Allow message retries for "pending" status, refactor message context handling, remove workspace import/export routes, and add SQLite-backed rate limiting.
This commit is contained in:
@@ -8,7 +8,7 @@ function pruneBucket(bucket, windowMs, now) {
|
||||
}
|
||||
}
|
||||
|
||||
export function createRateLimiter({windowMs, max, keyGenerator, message, cleanupIntervalMs = 60 * 1000}) {
|
||||
export function createRateLimiter({windowMs, max, keyGenerator, message, cleanupIntervalMs = 60 * 1000, store = null}) {
|
||||
const hits = new Map();
|
||||
let cleanupTimer = null;
|
||||
|
||||
@@ -22,7 +22,7 @@ export function createRateLimiter({windowMs, max, keyGenerator, message, cleanup
|
||||
}
|
||||
};
|
||||
|
||||
if (cleanupIntervalMs > 0) {
|
||||
if (!store && cleanupIntervalMs > 0) {
|
||||
cleanupTimer = setInterval(cleanup, cleanupIntervalMs);
|
||||
// Der Aufräumtimer allein soll den Node-Prozess beim Herunterfahren nicht offen halten.
|
||||
cleanupTimer.unref();
|
||||
@@ -35,6 +35,10 @@ export function createRateLimiter({windowMs, max, keyGenerator, message, cleanup
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (store) {
|
||||
if (!store.consume(key, now, windowMs, max)) return res.status(429).json({error: message});
|
||||
return next();
|
||||
}
|
||||
const bucket = hits.get(key) || [];
|
||||
pruneBucket(bucket, windowMs, now);
|
||||
if (!bucket.length) {
|
||||
@@ -59,3 +63,24 @@ export function createRateLimiter({windowMs, max, keyGenerator, message, cleanup
|
||||
|
||||
return limiter;
|
||||
}
|
||||
|
||||
// SQLite is already shared by all application instances and serializes this
|
||||
// short transaction, so the check-and-increment remains atomic across workers.
|
||||
export function createSqliteRateLimitStore(db) {
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS rate_limit_hits (
|
||||
bucket_key TEXT NOT NULL,
|
||||
hit_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS rate_limit_hits_expiry ON rate_limit_hits(hit_at);`);
|
||||
db.pragma("busy_timeout = 5000");
|
||||
const prune = db.prepare("DELETE FROM rate_limit_hits WHERE bucket_key = ? AND hit_at <= ?");
|
||||
const count = db.prepare("SELECT COUNT(*) AS count FROM rate_limit_hits WHERE bucket_key = ?");
|
||||
const insert = db.prepare("INSERT INTO rate_limit_hits (bucket_key, hit_at) VALUES (?, ?)");
|
||||
const consume = db.transaction((key, now, windowMs, max) => {
|
||||
prune.run(key, now - windowMs);
|
||||
if (count.get(key).count >= max) return false;
|
||||
insert.run(key, now);
|
||||
return true;
|
||||
});
|
||||
return {consume};
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
*/
|
||||
import {createRateLimiter} from "./rateLimit.js";
|
||||
|
||||
export function createAuthRateLimiters(config) {
|
||||
export function createAuthRateLimiters(config, store = null) {
|
||||
return [
|
||||
createRateLimiter({
|
||||
windowMs: config.authRateLimitWindowMs,
|
||||
max: config.authRateLimitMaxPerIp,
|
||||
cleanupIntervalMs: config.rateLimitCleanupIntervalMs,
|
||||
store,
|
||||
keyGenerator: (req) => `auth:ip:${req.ip}`,
|
||||
message: "Too many authentication attempts from this IP. Please try again later."
|
||||
}),
|
||||
@@ -16,6 +17,7 @@ export function createAuthRateLimiters(config) {
|
||||
windowMs: config.authRateLimitWindowMs,
|
||||
max: config.authRateLimitMaxPerAccount,
|
||||
cleanupIntervalMs: config.rateLimitCleanupIntervalMs,
|
||||
store,
|
||||
keyGenerator: (req) => {
|
||||
const username = String(req.body?.username || "").trim().toLowerCase();
|
||||
return username ? `auth:user:${username}` : null;
|
||||
@@ -25,12 +27,13 @@ export function createAuthRateLimiters(config) {
|
||||
];
|
||||
}
|
||||
|
||||
export function createChatRateLimiters(config) {
|
||||
export function createChatRateLimiters(config, store = null) {
|
||||
return [
|
||||
createRateLimiter({
|
||||
windowMs: config.chatRateLimitWindowMs,
|
||||
max: config.chatRateLimitMaxPerIp,
|
||||
cleanupIntervalMs: config.rateLimitCleanupIntervalMs,
|
||||
store,
|
||||
keyGenerator: (req) => `chat:ip:${req.ip}`,
|
||||
message: "Too many chat requests from this IP. Please slow down."
|
||||
}),
|
||||
@@ -38,6 +41,7 @@ export function createChatRateLimiters(config) {
|
||||
windowMs: config.chatRateLimitWindowMs,
|
||||
max: config.chatRateLimitMaxPerAccount,
|
||||
cleanupIntervalMs: config.rateLimitCleanupIntervalMs,
|
||||
store,
|
||||
keyGenerator: (req) => req.session.user ? `chat:user:${req.session.user}` : null,
|
||||
message: "Too many chat requests for this account. Please slow down."
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user