add changes

This commit is contained in:
AD2025
2025-11-12 23:06:27 +02:00
parent c664d0a341
commit ec6534fcc2
42 changed files with 11854 additions and 299 deletions

267
backend/middleware/cache.js Normal file
View File

@@ -0,0 +1,267 @@
const { getCache, setCache, deleteCache } = require('../config/redis');
const logger = require('../config/logger');
/**
* Cache middleware for GET requests
* @param {number} ttl - Time to live in seconds (default: 300 = 5 minutes)
* @param {function} keyGenerator - Function to generate cache key from req
*/
const cacheMiddleware = (ttl = 300, keyGenerator = null) => {
return async (req, res, next) => {
// Only cache GET requests
if (req.method !== 'GET') {
return next();
}
try {
// Generate cache key
const cacheKey = keyGenerator
? keyGenerator(req)
: `cache:${req.originalUrl}`;
// Try to get from cache
const cachedData = await getCache(cacheKey);
if (cachedData) {
logger.debug(`Cache hit for: ${cacheKey}`);
return res.status(200).json(cachedData);
}
// Cache miss - store original json method
const originalJson = res.json.bind(res);
// Override json method to cache response
res.json = function(data) {
// Only cache successful responses
if (res.statusCode === 200) {
setCache(cacheKey, data, ttl).catch(err => {
logger.error('Failed to cache response:', err);
});
}
// Call original json method
return originalJson(data);
};
next();
} catch (error) {
logger.error('Cache middleware error:', error);
next(); // Continue even if cache fails
}
};
};
/**
* Cache categories (rarely change)
*/
const cacheCategories = cacheMiddleware(3600, (req) => {
// Cache for 1 hour
return 'cache:categories:list';
});
/**
* Cache single category
*/
const cacheSingleCategory = cacheMiddleware(3600, (req) => {
return `cache:category:${req.params.id}`;
});
/**
* Cache guest settings (rarely change)
*/
const cacheGuestSettings = cacheMiddleware(1800, (req) => {
// Cache for 30 minutes
return 'cache:guest:settings';
});
/**
* Cache system statistics (update frequently)
*/
const cacheStatistics = cacheMiddleware(300, (req) => {
// Cache for 5 minutes
return 'cache:admin:statistics';
});
/**
* Cache guest analytics
*/
const cacheGuestAnalytics = cacheMiddleware(600, (req) => {
// Cache for 10 minutes
return 'cache:admin:guest-analytics';
});
/**
* Cache user dashboard
*/
const cacheUserDashboard = cacheMiddleware(300, (req) => {
// Cache for 5 minutes
const userId = req.params.userId || req.user?.id;
return `cache:user:${userId}:dashboard`;
});
/**
* Cache questions list (with filters)
*/
const cacheQuestions = cacheMiddleware(600, (req) => {
// Cache for 10 minutes
const { categoryId, difficulty, questionType, visibility } = req.query;
const filters = [categoryId, difficulty, questionType, visibility]
.filter(Boolean)
.join(':');
return `cache:questions:${filters || 'all'}`;
});
/**
* Cache single question
*/
const cacheSingleQuestion = cacheMiddleware(1800, (req) => {
return `cache:question:${req.params.id}`;
});
/**
* Cache user bookmarks
*/
const cacheUserBookmarks = cacheMiddleware(300, (req) => {
const userId = req.params.userId || req.user?.id;
return `cache:user:${userId}:bookmarks`;
});
/**
* Cache user history
*/
const cacheUserHistory = cacheMiddleware(300, (req) => {
const userId = req.params.userId || req.user?.id;
const page = req.query.page || 1;
return `cache:user:${userId}:history:page:${page}`;
});
/**
* Invalidate cache patterns
*/
const invalidateCache = {
/**
* Invalidate user-related cache
*/
user: async (userId) => {
await deleteCache(`cache:user:${userId}:*`);
logger.debug(`Invalidated cache for user ${userId}`);
},
/**
* Invalidate category cache
*/
category: async (categoryId = null) => {
if (categoryId) {
await deleteCache(`cache:category:${categoryId}`);
}
await deleteCache('cache:categories:*');
logger.debug('Invalidated category cache');
},
/**
* Invalidate question cache
*/
question: async (questionId = null) => {
if (questionId) {
await deleteCache(`cache:question:${questionId}`);
}
await deleteCache('cache:questions:*');
logger.debug('Invalidated question cache');
},
/**
* Invalidate statistics cache
*/
statistics: async () => {
await deleteCache('cache:admin:statistics');
await deleteCache('cache:admin:guest-analytics');
logger.debug('Invalidated statistics cache');
},
/**
* Invalidate guest settings cache
*/
guestSettings: async () => {
await deleteCache('cache:guest:settings');
logger.debug('Invalidated guest settings cache');
},
/**
* Invalidate all quiz-related cache
*/
quiz: async (userId = null, guestId = null) => {
if (userId) {
await deleteCache(`cache:user:${userId}:*`);
}
if (guestId) {
await deleteCache(`cache:guest:${guestId}:*`);
}
await invalidateCache.statistics();
logger.debug('Invalidated quiz cache');
}
};
/**
* Middleware to invalidate cache after mutations
*/
const invalidateCacheMiddleware = (pattern) => {
return async (req, res, next) => {
// Store original json method
const originalJson = res.json.bind(res);
// Override json method
res.json = async function(data) {
// Only invalidate on successful mutations
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
if (typeof pattern === 'function') {
await pattern(req);
} else {
await deleteCache(pattern);
}
} catch (error) {
logger.error('Cache invalidation error:', error);
}
}
// Call original json method
return originalJson(data);
};
next();
};
};
/**
* Cache warming - preload frequently accessed data
*/
const warmCache = async () => {
try {
logger.info('Warming cache...');
// This would typically fetch and cache common data
// For now, we'll just log the intent
// In a real scenario, you'd fetch categories, popular questions, etc.
logger.info('Cache warming complete');
} catch (error) {
logger.error('Cache warming error:', error);
}
};
module.exports = {
cacheMiddleware,
cacheCategories,
cacheSingleCategory,
cacheGuestSettings,
cacheStatistics,
cacheGuestAnalytics,
cacheUserDashboard,
cacheQuestions,
cacheSingleQuestion,
cacheUserBookmarks,
cacheUserHistory,
invalidateCache,
invalidateCacheMiddleware,
warmCache
};

