392 lines
9.7 KiB
JavaScript
392 lines
9.7 KiB
JavaScript
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,
|
|
};
|