/** * Validation Middleware * Provides reusable validation middleware for common request patterns */ const { validateUsername, validateEmail, validateUrl, validatePlaylistName, validateChannelName, validateDescription, validateFilename, validateSettingKey, validateInteger, validateBoolean, validateJSON, sanitizeObject } = require('../utils/inputValidator'); const logger = require('../utils/logger'); /** * Generic validation middleware factory */ function createValidationMiddleware(validators) { return (req, res, next) => { const errors = {}; const sanitized = {}; let hasErrors = false; // Validate body parameters if (validators.body) { for (const [field, validator] of Object.entries(validators.body)) { const value = req.body[field]; const result = validator(value); if (!result.valid) { errors[field] = result.errors; hasErrors = true; } else if (result.sanitized !== undefined) { sanitized[field] = result.sanitized; } } } // Validate query parameters if (validators.query) { for (const [field, validator] of Object.entries(validators.query)) { const value = req.query[field]; const result = validator(value); if (!result.valid) { errors[`query.${field}`] = result.errors; hasErrors = true; } else if (result.sanitized !== undefined) { if (!req.sanitizedQuery) req.sanitizedQuery = {}; req.sanitizedQuery[field] = result.sanitized; } } } // Validate params if (validators.params) { for (const [field, validator] of Object.entries(validators.params)) { const value = req.params[field]; const result = validator(value); if (!result.valid) { errors[`params.${field}`] = result.errors; hasErrors = true; } else if (result.sanitized !== undefined) { if (!req.sanitizedParams) req.sanitizedParams = {}; req.sanitizedParams[field] = result.sanitized; } } } if (hasErrors) { logger.warn('Validation failed:', { errors, path: req.path, ip: req.ip }); return res.status(400).json({ error: 'Validation failed', details: errors }); } // Replace request data with sanitized versions if (Object.keys(sanitized).length > 0) { req.body = { ...req.body, ...sanitized }; } next(); }; } /** * Validate playlist creation/update */ const validatePlaylist = createValidationMiddleware({ body: { name: validatePlaylistName, url: (value) => validateUrl(value, false), category: (value) => { if (!value) return { valid: true, errors: [], sanitized: null }; return validateDescription(value); }, type: (value) => { if (!value) return { valid: true, errors: [], sanitized: 'live' }; const allowed = ['live', 'vod', 'series', 'radio']; if (!allowed.includes(value)) { return { valid: false, errors: ['Invalid playlist type'], sanitized: null }; } return { valid: true, errors: [], sanitized: value }; } } }); /** * Validate channel update */ const validateChannelUpdate = createValidationMiddleware({ body: { name: (value) => { if (!value) return { valid: true, errors: [], sanitized: undefined }; return validateChannelName(value); }, group_name: (value) => { if (!value) return { valid: true, errors: [], sanitized: undefined }; return validateDescription(value); } } }); /** * Validate settings */ const validateSettings = createValidationMiddleware({ params: { key: validateSettingKey }, body: { value: (value) => { // Settings can be strings, numbers, booleans, or JSON objects if (value === undefined || value === null) { return { valid: false, errors: ['Value is required'], sanitized: null }; } // If it's an object, validate as JSON if (typeof value === 'object') { return validateJSON(value, 100000); } return { valid: true, errors: [], sanitized: value }; } } }); /** * Validate ID parameter */ const validateIdParam = createValidationMiddleware({ params: { id: (value) => validateInteger(value, 1, Number.MAX_SAFE_INTEGER) } }); /** * Validate channelId parameter */ const validateChannelIdParam = createValidationMiddleware({ params: { channelId: (value) => validateInteger(value, 1, Number.MAX_SAFE_INTEGER) } }); /** * Validate pagination parameters */ const validatePagination = createValidationMiddleware({ query: { limit: (value) => { if (!value) return { valid: true, errors: [], sanitized: 100 }; return validateInteger(value, 1, 1000); }, offset: (value) => { if (!value) return { valid: true, errors: [], sanitized: 0 }; return validateInteger(value, 0, Number.MAX_SAFE_INTEGER); } } }); /** * Validate search query */ const validateSearch = createValidationMiddleware({ query: { search: (value) => { if (!value) return { valid: true, errors: [], sanitized: undefined }; return validateDescription(value); }, q: (value) => { if (!value) return { valid: true, errors: [], sanitized: undefined }; return validateDescription(value); } } }); /** * Validate user creation */ const validateUserCreation = createValidationMiddleware({ body: { username: validateUsername, email: validateEmail, password: (value) => { if (!value || typeof value !== 'string') { return { valid: false, errors: ['Password is required'], sanitized: null }; } if (value.length < 8) { return { valid: false, errors: ['Password must be at least 8 characters'], sanitized: null }; } if (value.length > 128) { return { valid: false, errors: ['Password must not exceed 128 characters'], sanitized: null }; } return { valid: true, errors: [], sanitized: value }; }, role: (value) => { const allowed = ['user', 'admin']; if (!allowed.includes(value)) { return { valid: false, errors: ['Invalid role'], sanitized: null }; } return { valid: true, errors: [], sanitized: value }; } } }); /** * Validate user update */ const validateUserUpdate = createValidationMiddleware({ params: { id: (value) => validateInteger(value, 1, Number.MAX_SAFE_INTEGER) }, body: { username: (value) => { if (!value) return { valid: true, errors: [], sanitized: undefined }; return validateUsername(value); }, email: (value) => { if (!value) return { valid: true, errors: [], sanitized: undefined }; return validateEmail(value); }, role: (value) => { if (!value) return { valid: true, errors: [], sanitized: undefined }; const allowed = ['user', 'admin']; if (!allowed.includes(value)) { return { valid: false, errors: ['Invalid role'], sanitized: null }; } return { valid: true, errors: [], sanitized: value }; }, is_active: (value) => { if (value === undefined) return { valid: true, errors: [], sanitized: undefined }; return validateBoolean(value); } } }); /** * Validate filename for file operations */ const validateFileOperation = createValidationMiddleware({ params: { filename: validateFilename } }); /** * Validate bulk delete operations */ const validateBulkDelete = createValidationMiddleware({ body: { ids: (value) => { if (!Array.isArray(value)) { return { valid: false, errors: ['IDs must be an array'], sanitized: null }; } if (value.length === 0) { return { valid: false, errors: ['At least one ID required'], sanitized: null }; } if (value.length > 1000) { return { valid: false, errors: ['Cannot delete more than 1000 items at once'], sanitized: null }; } const errors = []; const sanitized = []; for (let i = 0; i < value.length; i++) { const result = validateInteger(value[i], 1, Number.MAX_SAFE_INTEGER); if (!result.valid) { errors.push(`Invalid ID at index ${i}`); } else { sanitized.push(result.sanitized); } } if (errors.length > 0) { return { valid: false, errors, sanitized: null }; } return { valid: true, errors: [], sanitized }; } } }); /** * Generic text field validator */ function validateTextField(maxLength = 1000, required = true) { return (value) => { if (!value || value === '') { if (required) { return { valid: false, errors: ['This field is required'], sanitized: null }; } return { valid: true, errors: [], sanitized: '' }; } if (typeof value !== 'string') { return { valid: false, errors: ['Must be a string'], sanitized: null }; } const trimmed = value.trim(); if (required && trimmed.length === 0) { return { valid: false, errors: ['This field is required'], sanitized: null }; } if (trimmed.length > maxLength) { return { valid: false, errors: [`Must not exceed ${maxLength} characters`], sanitized: null }; } const result = validateDescription(trimmed); return result; }; } module.exports = { createValidationMiddleware, validatePlaylist, validateChannelUpdate, validateSettings, validateIdParam, validateChannelIdParam, validatePagination, validateSearch, validateUserCreation, validateUserUpdate, validateFileOperation, validateBulkDelete, validateTextField };