View File

@@ -0,0 +1,248 @@
const logger = require('../config/logger');
const { AppError } = require('../utils/AppError');
/**
* Handle Sequelize validation errors
*/
const handleSequelizeValidationError = (error) => {
const errors = error.errors.map(err => ({
field: err.path,
message: err.message,
value: err.value
}));
return {
statusCode: 400,
message: 'Validation error',
errors
};
};
/**
* Handle Sequelize unique constraint errors
*/
const handleSequelizeUniqueConstraintError = (error) => {
const field = error.errors[0]?.path;
const value = error.errors[0]?.value;
return {
statusCode: 409,
message: `${field} '${value}' already exists`
};
};
/**
* Handle Sequelize foreign key constraint errors
*/
const handleSequelizeForeignKeyConstraintError = (error) => {
return {
statusCode: 400,
message: 'Invalid reference to related resource'
};
};
/**
* Handle Sequelize database connection errors
*/
const handleSequelizeConnectionError = (error) => {
return {
statusCode: 503,
message: 'Database connection error. Please try again later.'
};
};
/**
* Handle JWT errors
*/
const handleJWTError = () => {
return {
statusCode: 401,
message: 'Invalid token. Please log in again.'
};
};
/**
* Handle JWT expired errors
*/
const handleJWTExpiredError = () => {
return {
statusCode: 401,
message: 'Your token has expired. Please log in again.'
};
};
/**
* Handle Sequelize errors
*/
const handleSequelizeError = (error) => {
// Validation error
if (error.name === 'SequelizeValidationError') {
return handleSequelizeValidationError(error);
}
// Unique constraint violation
if (error.name === 'SequelizeUniqueConstraintError') {
return handleSequelizeUniqueConstraintError(error);
}
// Foreign key constraint violation
if (error.name === 'SequelizeForeignKeyConstraintError') {
return handleSequelizeForeignKeyConstraintError(error);
}
// Database connection error
if (error.name === 'SequelizeConnectionError' ||
error.name === 'SequelizeConnectionRefusedError' ||
error.name === 'SequelizeHostNotFoundError' ||
error.name === 'SequelizeAccessDeniedError') {
return handleSequelizeConnectionError(error);
}
// Generic database error
return {
statusCode: 500,
message: 'Database error occurred'
};
};
/**
* Send error response in development
*/
const sendErrorDev = (err, res) => {
res.status(err.statusCode).json({
status: err.status,
message: err.message,
error: err,
stack: err.stack,
...(err.errors && { errors: err.errors })
});
};
/**
* Send error response in production
*/
const sendErrorProd = (err, res) => {
// Operational, trusted error: send message to client
if (err.isOperational) {
res.status(err.statusCode).json({
status: err.status,
message: err.message,
...(err.errors && { errors: err.errors })
});
}
// Programming or unknown error: don't leak error details
else {
// Log error for debugging
logger.error('ERROR 💥', err);
// Send generic message
res.status(500).json({
status: 'error',
message: 'Something went wrong. Please try again later.'
});
}
};
/**
* Centralized error handling middleware
*/
const errorHandler = (err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
// Log the error
logger.logError(err, req);
// Handle specific error types
let error = { ...err };
error.message = err.message;
error.stack = err.stack;
// Sequelize errors
if (err.name && err.name.startsWith('Sequelize')) {
const handled = handleSequelizeError(err);
error.statusCode = handled.statusCode;
error.message = handled.message;
error.isOperational = true;
if (handled.errors) error.errors = handled.errors;
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
const handled = handleJWTError();
error.statusCode = handled.statusCode;
error.message = handled.message;
error.isOperational = true;
}
if (err.name === 'TokenExpiredError') {
const handled = handleJWTExpiredError();
error.statusCode = handled.statusCode;
error.message = handled.message;
error.isOperational = true;
}
// Multer errors (file upload)
if (err.name === 'MulterError') {
error.statusCode = 400;
error.message = `File upload error: ${err.message}`;
error.isOperational = true;
}
// Send error response
if (process.env.NODE_ENV === 'development') {
sendErrorDev(error, res);
} else {
sendErrorProd(error, res);
}
};
/**
* Handle async errors (wrap async route handlers)
*/
const catchAsync = (fn) => {
return (req, res, next) => {
fn(req, res, next).catch(next);
};
};
/**
* Handle 404 Not Found errors
*/
const notFoundHandler = (req, res, next) => {
const error = new AppError(
`Cannot find ${req.originalUrl} on this server`,
404
);
next(error);
};
/**
* Log unhandled rejections
*/
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection at:', {
promise,
reason: reason.stack || reason
});
// Optional: Exit process in production
// process.exit(1);
});
/**
* Log uncaught exceptions
*/
process.on('uncaughtException', (error) => {
logger.error('Uncaught Exception:', {
message: error.message,
stack: error.stack
});
// Exit process on uncaught exception
process.exit(1);
});
module.exports = {
errorHandler,
catchAsync,
notFoundHandler
};

