Initial commit: StreamFlow IPTV platform
This commit is contained in:
commit
73a8ae9ffd
1240 changed files with 278451 additions and 0 deletions
360
backend/middleware/inputValidation.js
Normal file
360
backend/middleware/inputValidation.js
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
/**
|
||||
* 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
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue