Files
SkyArtShop/backend/middleware/validators.js

322 lines
8.6 KiB
JavaScript
Raw Normal View History

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")
2026-01-18 02:22:05 -06:00
.normalizeEmail({ gmail_remove_dots: false })
2025-12-19 20:44:46 -06:00
.trim(),
2026-01-04 17:52:37 -06:00
body("password").notEmpty().withMessage("Password is required").trim(),
2025-12-19 20:44:46 -06:00
],
// User validators
createUser: [
body("email")
.isEmail()
.withMessage("Valid email is required")
2026-01-18 02:22:05 -06:00
.normalizeEmail({ gmail_remove_dots: false })
2025-12-19 20:44:46 -06:00
.trim(),
body("username")
.isLength({ min: 3, max: 50 })
.matches(/^[a-zA-Z0-9_-]+$/)
.withMessage(
2026-01-18 02:22:05 -06:00
"Username must be 3-50 characters and contain only letters, numbers, hyphens, and underscores",
2025-12-19 20:44:46 -06:00
)
.trim(),
body("password")
2026-01-04 17:52:37 -06:00
.isLength({ min: 12 })
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#])/)
2025-12-19 20:44:46 -06:00
.withMessage(
2026-01-18 02:22:05 -06:00
"Password must be at least 12 characters with uppercase, lowercase, number, and special character",
2025-12-19 20:44:46 -06:00
),
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")
2026-01-18 02:22:05 -06:00
.normalizeEmail({ gmail_remove_dots: false })
2025-12-19 20:44:46 -06:00
.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(
2026-01-18 02:22:05 -06:00
"Slug must contain only lowercase letters, numbers, and hyphens",
2025-12-19 20:44:46 -06:00
)
.trim(),
body("content").notEmpty().withMessage("Content is required").trim(),
],
2026-01-18 02:22:05 -06:00
// 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"),
],
2025-12-19 20:44:46 -06:00
// Pagination validators
pagination: [
query("page")
.optional()
.isInt({ min: 1 })
2026-01-18 02:22:05 -06:00
.withMessage("Page must be a positive integer")
.toInt(),
2025-12-19 20:44:46 -06:00
query("limit")
.optional()
.isInt({ min: 1, max: 100 })
2026-01-18 02:22:05 -06:00
.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(),
2025-12-19 20:44:46 -06:00
],
};
module.exports = {
validators,
handleValidationErrors,
};