272 lines
6.8 KiB
JavaScript
272 lines
6.8 KiB
JavaScript
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("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,
|
|
];
|
|
|
|
module.exports = {
|
|
sanitizeInput,
|
|
handleValidationErrors,
|
|
validateRegistration,
|
|
validateLogin,
|
|
validateGoogleAuth,
|
|
validateProfileUpdate,
|
|
validatePasswordChange,
|
|
};
|