246 lines
6.0 KiB
JavaScript
246 lines
6.0 KiB
JavaScript
/**
|
|
* Input Validation Utilities
|
|
* Reusable validation functions with consistent error messages
|
|
*/
|
|
|
|
const { AppError } = require("../middleware/errorHandler");
|
|
const { HTTP_STATUS } = require("../config/constants");
|
|
|
|
/**
|
|
* Validate required fields
|
|
* @param {Object} data - Data object to validate
|
|
* @param {string[]} requiredFields - Array of required field names
|
|
* @throws {AppError} If validation fails
|
|
*/
|
|
const validateRequiredFields = (data, requiredFields) => {
|
|
const missingFields = requiredFields.filter(
|
|
(field) =>
|
|
!data[field] ||
|
|
(typeof data[field] === "string" && data[field].trim() === ""),
|
|
);
|
|
|
|
if (missingFields.length > 0) {
|
|
throw new AppError(
|
|
`Missing required fields: ${missingFields.join(", ")}`,
|
|
HTTP_STATUS.BAD_REQUEST,
|
|
);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Validate email format
|
|
* @param {string} email - Email to validate
|
|
* @returns {boolean} True if valid
|
|
*/
|
|
const isValidEmail = (email) => {
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
return emailRegex.test(email);
|
|
};
|
|
|
|
/**
|
|
* Validate email field
|
|
* @param {string} email - Email to validate
|
|
* @throws {AppError} If validation fails
|
|
*/
|
|
const validateEmail = (email) => {
|
|
if (!email || !isValidEmail(email)) {
|
|
throw new AppError("Invalid email format", HTTP_STATUS.BAD_REQUEST);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Validate UUID format
|
|
* @param {string} id - UUID to validate
|
|
* @returns {boolean} True if valid UUID
|
|
*/
|
|
const isValidUUID = (id) => {
|
|
const uuidRegex =
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
return uuidRegex.test(id);
|
|
};
|
|
|
|
/**
|
|
* Validate number range
|
|
* @param {number} value - Value to validate
|
|
* @param {number} min - Minimum value
|
|
* @param {number} max - Maximum value
|
|
* @param {string} fieldName - Field name for error message
|
|
* @throws {AppError} If validation fails
|
|
*/
|
|
const validateNumberRange = (value, min, max, fieldName = "Value") => {
|
|
const num = parseFloat(value);
|
|
if (isNaN(num) || num < min || num > max) {
|
|
throw new AppError(
|
|
`${fieldName} must be between ${min} and ${max}`,
|
|
HTTP_STATUS.BAD_REQUEST,
|
|
);
|
|
}
|
|
return num;
|
|
};
|
|
|
|
/**
|
|
* Validate string length
|
|
* @param {string} value - String to validate
|
|
* @param {number} min - Minimum length
|
|
* @param {number} max - Maximum length
|
|
* @param {string} fieldName - Field name for error message
|
|
* @throws {AppError} If validation fails
|
|
*/
|
|
const validateStringLength = (value, min, max, fieldName = "Field") => {
|
|
if (!value || value.length < min || value.length > max) {
|
|
throw new AppError(
|
|
`${fieldName} must be between ${min} and ${max} characters`,
|
|
HTTP_STATUS.BAD_REQUEST,
|
|
);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Sanitize string input (remove HTML tags, trim)
|
|
* @param {string} input - String to sanitize
|
|
* @returns {string} Sanitized string
|
|
*/
|
|
const sanitizeString = (input) => {
|
|
if (typeof input !== "string") return "";
|
|
return input
|
|
.replace(/<[^>]*>/g, "") // Remove HTML tags
|
|
.trim();
|
|
};
|
|
|
|
/**
|
|
* Validate and sanitize slug
|
|
* @param {string} slug - Slug to validate
|
|
* @returns {string} Valid slug
|
|
* @throws {AppError} If validation fails
|
|
*/
|
|
const validateSlug = (slug) => {
|
|
const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
const sanitized = slug.toLowerCase().trim();
|
|
|
|
if (!slugRegex.test(sanitized)) {
|
|
throw new AppError(
|
|
"Slug can only contain lowercase letters, numbers, and hyphens",
|
|
HTTP_STATUS.BAD_REQUEST,
|
|
);
|
|
}
|
|
|
|
return sanitized;
|
|
};
|
|
|
|
/**
|
|
* Generate slug from string
|
|
* @param {string} text - Text to convert to slug
|
|
* @returns {string} Generated slug
|
|
*/
|
|
const generateSlug = (text) => {
|
|
return text
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9\s-]/g, "")
|
|
.replace(/\s+/g, "-")
|
|
.replace(/-+/g, "-")
|
|
.trim();
|
|
};
|
|
|
|
/**
|
|
* Validate pagination parameters
|
|
* @param {Object} query - Query parameters
|
|
* @returns {Object} Validated pagination params
|
|
*/
|
|
const validatePagination = (query) => {
|
|
const page = Math.max(1, parseInt(query.page) || 1);
|
|
const limit = Math.min(100, Math.max(1, parseInt(query.limit) || 20));
|
|
|
|
return { page, limit };
|
|
};
|
|
|
|
/**
|
|
* Validate image file
|
|
* @param {Object} file - Multer file object
|
|
* @throws {AppError} If validation fails
|
|
*/
|
|
const validateImageFile = (file) => {
|
|
const allowedMimeTypes = [
|
|
"image/jpeg",
|
|
"image/jpg",
|
|
"image/png",
|
|
"image/gif",
|
|
"image/webp",
|
|
"image/bmp",
|
|
"image/tiff",
|
|
"image/svg+xml",
|
|
"image/x-icon",
|
|
"image/vnd.microsoft.icon",
|
|
"image/ico",
|
|
"image/avif",
|
|
"image/heic",
|
|
"image/heif",
|
|
];
|
|
const maxSize = 10 * 1024 * 1024; // 10MB for larger image formats
|
|
|
|
if (!file) {
|
|
throw new AppError("No file provided", HTTP_STATUS.BAD_REQUEST);
|
|
}
|
|
|
|
if (!allowedMimeTypes.includes(file.mimetype)) {
|
|
throw new AppError(
|
|
"Invalid file type. Allowed: JPEG, PNG, GIF, WebP, BMP, TIFF, SVG, ICO, AVIF, HEIC",
|
|
HTTP_STATUS.BAD_REQUEST,
|
|
);
|
|
}
|
|
|
|
if (file.size > maxSize) {
|
|
throw new AppError(
|
|
"File too large. Maximum size is 5MB",
|
|
HTTP_STATUS.BAD_REQUEST,
|
|
);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Validate price value
|
|
* @param {number} price - Price to validate
|
|
* @param {string} fieldName - Field name for error message
|
|
* @returns {number} Validated price
|
|
* @throws {AppError} If validation fails
|
|
*/
|
|
const validatePrice = (price, fieldName = "Price") => {
|
|
return validateNumberRange(price, 0, 999999, fieldName);
|
|
};
|
|
|
|
/**
|
|
* Validate stock quantity
|
|
* @param {number} stock - Stock to validate
|
|
* @returns {number} Validated stock
|
|
* @throws {AppError} If validation fails
|
|
*/
|
|
const validateStock = (stock) => {
|
|
return validateNumberRange(stock, 0, 999999, "Stock quantity");
|
|
};
|
|
|
|
/**
|
|
* Validate color code (hex format)
|
|
* @param {string} colorCode - Color code to validate
|
|
* @returns {boolean} True if valid
|
|
*/
|
|
const isValidColorCode = (colorCode) => {
|
|
const hexRegex = /^#[0-9A-F]{6}$/i;
|
|
return hexRegex.test(colorCode);
|
|
};
|
|
|
|
module.exports = {
|
|
validateRequiredFields,
|
|
validateEmail,
|
|
isValidEmail,
|
|
isValidUUID,
|
|
validateNumberRange,
|
|
validateStringLength,
|
|
sanitizeString,
|
|
validateSlug,
|
|
generateSlug,
|
|
validatePagination,
|
|
validateImageFile,
|
|
validatePrice,
|
|
validateStock,
|
|
isValidColorCode,
|
|
};
|