View File

@@ -0,0 +1,150 @@
const rateLimit = require('express-rate-limit');
const logger = require('../config/logger');
/**
* Create a custom rate limit handler
*/
const createRateLimitHandler = (req, res) => {
logger.logSecurityEvent('Rate limit exceeded', req);
res.status(429).json({
status: 'error',
message: 'Too many requests from this IP, please try again later.',
retryAfter: res.getHeader('Retry-After')
});
};
/**
* General API rate limiter
* 100 requests per 15 minutes per IP
*/
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again after 15 minutes',
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
handler: createRateLimitHandler,
skip: (req) => {
// Skip rate limiting for health check endpoint
return req.path === '/health';
}
});
/**
* Strict rate limiter for authentication endpoints
* 5 requests per 15 minutes per IP
*/
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Limit each IP to 5 requests per windowMs
message: 'Too many authentication attempts, please try again after 15 minutes',
standardHeaders: true,
legacyHeaders: false,
handler: createRateLimitHandler,
skipSuccessfulRequests: false // Count all requests, including successful ones
});
/**
* Rate limiter for login attempts
* More restrictive - 5 attempts per 15 minutes
*/
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: 'Too many login attempts, please try again after 15 minutes',
standardHeaders: true,
legacyHeaders: false,
handler: createRateLimitHandler,
skipFailedRequests: false // Count both successful and failed attempts
});
/**
* Rate limiter for registration
* 3 registrations per hour per IP
*/
const registerLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 3,
message: 'Too many accounts created from this IP, please try again after an hour',
standardHeaders: true,
legacyHeaders: false,
handler: createRateLimitHandler
});
/**
* Rate limiter for password reset
* 3 requests per hour per IP
*/
const passwordResetLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 3,
message: 'Too many password reset attempts, please try again after an hour',
standardHeaders: true,
legacyHeaders: false,
handler: createRateLimitHandler
});
/**
* Rate limiter for quiz creation
* 30 quizzes per hour per user
*/
const quizLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 30,
message: 'Too many quizzes started, please try again later',
standardHeaders: true,
legacyHeaders: false,
handler: createRateLimitHandler
});
/**
* Rate limiter for admin operations
* 100 requests per 15 minutes
*/
const adminLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: 'Too many admin requests, please try again later',
standardHeaders: true,
legacyHeaders: false,
handler: createRateLimitHandler
});
/**
* Rate limiter for guest session creation
* 5 guest sessions per hour per IP
*/
const guestSessionLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 5,
message: 'Too many guest sessions created, please try again later',
standardHeaders: true,
legacyHeaders: false,
handler: createRateLimitHandler
});
/**
* Rate limiter for API documentation
* Prevent abuse of documentation endpoint
*/
const docsLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 50,
message: 'Too many requests to documentation, please try again later',
standardHeaders: true,
legacyHeaders: false,
handler: createRateLimitHandler
});
module.exports = {
apiLimiter,
authLimiter,
loginLimiter,
registerLimiter,
passwordResetLimiter,
quizLimiter,
adminLimiter,
guestSessionLimiter,
docsLimiter
};

