From 08c32bcf0fed60808c808a98e375d3bd083689a9 Mon Sep 17 00:00:00 2001 From: Kenearos Date: Tue, 7 Jul 2026 22:36:10 +0200 Subject: [PATCH] =?UTF-8?q?feat(auth):=20Story=202.1=20=E2=80=94=20In-Memo?= =?UTF-8?q?ry=20Rate-Limiter=20(pro=20Key)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/ratelimit.js | 23 +++++++++++++++++++++++ server/ratelimit.test.js | 18 ++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 server/ratelimit.js create mode 100644 server/ratelimit.test.js diff --git a/server/ratelimit.js b/server/ratelimit.js new file mode 100644 index 0000000..0738f21 --- /dev/null +++ b/server/ratelimit.js @@ -0,0 +1,23 @@ +// In-Memory Rate-Limiter pro Key (Sliding Fixed-Window). +// ponytail: per-Prozess, resettet bei Container-Restart — bei Magic-Link (kein +// Passwort-Brute-Force) akzeptierte Ceiling; Persistenz = YAGNI. +const buckets = new Map(); + +/** + * Zählt einen Treffer für `key`. Gibt true zurück, solange das Limit im Fenster + * nicht überschritten ist, sonst false (→ Aufrufer antwortet 429). + */ +function hit(key, limit, windowMin) { + const now = Date.now(); + let b = buckets.get(key); + if (!b || now >= b.resetAt) { + b = { count: 0, resetAt: now + windowMin * 60 * 1000 }; + buckets.set(key, b); + } + b.count += 1; + return b.count <= limit; +} + +function _reset() { buckets.clear(); } // nur für Tests + +module.exports = { hit, _reset }; diff --git a/server/ratelimit.test.js b/server/ratelimit.test.js new file mode 100644 index 0000000..061bbef --- /dev/null +++ b/server/ratelimit.test.js @@ -0,0 +1,18 @@ +const { test } = require('node:test'); +const assert = require('node:assert'); +const { hit, _reset } = require('./ratelimit'); + +test('erlaubt bis zum Limit, blockt danach', () => { + _reset(); + assert.strictEqual(hit('a@x.de', 3, 15), true); + assert.strictEqual(hit('a@x.de', 3, 15), true); + assert.strictEqual(hit('a@x.de', 3, 15), true); + assert.strictEqual(hit('a@x.de', 3, 15), false, '4. Anfrage geblockt'); +}); + +test('verschiedene Keys sind unabhängig (E-Mail vs IP)', () => { + _reset(); + assert.strictEqual(hit('email:a@x.de', 1, 15), true); + assert.strictEqual(hit('email:a@x.de', 1, 15), false); + assert.strictEqual(hit('ip:1.2.3.4', 100, 15), true, 'anderer Key unberührt'); +});