const { body, param, query, validationResult } = require("express-validator"); const logger = require("../config/logger"); // Validation error handler middleware const handleValidationErrors = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { logger.warn("Validation error", { path: req.path, errors: errors.array(), body: req.body, }); return res.status(400).json({ success: false, message: "Validation failed", errors: errors.array().map((err) => ({ field: err.param, message: err.msg, })), }); } next(); }; // Common validation rules const validators = { // Auth validators login: [ body("email") .isEmail() .withMessage("Valid email is required") .normalizeEmail({ gmail_remove_dots: false }) .trim(), body("password").notEmpty().withMessage("Password is required").trim(), ], // User validators createUser: [ body("email") .isEmail() .withMessage("Valid email is required") .normalizeEmail({ gmail_remove_dots: false }) .trim(), body("username") .isLength({ min: 3, max: 50 }) .matches(/^[a-zA-Z0-9_-]+$/) .withMessage( "Username must be 3-50 characters and contain only letters, numbers, hyphens, and underscores", ) .trim(), body("password") .isLength({ min: 12 }) .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#])/) .withMessage( "Password must be at least 12 characters with uppercase, lowercase, number, and special character", ), body("role_id").notEmpty().withMessage("Role is required").trim(), ], updateUser: [ param("id") .matches(/^user-[a-f0-9-]+$/) .withMessage("Invalid user ID format"), body("email") .optional() .isEmail() .withMessage("Valid email is required") .normalizeEmail({ gmail_remove_dots: false }) .trim(), body("username") .optional() .isLength({ min: 3, max: 50 }) .withMessage("Username must be 3-50 characters") .matches(/^[a-zA-Z0-9_-]+$/) .trim(), ], // Product validators createProduct: [ body("name") .isLength({ min: 1, max: 255 }) .withMessage("Product name is required (max 255 characters)") .trim(), body("shortdescription") .optional() .isString() .isLength({ max: 500 }) .withMessage("Short description must be under 500 characters") .trim(), body("description") .optional() .isString() .withMessage("Description must be text"), body("price") .isFloat({ min: 0 }) .withMessage("Price must be a positive number"), body("stockquantity") .optional() .isInt({ min: 0 }) .withMessage("Stock quantity must be a non-negative integer"), body("category") .optional() .isString() .withMessage("Category must be text") .trim(), body("sku") .optional() .isString() .isLength({ max: 100 }) .withMessage("SKU must be under 100 characters") .trim(), body("weight") .optional() .isFloat({ min: 0 }) .withMessage("Weight must be a positive number"), body("dimensions") .optional() .isString() .isLength({ max: 100 }) .withMessage("Dimensions must be under 100 characters") .trim(), body("material") .optional() .isString() .isLength({ max: 255 }) .withMessage("Material must be under 255 characters") .trim(), body("isactive") .optional() .isBoolean() .withMessage("Active status must be true or false"), body("isfeatured") .optional() .isBoolean() .withMessage("Featured status must be true or false"), body("isbestseller") .optional() .isBoolean() .withMessage("Bestseller status must be true or false"), body("images").optional().isArray().withMessage("Images must be an array"), body("images.*.color_variant") .optional() .isString() .isLength({ max: 100 }) .withMessage("Color variant must be under 100 characters") .trim(), body("images.*.image_url") .optional() .isString() .isLength({ max: 500 }) .withMessage("Image URL must be under 500 characters"), body("images.*.alt_text") .optional() .isString() .isLength({ max: 255 }) .withMessage("Alt text must be under 255 characters") .trim(), ], updateProduct: [ param("id").notEmpty().withMessage("Invalid product ID"), body("name") .optional() .isLength({ min: 1, max: 255 }) .withMessage("Product name must be 1-255 characters") .trim(), body("shortdescription") .optional() .isString() .isLength({ max: 500 }) .withMessage("Short description must be under 500 characters") .trim(), body("description") .optional() .isString() .withMessage("Description must be text"), body("price") .optional() .isFloat({ min: 0 }) .withMessage("Price must be a positive number"), body("stockquantity") .optional() .isInt({ min: 0 }) .withMessage("Stock quantity must be a non-negative integer"), body("category") .optional() .isString() .withMessage("Category must be text") .trim(), body("sku") .optional() .isString() .isLength({ max: 100 }) .withMessage("SKU must be under 100 characters") .trim(), body("weight") .optional() .isFloat({ min: 0 }) .withMessage("Weight must be a positive number"), body("dimensions") .optional() .isString() .isLength({ max: 100 }) .withMessage("Dimensions must be under 100 characters") .trim(), body("material") .optional() .isString() .isLength({ max: 255 }) .withMessage("Material must be under 255 characters") .trim(), body("isactive") .optional() .isBoolean() .withMessage("Active status must be true or false"), body("isfeatured") .optional() .isBoolean() .withMessage("Featured status must be true or false"), body("isbestseller") .optional() .isBoolean() .withMessage("Bestseller status must be true or false"), body("images").optional().isArray().withMessage("Images must be an array"), body("images.*.color_variant") .optional() .isString() .isLength({ max: 100 }) .withMessage("Color variant must be under 100 characters") .trim(), body("images.*.image_url") .optional() .isString() .isLength({ max: 500 }) .withMessage("Image URL must be under 500 characters"), body("images.*.alt_text") .optional() .isString() .isLength({ max: 255 }) .withMessage("Alt text must be under 255 characters") .trim(), ], // Blog validators createBlogPost: [ body("title") .isLength({ min: 1, max: 255 }) .withMessage("Title is required (max 255 characters)") .trim() .escape(), body("slug") .isLength({ min: 1, max: 255 }) .matches(/^[a-z0-9-]+$/) .withMessage( "Slug must contain only lowercase letters, numbers, and hyphens", ) .trim(), body("content").notEmpty().withMessage("Content is required").trim(), ], // Generic ID validator - SECURITY: Validate ID format to prevent injection idParam: [ param("id") .notEmpty() .withMessage("ID is required") .trim() .matches(/^[a-zA-Z0-9_-]+$/) .withMessage("Invalid ID format") .isLength({ max: 100 }) .withMessage("ID too long"), ], // Product ID validator productIdParam: [ param("id") .notEmpty() .withMessage("Product ID is required") .trim() .matches(/^prod-[a-zA-Z0-9-]+$/) .withMessage("Invalid product ID format"), ], // User ID validator userIdParam: [ param("id") .notEmpty() .withMessage("User ID is required") .trim() .matches(/^user-[a-f0-9-]+$/) .withMessage("Invalid user ID format"), ], // Pagination validators pagination: [ query("page") .optional() .isInt({ min: 1 }) .withMessage("Page must be a positive integer") .toInt(), query("limit") .optional() .isInt({ min: 1, max: 100 }) .withMessage("Limit must be between 1 and 100") .toInt(), ], // SECURITY: Sanitize search queries searchQuery: [ query("q") .optional() .trim() .isLength({ max: 200 }) .withMessage("Search query too long") .escape(), ], }; module.exports = { validators, handleValidationErrors, };