const { body, validationResult } = require("express-validator"); const DOMPurify = require("dompurify"); const { JSDOM } = require("jsdom"); // Create a DOM purify instance for server-side sanitization const window = new JSDOM("").window; const purify = DOMPurify(window); // Password strength validation const passwordStrengthRegex = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/; const commonPasswords = [ "password", "123456", "123456789", "qwerty", "abc123", "password123", "admin", "letmein", "welcome", "monkey", "1234567890", ]; // Sanitization middleware const sanitizeInput = (req, res, next) => { const sanitizeValue = (value) => { if (typeof value === "string") { return purify.sanitize(value, { ALLOWED_TAGS: [] }); } if (typeof value === "object" && value !== null) { const sanitized = {}; for (const [key, val] of Object.entries(value)) { sanitized[key] = sanitizeValue(val); } return sanitized; } return value; }; if (req.body) { req.body = sanitizeValue(req.body); } if (req.query) { req.query = sanitizeValue(req.query); } if (req.params) { req.params = sanitizeValue(req.params); } next(); }; // Validation error handler const handleValidationErrors = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ error: "Validation failed", details: errors.array().map((err) => ({ field: err.path, message: err.msg, })), }); } next(); }; // Registration validation rules const validateRegistration = [ body("email") .isEmail() .normalizeEmail() .withMessage("Please provide a valid email address") .isLength({ max: 255 }) .withMessage("Email must be less than 255 characters"), body("password") .isLength({ min: 8, max: 128 }) .withMessage("Password must be between 8 and 128 characters") .matches(passwordStrengthRegex) .withMessage( "Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character" ) .custom((value) => { if (commonPasswords.includes(value.toLowerCase())) { throw new Error( "Password is too common. Please choose a stronger password" ); } return true; }), body("firstName") .trim() .isLength({ min: 1, max: 50 }) .withMessage("First name must be between 1 and 50 characters") .matches(/^[a-zA-Z\s\-']+$/) .withMessage( "First name can only contain letters, spaces, hyphens, and apostrophes" ), body("lastName") .trim() .isLength({ min: 1, max: 50 }) .withMessage("Last name must be between 1 and 50 characters") .matches(/^[a-zA-Z\s\-']+$/) .withMessage( "Last name can only contain letters, spaces, hyphens, and apostrophes" ), body("username") .optional() .trim() .isLength({ min: 3, max: 30 }) .withMessage("Username must be between 3 and 30 characters") .matches(/^[a-zA-Z0-9_-]+$/) .withMessage( "Username can only contain letters, numbers, underscores, and hyphens" ), body("phone") .optional() .isMobilePhone() .withMessage("Please provide a valid phone number"), handleValidationErrors, ]; // Login validation rules const validateLogin = [ body("email") .isEmail() .normalizeEmail() .withMessage("Please provide a valid email address"), body("password") .notEmpty() .withMessage("Password is required") .isLength({ max: 128 }) .withMessage("Password is too long"), handleValidationErrors, ]; // Google auth validation const validateGoogleAuth = [ body("idToken") .notEmpty() .withMessage("Google ID token is required") .isLength({ max: 2048 }) .withMessage("Invalid token format"), handleValidationErrors, ]; // Profile update validation const validateProfileUpdate = [ body("firstName") .optional() .trim() .isLength({ min: 1, max: 50 }) .withMessage("First name must be between 1 and 50 characters") .matches(/^[a-zA-Z\s\-']+$/) .withMessage( "First name can only contain letters, spaces, hyphens, and apostrophes" ), body("lastName") .optional() .trim() .isLength({ min: 1, max: 50 }) .withMessage("Last name must be between 1 and 50 characters") .matches(/^[a-zA-Z\s\-']+$/) .withMessage( "Last name can only contain letters, spaces, hyphens, and apostrophes" ), body("phone") .optional() .isMobilePhone() .withMessage("Please provide a valid phone number"), body("address1") .optional() .trim() .isLength({ max: 255 }) .withMessage("Address line 1 must be less than 255 characters"), body("address2") .optional() .trim() .isLength({ max: 255 }) .withMessage("Address line 2 must be less than 255 characters"), body("city") .optional() .trim() .isLength({ max: 100 }) .withMessage("City must be less than 100 characters") .matches(/^[a-zA-Z\s\-']+$/) .withMessage( "City can only contain letters, spaces, hyphens, and apostrophes" ), body("state") .optional() .trim() .isLength({ max: 100 }) .withMessage("State must be less than 100 characters"), body("zipCode") .optional() .trim() .matches(/^[0-9]{5}(-[0-9]{4})?$/) .withMessage("Please provide a valid ZIP code"), body("country") .optional() .trim() .isLength({ max: 100 }) .withMessage("Country must be less than 100 characters"), handleValidationErrors, ]; // Password change validation const validatePasswordChange = [ body("currentPassword") .notEmpty() .withMessage("Current password is required"), body("newPassword") .isLength({ min: 8, max: 128 }) .withMessage("New password must be between 8 and 128 characters") .matches(passwordStrengthRegex) .withMessage( "New password must contain at least one uppercase letter, one lowercase letter, one number, and one special character" ) .custom((value, { req }) => { if (value === req.body.currentPassword) { throw new Error("New password must be different from current password"); } if (commonPasswords.includes(value.toLowerCase())) { throw new Error( "Password is too common. Please choose a stronger password" ); } return true; }), body("confirmPassword").custom((value, { req }) => { if (value !== req.body.newPassword) { throw new Error("Password confirmation does not match"); } return true; }), handleValidationErrors, ]; module.exports = { sanitizeInput, handleValidationErrors, validateRegistration, validateLogin, validateGoogleAuth, validateProfileUpdate, validatePasswordChange, };