View File

@@ -0,0 +1,262 @@
const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');
const hpp = require('hpp');
const { body, param, query, validationResult } = require('express-validator');
const { BadRequestError } = require('../utils/AppError');
/**
* MongoDB NoSQL Injection Prevention
* Removes $ and . characters from user input to prevent NoSQL injection
*/
const sanitizeMongoData = mongoSanitize({
replaceWith: '_',
onSanitize: ({ req, key }) => {
const logger = require('../config/logger');
logger.logSecurityEvent(`MongoDB injection attempt detected in ${key}`, req);
}
});
/**
* XSS (Cross-Site Scripting) Prevention
* Sanitizes user input to prevent XSS attacks
*/
const sanitizeXSS = xss();
/**
* HTTP Parameter Pollution Prevention
* Protects against attacks where parameters are sent multiple times
*/
const preventHPP = hpp({
whitelist: [
// Query parameters that are allowed to have multiple values
'category',
'categoryId',
'difficulty',
'tags',
'keywords',
'sort',
'fields'
]
});
/**
* Sanitize request body, query, and params
* Custom middleware that runs after mongoSanitize and xss
*/
const sanitizeInput = (req, res, next) => {
// Additional sanitization for specific patterns
const sanitizeObject = (obj) => {
if (!obj || typeof obj !== 'object') return obj;
for (const key in obj) {
if (typeof obj[key] === 'string') {
// Remove null bytes
obj[key] = obj[key].replace(/\0/g, '');
// Trim whitespace
obj[key] = obj[key].trim();
// Remove any remaining dangerous patterns
obj[key] = obj[key]
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/javascript:/gi, '')
.replace(/on\w+\s*=/gi, '');
} else if (typeof obj[key] === 'object') {
sanitizeObject(obj[key]);
}
}
return obj;
};
if (req.body) sanitizeObject(req.body);
if (req.query) sanitizeObject(req.query);
if (req.params) sanitizeObject(req.params);
next();
};
/**
* Validate and sanitize email addresses
*/
const sanitizeEmail = [
body('email')
.trim()
.toLowerCase()
.isEmail().withMessage('Invalid email format')
.normalizeEmail({
gmail_remove_dots: false,
gmail_remove_subaddress: false,
outlookdotcom_remove_subaddress: false,
yahoo_remove_subaddress: false,
icloud_remove_subaddress: false
})
.isLength({ max: 255 }).withMessage('Email too long')
];
/**
* Validate and sanitize passwords
*/
const sanitizePassword = [
body('password')
.trim()
.isLength({ min: 8 }).withMessage('Password must be at least 8 characters')
.isLength({ max: 128 }).withMessage('Password too long')
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/)
.withMessage('Password must contain uppercase, lowercase, number and special character')
];
/**
* Validate and sanitize usernames
*/
const sanitizeUsername = [
body('username')
.trim()
.isLength({ min: 3, max: 30 }).withMessage('Username must be 3-30 characters')
.matches(/^[a-zA-Z0-9_-]+$/).withMessage('Username can only contain letters, numbers, underscores and hyphens')
];
/**
* Validate and sanitize numeric IDs
*/
const sanitizeId = [
param('id')
.isInt({ min: 1 }).withMessage('Invalid ID')
.toInt()
];
/**
* Validate and sanitize pagination parameters
*/
const sanitizePagination = [
query('page')
.optional()
.isInt({ min: 1, max: 10000 }).withMessage('Invalid page number')
.toInt(),
query('limit')
.optional()
.isInt({ min: 1, max: 100 }).withMessage('Limit must be between 1 and 100')
.toInt(),
query('sort')
.optional()
.trim()
.isIn(['asc', 'desc', 'ASC', 'DESC']).withMessage('Sort must be asc or desc')
];
/**
* Validate and sanitize search queries
*/
const sanitizeSearch = [
query('search')
.optional()
.trim()
.isLength({ max: 200 }).withMessage('Search query too long')
.matches(/^[a-zA-Z0-9\s-_.,!?'"]+$/).withMessage('Search contains invalid characters')
];
/**
* Validate and sanitize quiz parameters
*/
const sanitizeQuizParams = [
body('categoryId')
.isInt({ min: 1 }).withMessage('Invalid category ID')
.toInt(),
body('questionCount')
.optional()
.isInt({ min: 1, max: 50 }).withMessage('Question count must be between 1 and 50')
.toInt(),
body('difficulty')
.optional()
.trim()
.isIn(['easy', 'medium', 'hard']).withMessage('Invalid difficulty level'),
body('quizType')
.optional()
.trim()
.isIn(['practice', 'timed', 'exam']).withMessage('Invalid quiz type')
];
/**
* Middleware to check validation results
*/
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
const logger = require('../config/logger');
logger.logSecurityEvent('Validation error', req);
throw new BadRequestError('Validation failed', errors.array().map(err => ({
field: err.path || err.param,
message: err.msg
})));
}
next();
};
/**
* Comprehensive sanitization middleware chain
* Use this for all API routes
*/
const sanitizeAll = [
sanitizeMongoData,
sanitizeXSS,
preventHPP,
sanitizeInput
];
/**
* File upload sanitization
*/
const sanitizeFileUpload = (req, res, next) => {
if (req.file) {
// Validate file type
const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!allowedMimeTypes.includes(req.file.mimetype)) {
return res.status(400).json({
status: 'error',
message: 'Invalid file type. Only images are allowed.'
});
}
// Validate file size (5MB max)
if (req.file.size > 5 * 1024 * 1024) {
return res.status(400).json({
status: 'error',
message: 'File too large. Maximum size is 5MB.'
});
}
// Sanitize filename
req.file.originalname = req.file.originalname
.replace(/[^a-zA-Z0-9.-]/g, '_')
.substring(0, 100);
}
next();
};
module.exports = {
// Core sanitization middleware
sanitizeMongoData,
sanitizeXSS,
preventHPP,
sanitizeInput,
sanitizeAll,
// Specific field validators
sanitizeEmail,
sanitizePassword,
sanitizeUsername,
sanitizeId,
sanitizePagination,
sanitizeSearch,
sanitizeQuizParams,
// Validation handler
handleValidationErrors,
// File upload sanitization
sanitizeFileUpload
};

