feat: replace fixed login rate limit with progressive lockout

Made-with: Cursor
This commit is contained in:
Anton
2026-03-04 17:55:25 +03:00
parent e3d7f1d24c
commit dcc4fd370a
5 changed files with 141 additions and 15 deletions

View File

@@ -1,5 +1,6 @@
import type { FastifyInstance } from 'fastify';
import { AuthService } from '../services/auth/auth.service.js';
import { checkBlocked, clearOnSuccess, recordFailedAttempt } from '../utils/loginLockout.js';
const registerSchema = {
body: {
@@ -89,20 +90,43 @@ export async function authRoutes(app: FastifyInstance) {
app.post(
'/login',
{ schema: loginSchema, config: { rateLimit: rateLimitOptions.login } },
{
schema: loginSchema,
preValidation: async (req, reply) => {
const ip = req.ip ?? 'unknown';
const { blocked, retryAfter } = await checkBlocked(app.redis, ip);
if (blocked) {
if (retryAfter !== undefined) {
reply.header('Retry-After', String(retryAfter));
}
return reply.status(429).send({
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Too many failed login attempts. Please try again later.',
retryAfter,
},
});
}
},
},
async (req, reply) => {
const body = req.body as { email: string; password: string };
const userAgent = req.headers['user-agent'];
const ipAddress = req.ip;
const ip = req.ip ?? 'unknown';
const result = await authService.login({
email: body.email,
password: body.password,
userAgent,
ipAddress,
});
return reply.send(result);
try {
const result = await authService.login({
email: body.email,
password: body.password,
userAgent,
ipAddress: ip,
});
await clearOnSuccess(app.redis, ip);
return reply.send(result);
} catch (err) {
await recordFailedAttempt(app.redis, ip);
throw err;
}
},
);