- Fixed unused function parameters by prefixing with underscore - Removed unused imports and variables - Applied eslint --fix for automatic style fixes - Property shorthand - String template literals - Prefer const over let where appropriate - Spacing and formatting Reduces lint errors from 108+ to 78 (61 unused vars, 17 other issues) Related to CI lint failures in previous commit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
211 lines
4.6 KiB
JavaScript
211 lines
4.6 KiB
JavaScript
/**
|
|
* Validation Middleware
|
|
* Input validation and sanitization
|
|
*/
|
|
|
|
const validator = require('validator');
|
|
const sanitizeHtml = require('sanitize-html');
|
|
|
|
/**
|
|
* Validate email
|
|
* Supports nested fields: validateEmail('contact.email')
|
|
*/
|
|
function validateEmail(fieldPath = 'email') {
|
|
return (req, res, next) => {
|
|
// Get value from nested path (e.g., 'contact.email')
|
|
const getValue = (obj, path) => {
|
|
return path.split('.').reduce((current, key) => current?.[key], obj);
|
|
};
|
|
|
|
// Set value at nested path
|
|
const setValue = (obj, path, value) => {
|
|
const keys = path.split('.');
|
|
const lastKey = keys.pop();
|
|
const target = keys.reduce((current, key) => {
|
|
if (!current[key]) current[key] = {};
|
|
return current[key];
|
|
}, obj);
|
|
target[lastKey] = value;
|
|
};
|
|
|
|
const email = getValue(req.body, fieldPath);
|
|
|
|
if (!email || !validator.isEmail(email)) {
|
|
return res.status(400).json({
|
|
error: 'Validation failed',
|
|
message: `Valid email address is required for ${fieldPath}`
|
|
});
|
|
}
|
|
|
|
// Normalize email
|
|
const normalized = validator.normalizeEmail(email);
|
|
setValue(req.body, fieldPath, normalized);
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validate required fields
|
|
* Supports nested fields: validateRequired(['contact.name', 'contact.email'])
|
|
*/
|
|
function validateRequired(fields) {
|
|
return (req, res, next) => {
|
|
const missing = [];
|
|
|
|
// Get value from nested path (e.g., 'contact.email')
|
|
const getValue = (obj, path) => {
|
|
return path.split('.').reduce((current, key) => current?.[key], obj);
|
|
};
|
|
|
|
for (const field of fields) {
|
|
const value = getValue(req.body, field);
|
|
|
|
if (value === undefined || value === null ||
|
|
(typeof value === 'string' && value.trim() === '')) {
|
|
missing.push(field);
|
|
}
|
|
}
|
|
|
|
if (missing.length > 0) {
|
|
return res.status(400).json({
|
|
error: 'Validation failed',
|
|
message: 'Required fields missing',
|
|
missing
|
|
});
|
|
}
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Sanitize string inputs
|
|
*/
|
|
function sanitizeInputs(req, res, next) {
|
|
const sanitizeString = str => {
|
|
if (typeof str !== 'string') return str;
|
|
return validator.escape(str.trim());
|
|
};
|
|
|
|
const sanitizeObject = obj => {
|
|
const sanitized = {};
|
|
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
if (typeof value === 'string') {
|
|
sanitized[key] = sanitizeString(value);
|
|
} else if (typeof value === 'object' && value !== null) {
|
|
sanitized[key] = sanitizeObject(value);
|
|
} else {
|
|
sanitized[key] = value;
|
|
}
|
|
}
|
|
|
|
return sanitized;
|
|
};
|
|
|
|
req.body = sanitizeObject(req.body);
|
|
|
|
next();
|
|
}
|
|
|
|
/**
|
|
* Validate MongoDB ObjectId
|
|
*/
|
|
function validateObjectId(paramName = 'id') {
|
|
return (req, res, next) => {
|
|
const id = req.params[paramName];
|
|
|
|
if (!id || !validator.isMongoId(id)) {
|
|
return res.status(400).json({
|
|
error: 'Validation failed',
|
|
message: 'Invalid ID format'
|
|
});
|
|
}
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validate slug format
|
|
*/
|
|
function validateSlug(req, res, next) {
|
|
const { slug } = req.body;
|
|
|
|
if (!slug) {
|
|
return next();
|
|
}
|
|
|
|
const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
|
|
if (!slugRegex.test(slug)) {
|
|
return res.status(400).json({
|
|
error: 'Validation failed',
|
|
message: 'Slug must be lowercase letters, numbers, and hyphens only'
|
|
});
|
|
}
|
|
|
|
next();
|
|
}
|
|
|
|
/**
|
|
* Validate URL
|
|
*/
|
|
function validateUrl(fieldName = 'url') {
|
|
return (req, res, next) => {
|
|
const url = req.body[fieldName];
|
|
|
|
if (!url || !validator.isURL(url, { require_protocol: true })) {
|
|
return res.status(400).json({
|
|
error: 'Validation failed',
|
|
message: `Valid URL required for ${fieldName}`
|
|
});
|
|
}
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Sanitize HTML content
|
|
*/
|
|
function sanitizeContent(fieldName = 'content') {
|
|
return (req, res, next) => {
|
|
const content = req.body[fieldName];
|
|
|
|
if (!content) {
|
|
return next();
|
|
}
|
|
|
|
req.body[fieldName] = sanitizeHtml(content, {
|
|
allowedTags: [
|
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
'p', 'br', 'hr',
|
|
'strong', 'em', 'u', 'code', 'pre',
|
|
'a', 'img',
|
|
'ul', 'ol', 'li',
|
|
'blockquote',
|
|
'table', 'thead', 'tbody', 'tr', 'th', 'td'
|
|
],
|
|
allowedAttributes: {
|
|
'a': ['href', 'title', 'target', 'rel'],
|
|
'img': ['src', 'alt', 'title'],
|
|
'code': ['class'],
|
|
'pre': ['class']
|
|
}
|
|
});
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
validateEmail,
|
|
validateRequired,
|
|
sanitizeInputs,
|
|
validateObjectId,
|
|
validateSlug,
|
|
validateUrl,
|
|
sanitizeContent
|
|
};
|