View File

@@ -0,0 +1,155 @@
const helmet = require('helmet');
/**
* Helmet security configuration
* Helmet helps secure Express apps by setting various HTTP headers
*/
const helmetConfig = helmet({
// Content Security Policy
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"], // Allow inline styles for Swagger UI
scriptSrc: ["'self'", "'unsafe-inline'"], // Allow inline scripts for Swagger UI
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'", "data:"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"]
}
},
// Cross-Origin-Embedder-Policy
crossOriginEmbedderPolicy: false, // Disabled for API compatibility
// Cross-Origin-Opener-Policy
crossOriginOpenerPolicy: { policy: "same-origin" },
// Cross-Origin-Resource-Policy
crossOriginResourcePolicy: { policy: "cross-origin" },
// DNS Prefetch Control
dnsPrefetchControl: { allow: false },
// Expect-CT (deprecated but included for older browsers)
expectCt: { maxAge: 86400 },
// Frameguard (prevent clickjacking)
frameguard: { action: "deny" },
// Hide Powered-By header
hidePoweredBy: true,
// HTTP Strict Transport Security
hsts: {
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true
},
// IE No Open
ieNoOpen: true,
// No Sniff (prevent MIME type sniffing)
noSniff: true,
// Origin-Agent-Cluster
originAgentCluster: true,
// Permitted Cross-Domain Policies
permittedCrossDomainPolicies: { permittedPolicies: "none" },
// Referrer Policy
referrerPolicy: { policy: "no-referrer" }
});
/**
* Custom security headers middleware
* Only adds headers not already set by Helmet
*/
const customSecurityHeaders = (req, res, next) => {
// Add Permissions-Policy (not in Helmet)
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
// Prevent caching of sensitive data
if (req.path.includes('/api/auth') || req.path.includes('/api/admin') || req.path.includes('/api/users')) {
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
res.setHeader('Surrogate-Control', 'no-store');
}
next();
};
/**
* CORS configuration
*/
const getCorsOptions = () => {
const allowedOrigins = process.env.ALLOWED_ORIGINS
? process.env.ALLOWED_ORIGINS.split(',')
: ['http://localhost:3000', 'http://localhost:4200', 'http://localhost:5173'];
return {
origin: (origin, callback) => {
// Allow requests with no origin (mobile apps, Postman, etc.)
if (!origin) return callback(null, true);
if (allowedOrigins.indexOf(origin) !== -1 || process.env.NODE_ENV === 'development') {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-guest-token'],
exposedHeaders: ['RateLimit-Limit', 'RateLimit-Remaining', 'RateLimit-Reset'],
maxAge: 86400 // 24 hours
};
};
/**
* Security middleware for API routes
*/
const secureApiRoutes = (req, res, next) => {
// Log security-sensitive operations
if (req.method !== 'GET' && req.path.includes('/api/admin')) {
const logger = require('../config/logger');
logger.logSecurityEvent(`Admin ${req.method} request`, req);
}
next();
};
/**
* Prevent parameter pollution
* This middleware should be used after body parser
*/
const preventParameterPollution = (req, res, next) => {
// Whitelist of parameters that can have multiple values
const whitelist = ['category', 'difficulty', 'tags', 'keywords'];
// Check for duplicate parameters
if (req.query) {
for (const param in req.query) {
if (Array.isArray(req.query[param]) && !whitelist.includes(param)) {
return res.status(400).json({
status: 'error',
message: `Parameter pollution detected: '${param}' should not have multiple values`
});
}
}
}
next();
};
module.exports = {
helmetConfig,
customSecurityHeaders,
getCorsOptions,
secureApiRoutes,
preventParameterPollution
};