2025-12-19 20:44:46 -06:00
|
|
|
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()
|
|
|
|
|
.trim(),
|
|
|
|
|
body("password")
|
|
|
|
|
.isLength({ min: 8 })
|
|
|
|
|
.withMessage("Password must be at least 8 characters"),
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
// User validators
|
|
|
|
|
createUser: [
|
|
|
|
|
body("email")
|
|
|
|
|
.isEmail()
|
|
|
|
|
.withMessage("Valid email is required")
|
|
|
|
|
.normalizeEmail()
|
|
|
|
|
.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: 8 })
|
|
|
|
|
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
|
|
|
|
|
.withMessage(
|
|
|
|
|
"Password must be at least 8 characters with uppercase, lowercase, and number"
|
|
|
|
|
),
|
|
|
|
|
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()
|
|
|
|
|
.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)")
|
2025-12-24 00:13:23 -06:00
|
|
|
.trim(),
|
|
|
|
|
body("shortdescription")
|
2025-12-19 20:44:46 -06:00
|
|
|
.optional()
|
|
|
|
|
.isString()
|
2025-12-24 00:13:23 -06:00
|
|
|
.isLength({ max: 500 })
|
|
|
|
|
.withMessage("Short description must be under 500 characters")
|
2025-12-19 20:44:46 -06:00
|
|
|
.trim(),
|
2025-12-24 00:13:23 -06:00
|
|
|
body("description")
|
|
|
|
|
.optional()
|
|
|
|
|
.isString()
|
|
|
|
|
.withMessage("Description must be text"),
|
2025-12-19 20:44:46 -06:00
|
|
|
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")
|
2025-12-24 00:13:23 -06:00
|
|
|
.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(),
|
2025-12-19 20:44:46 -06:00
|
|
|
],
|
|
|
|
|
|
|
|
|
|
updateProduct: [
|
2025-12-24 00:13:23 -06:00
|
|
|
param("id").notEmpty().withMessage("Invalid product ID"),
|
2025-12-19 20:44:46 -06:00
|
|
|
body("name")
|
|
|
|
|
.optional()
|
|
|
|
|
.isLength({ min: 1, max: 255 })
|
|
|
|
|
.withMessage("Product name must be 1-255 characters")
|
2025-12-24 00:13:23 -06:00
|
|
|
.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"),
|
2025-12-19 20:44:46 -06:00
|
|
|
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"),
|
2025-12-24 00:13:23 -06:00
|
|
|
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(),
|
2025-12-19 20:44:46 -06:00
|
|
|
],
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
idParam: [param("id").notEmpty().withMessage("ID is required").trim()],
|
|
|
|
|
|
|
|
|
|
// Pagination validators
|
|
|
|
|
pagination: [
|
|
|
|
|
query("page")
|
|
|
|
|
.optional()
|
|
|
|
|
.isInt({ min: 1 })
|
|
|
|
|
.withMessage("Page must be a positive integer"),
|
|
|
|
|
query("limit")
|
|
|
|
|
.optional()
|
|
|
|
|
.isInt({ min: 1, max: 100 })
|
|
|
|
|
.withMessage("Limit must be between 1 and 100"),
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
|
validators,
|
|
|
|
|
handleValidationErrors,
|
|
|
|
|
};
|