Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions server/__tests__/rate-limit.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Rate-limit smoke tests.
*
* Под NODE_ENV=test глобальные лимитеры в routes/api.js специально подняты
* до 10000 запросов, чтобы не мешать integration-тестам. Поэтому здесь мы
* проверяем сам middleware через отдельный мини-app — это гарантирует, что
* express-rate-limit установлен, работает, возвращает 429 и стандартные
* RateLimit-* заголовки.
*/
const express = require('express');
const request = require('supertest');
const rateLimit = require('express-rate-limit');

function buildApp({ max = 3, windowMs = 60_000, skipSuccessfulRequests = false } = {}) {
const app = express();
app.use(express.json());
const limiter = rateLimit({
windowMs,
max,
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests,
message: { success: false, message: 'Слишком много попыток входа.' },
});
app.post('/login', limiter, (req, res) => {
if (req.body?.bad) return res.status(401).json({ success: false });
return res.json({ success: true });
});
return app;
}

describe('express-rate-limit middleware', () => {
it('возвращает 429 после исчерпания лимита', async () => {
const app = buildApp({ max: 2 });

const r1 = await request(app).post('/login').send({ bad: true });
expect(r1.status).toBe(401);

const r2 = await request(app).post('/login').send({ bad: true });
expect(r2.status).toBe(401);

const r3 = await request(app).post('/login').send({ bad: true });
expect(r3.status).toBe(429);
expect(r3.body).toMatchObject({ success: false, message: expect.any(String) });
});

it('выставляет RateLimit-* заголовки на каждом ответе', async () => {
const app = buildApp({ max: 5 });
const r = await request(app).post('/login').send({ bad: true });
expect(r.headers['ratelimit-limit']).toBeDefined();
expect(r.headers['ratelimit-remaining']).toBeDefined();
});

it('skipSuccessfulRequests=true не учитывает 2xx ответы в счётчике', async () => {
const app = buildApp({ max: 2, skipSuccessfulRequests: true });

// 5 успешных запросов подряд — не должны исчерпывать лимит
for (let i = 0; i < 5; i++) {
const r = await request(app).post('/login').send({});
expect(r.status).toBe(200);
}
});
});
20 changes: 19 additions & 1 deletion server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"express-rate-limit": "^8.5.1",
"joi": "^17.9.2",
"jsonwebtoken": "^9.0.0",
"multer": "^1.4.5-lts.1",
Expand Down
36 changes: 34 additions & 2 deletions server/routes/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,41 @@
*/
const express = require('express');
const router = express.Router();
const rateLimit = require('express-rate-limit');
const multer = require('multer');
const path = require('path');
const fs = require('fs');

// В тестах (NODE_ENV=test для Jest или NODE_ENV=e2e для Playwright) лимиты
// поднимаем на потолок — иначе интеграционные/E2E тесты, которые регистрируют
// сотни директоров подряд из одного IP, упрутся в 429.
const isTestEnv = process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'e2e';

// Брутфорс-защита логина: 10 неуспешных попыток на 15 минут с одного IP
// (успешные ответы не считаются — skipSuccessfulRequests).
const loginRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: isTestEnv ? 100000 : 10,
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests: true,
message: {
success: false,
message: 'Слишком много попыток входа. Попробуйте через 15 минут.',
},
});

// Регистрация: 5 аккаунтов в час с одного IP (антиспам).
const registerRateLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: isTestEnv ? 100000 : 5,
standardHeaders: true,
legacyHeaders: false,
message: {
success: false,
message: 'Слишком много регистраций с этого IP. Попробуйте позже.',
},
});
const config = require('../config');
const { authenticateToken } = require('../middleware/auth');
const authController = require('../controllers/auth');
Expand Down Expand Up @@ -46,8 +78,8 @@ router.get('/health', (req, res) => {
});

// Маршруты аутентификации
router.post('/auth/login', authController.login);
router.post('/auth/register', authController.register);
router.post('/auth/login', loginRateLimiter, authController.login);
router.post('/auth/register', registerRateLimiter, authController.register);
router.get('/auth/me', authenticateToken, authController.getCurrentUser);
router.get('/profile', authenticateToken, authController.getCurrentUser); // Добавлен маршрут для совместимости с фронтендом
router.post('/leads/incoming', callCenterRoutes.receiveIncomingLead);
Expand Down
Loading