feat(matches): implement spam protection and socket notifications

S15.1-15.2: Rate Limiting & Spam Protection
- Add max 20 pending outgoing match requests limit
- Implement rate limiter: 10 match requests per minute per user
- Return 429 status with clear error messages

S16.1: Socket Notifications for New Suggestions
- Emit 'recording_suggestions_created' event when matching creates suggestions
- Notify only assigned recorders (not NOT_FOUND status)
- Group suggestions by recorder for efficiency
- Include event details and suggestion count

Implementation:
- backend/src/routes/matches.js: Rate limiter + pending limit check
- backend/src/services/matching.js: Socket notifications in saveMatchingResults
- backend/src/__tests__/spam-protection-notifications.test.js: 8 test cases

Test coverage:
- TC1-TC3: Max pending requests (spam protection)
- TC4-TC5: Rate limiting (10/min)
- TC6-TC8: Socket notifications for new suggestions
This commit is contained in:
Radosław Gierwiało
2025-12-01 00:03:46 +01:00
parent 964897bdc0
commit ec659d83e8
3 changed files with 461 additions and 1 deletions

View File

@@ -1,4 +1,5 @@
const express = require('express');
const rateLimit = require('express-rate-limit');
const { prisma } = require('../utils/db');
const { authenticate } = require('../middleware/auth');
const { getIO } = require('../socket');
@@ -7,8 +8,20 @@ const matchingService = require('../services/matching');
const router = express.Router();
// Rate limiter for match creation: 10 requests per minute per user
const matchRequestLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 10, // 10 requests per minute
message: { success: false, error: 'Too many match requests. Please wait a minute before trying again.' },
standardHeaders: true,
legacyHeaders: false,
// Use user ID as key (from authenticate middleware)
keyGenerator: (req) => req.user?.id?.toString() || 'unauthenticated',
skip: (req) => !req.user, // Skip if not authenticated (will fail at authenticate middleware)
});
// POST /api/matches - Create a match request
router.post('/', authenticate, async (req, res, next) => {
router.post('/', authenticate, matchRequestLimiter, async (req, res, next) => {
try {
const { targetUserId, eventSlug } = req.body;
const requesterId = req.user.id;
@@ -28,6 +41,22 @@ router.post('/', authenticate, async (req, res, next) => {
});
}
// S15.1: Check max pending outgoing requests (spam protection)
const pendingOutgoingCount = await prisma.match.count({
where: {
user1Id: requesterId, // user1 is the requester
status: MATCH_STATUS.PENDING,
},
});
if (pendingOutgoingCount >= 20) {
return res.status(429).json({
success: false,
error: 'You have too many pending match requests. Please wait for some to be accepted or rejected before sending more.',
pendingCount: pendingOutgoingCount,
});
}
// Find event by slug
const event = await prisma.event.findUnique({
where: { slug: eventSlug },