feat(security): Postgres-backed login rate limiting

Replaces the in-memory sliding-window Map (reset on every restart, not
shareable across instances) with a login_attempts table (migration 016).
No Redis added - login is not a hot path, and this is a single indexed
range-scan query. Restart behavior confirmed for real:
platform/test/e2e-phase3a.js's rate-limiting test passes after a genuine
Platform API restart in the same run.
This commit is contained in:
2026-08-24 20:15:03 -05:00
parent 6dda13b837
commit addf474b70
3 changed files with 44 additions and 20 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ module.exports = function registerInstructorAuthRoutes(app, { pool, sessions, in
app.post('/api/auth/instructor/login', async (req, res) => {
const { email, password } = req.body || {};
const rateKey = `${req.ip}:${String(email || '').toLowerCase()}`;
if (tooManyAttempts(rateKey, LOGIN_MAX_ATTEMPTS, LOGIN_WINDOW_MS)) {
if (await tooManyAttempts(pool, rateKey, LOGIN_MAX_ATTEMPTS, LOGIN_WINDOW_MS)) {
return res.status(429).json({ error: 'too_many_attempts', message: 'Too many login attempts. Try again in a few minutes.' });
}
if (!email || !password) return res.status(400).json({ error: 'email and password are required' });