const { body, query, 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 does not meet requirements" ) .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("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("code") .notEmpty() .withMessage("Authorization code is required") .isLength({ max: 512 }) .withMessage("Invalid authorization code 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, ]; // Forgot password validation const validateForgotPassword = [ body("email") .isEmail() .normalizeEmail() .withMessage("Please provide a valid email address") .isLength({ max: 255 }) .withMessage("Email must be less than 255 characters"), handleValidationErrors, ]; // Reset password validation const validateResetPassword = [ body("token") .notEmpty() .withMessage("Reset token is required") .isLength({ min: 64, max: 64 }) .withMessage("Invalid reset token format"), body("newPassword") .isLength({ min: 8, max: 128 }) .withMessage("Password must be between 8 and 128 characters") .matches(passwordStrengthRegex) .withMessage( "Password does not meet requirements" ) .custom((value) => { if (commonPasswords.includes(value.toLowerCase())) { throw new Error( "Password is too common. Please choose a stronger password" ); } return true; }), handleValidationErrors, ]; // Verify reset token validation const validateVerifyResetToken = [ body("token") .notEmpty() .withMessage("Reset token is required") .isLength({ min: 64, max: 64 }) .withMessage("Invalid reset token format"), handleValidationErrors, ]; // Feedback validation const validateFeedback = [ body("feedbackText") .trim() .isLength({ min: 5, max: 5000 }) .withMessage("Feedback must be between 5 and 5000 characters"), body("url") .optional() .trim() .isLength({ max: 500 }) .withMessage("URL must be less than 500 characters"), handleValidationErrors, ]; // Coordinate validation for query parameters (e.g., location search) const validateCoordinatesQuery = [ query("lat") .optional() .isFloat({ min: -90, max: 90 }) .withMessage("Latitude must be between -90 and 90"), query("lng") .optional() .isFloat({ min: -180, max: 180 }) .withMessage("Longitude must be between -180 and 180"), query("radius") .optional() .isFloat({ min: 0.1, max: 100 }) .withMessage("Radius must be between 0.1 and 100 miles"), handleValidationErrors, ]; // Coordinate validation for body parameters (e.g., user addresses, forum posts) const validateCoordinatesBody = [ body("latitude") .optional() .isFloat({ min: -90, max: 90 }) .withMessage("Latitude must be between -90 and 90"), body("longitude") .optional() .isFloat({ min: -180, max: 180 }) .withMessage("Longitude must be between -180 and 180"), ]; // Two-Factor Authentication validation const validateTotpCode = [ body("code") .trim() .matches(/^\d{6}$/) .withMessage("TOTP code must be exactly 6 digits"), handleValidationErrors, ]; const validateEmailOtp = [ body("code") .trim() .matches(/^\d{6}$/) .withMessage("Email OTP must be exactly 6 digits"), handleValidationErrors, ]; const validateRecoveryCode = [ body("code") .trim() .matches(/^[A-Za-z0-9]{4}-[A-Za-z0-9]{4}$/i) .withMessage("Recovery code must be in format XXXX-XXXX"), handleValidationErrors, ]; module.exports = { sanitizeInput, handleValidationErrors, validateRegistration, validateLogin, validateGoogleAuth, validateProfileUpdate, validatePasswordChange, validateForgotPassword, validateResetPassword, validateVerifyResetToken, validateFeedback, validateCoordinatesQuery, validateCoordinatesBody, // Two-Factor Authentication validateTotpCode, validateEmailOtp, validateRecoveryCode, };