/
varsl
/
HTML-CSS
Обзор
Документация
Войти
/
varsl
/
HTML-CSS
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
backend/utils/validation.js
423 строки
15 KB
varsl
Добавление файла для валидации данных
21 дек 2025, 23:55
21 дек 2025, 23:55
4982066
Код
Авторство
О чём код?
const { body, param, query, validationResult } = require('express-validator'); // Валидация регистрации const validateRegistration = [ body('email') .isEmail() .normalizeEmail() .withMessage('Введите корректный email адрес'), body('password') .isLength({ min: 6 }) .withMessage('Пароль должен содержать не менее 6 символов') .matches(/[A-Z]/) .withMessage('Пароль должен содержать хотя бы одну заглавную букву') .matches(/[a-z]/) .withMessage('Пароль должен содержать хотя бы одну строчную букву') .matches(/\d/) .withMessage('Пароль должен содержать хотя бы одну цифру'), body('name') .trim() .notEmpty() .withMessage('Имя обязательно для заполнения') .isLength({ min: 2, max: 50 }) .withMessage('Имя должно содержать от 2 до 50 символов') .matches(/^[a-zA-Zа-яА-ЯёЁ\s\-]+$/) .withMessage('Имя может содержать только буквы, пробелы и дефисы'), body('avatar') .optional() .isURL() .withMessage('Аватар должен быть валидным URL') ]; // Валидация входа const validateLogin = [ body('email') .isEmail() .normalizeEmail() .withMessage('Введите корректный email адрес'), body('password') .notEmpty() .withMessage('Пароль обязателен для заполнения') ]; // Валидация отзыва const validateReview = [ body('animalId') .notEmpty() .withMessage('ID животного обязательно'), body('rating') .isInt({ min: 1, max: 5 }) .withMessage('Рейтинг должен быть от 1 до 5'), body('comment') .optional() .trim() .isLength({ max: 1000 }) .withMessage('Комментарий не должен превышать 1000 символов') ]; // Валидация билета const validateTicket = [ body('tickets') .isObject() .withMessage('Данные билетов должны быть объектом') .custom((tickets) => { const validTypes = ['adult', 'child', 'student', 'senior', 'family', 'group']; for (const type in tickets) { if (!validTypes.includes(type)) { throw new Error(`Неверный тип билета: ${type}`); } if (!Number.isInteger(tickets[type]) || tickets[type] < 0) { throw new Error(`Количество билетов "${type}" должно быть целым неотрицательным числом`); } } return true; }), body('date') .isISO8601() .withMessage('Неверный формат даты (используйте YYYY-MM-DD)') .custom((date) => { const selectedDate = new Date(date); const today = new Date(); today.setHours(0, 0, 0, 0); if (selectedDate < today) { throw new Error('Дата не может быть в прошлом'); } // Максимум 90 дней вперед const maxDate = new Date(); maxDate.setDate(today.getDate() + 90); if (selectedDate > maxDate) { throw new Error('Дата не может быть более чем на 90 дней вперед'); } return true; }), body('time') .matches(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/) .withMessage('Неверный формат времени (используйте HH:MM)'), body('customer.name') .notEmpty() .withMessage('Имя обязательно') .isLength({ min: 2, max: 100 }) .withMessage('Имя должно содержать от 2 до 100 символов'), body('customer.email') .isEmail() .normalizeEmail() .withMessage('Введите корректный email адрес'), body('customer.phone') .notEmpty() .withMessage('Телефон обязателен') .matches(/^[\+]?[0-9\s\-\(\)]+$/) .withMessage('Введите корректный номер телефона') .custom((phone) => { const digits = phone.replace(/\D/g, ''); if (digits.length < 10) { throw new Error('Номер телефона должен содержать не менее 10 цифр'); } return true; }), body('total') .isFloat({ min: 0 }) .withMessage('Сумма должна быть положительным числом') ]; // Валидация обновления профиля const validateProfileUpdate = [ body('name') .optional() .trim() .isLength({ min: 2, max: 50 }) .withMessage('Имя должно содержать от 2 до 50 символов'), body('avatar') .optional() .isURL() .withMessage('Аватар должен быть валидным URL'), body('currentPassword') .optional() .notEmpty() .withMessage('Текущий пароль обязателен для смены пароля'), body('newPassword') .optional() .isLength({ min: 6 }) .withMessage('Новый пароль должен содержать не менее 6 символов') .custom((newPassword, { req }) => { if (req.body.currentPassword && !newPassword) { throw new Error('Новый пароль обязателен при указании текущего пароля'); } return true; }) ]; // Валидация смены пароля const validateChangePassword = [ body('currentPassword') .notEmpty() .withMessage('Текущий пароль обязателен'), body('newPassword') .isLength({ min: 6 }) .withMessage('Новый пароль должен содержать не менее 6 символов') .matches(/[A-Z]/) .withMessage('Новый пароль должен содержать хотя бы одну заглавную букву') .matches(/[a-z]/) .withMessage('Новый пароль должен содержать хотя бы одну строчную букву') .matches(/\d/) .withMessage('Новый пароль должен содержать хотя бы одну цифру') .custom((newPassword, { req }) => { if (newPassword === req.body.currentPassword) { throw new Error('Новый пароль должен отличаться от текущего'); } return true; }) ]; // Валидация email const validateEmail = [ body('email') .isEmail() .normalizeEmail() .withMessage('Введите корректный email адрес') ]; // Валидация ID параметра const validateIdParam = [ param('id') .isInt({ min: 1 }) .withMessage('ID должен быть положительным целым числом') ]; // Валидация animalId параметра const validateAnimalIdParam = [ param('animalId') .notEmpty() .withMessage('ID животного обязательно') .isLength({ max: 50 }) .withMessage('ID животного слишком длинный') ]; // Валидация orderId параметра const validateOrderIdParam = [ param('orderId') .notEmpty() .withMessage('ID заказа обязательно') .matches(/^TICKET-[A-Z0-9]{8}$/) .withMessage('Неверный формат ID заказа') ]; // Валидация поискового запроса const validateSearchQuery = [ query('query') .optional() .isLength({ min: 2 }) .withMessage('Поисковый запрос должен содержать не менее 2 символов') ]; // Валидация пагинации const validatePagination = [ query('limit') .optional() .isInt({ min: 1, max: 100 }) .withMessage('Лимит должен быть от 1 до 100'), query('offset') .optional() .isInt({ min: 0 }) .withMessage('Смещение должно быть неотрицательным числом') ]; // Валидация фильтров животных const validateAnimalFilters = [ query('type') .optional() .isIn(['mammal', 'bird', 'reptile', 'fish', 'amphibian', 'insect']) .withMessage('Неверный тип животного'), query('habitat') .optional() .isString() .withMessage('Неверный формат среды обитания'), query('diet') .optional() .isIn(['carnivore', 'herbivore', 'omnivore', 'insectivore']) .withMessage('Неверный тип питания'), query('conservationStatus') .optional() .isString() .withMessage('Неверный статус сохранения') ]; // Валидация сортировки const validateSorting = [ query('sort') .optional() .isIn(['name', 'popularity', 'type', 'created_at', 'rating', 'price']) .withMessage('Неверный параметр сортировки'), query('order') .optional() .isIn(['asc', 'desc']) .withMessage('Порядок должен быть asc или desc') ]; // Функция обработки ошибок валидации const validateResult = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { const formattedErrors = errors.array().map(error => ({ field: error.path, message: error.msg, value: error.value, location: error.location })); return res.status(400).json({ success: false, message: 'Ошибка валидации данных', errors: formattedErrors }); } next(); }; // Функция для санитизации строк (защита от XSS) const sanitizeString = (str) => { if (typeof str !== 'string') return str; return str .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, ''') .replace(/\//g, '/'); }; // Функция для санитизации объекта const sanitizeObject = (obj) => { if (typeof obj !== 'object' || obj === null) return obj; const sanitized = {}; for (const key in obj) { if (typeof obj[key] === 'string') { sanitized[key] = sanitizeString(obj[key]); } else if (typeof obj[key] === 'object') { sanitized[key] = sanitizeObject(obj[key]); } else { sanitized[key] = obj[key]; } } return sanitized; }; // Middleware для санитизации входных данных const sanitizeInput = (req, res, next) => { if (req.body) { req.body = sanitizeObject(req.body); } if (req.query) { req.query = sanitizeObject(req.query); } // Параметры URL не санитизируем, так как они уже обработаны Express next(); }; // Проверка CSRF токена const validateCsrfToken = (req, res, next) => { // Для GET, HEAD, OPTIONS запросов пропускаем проверку if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) { return next(); } const csrfToken = req.headers['x-csrf-token'] || req.body._csrf; if (!csrfToken) { return res.status(403).json({ success: false, message: 'CSRF токен отсутствует' }); } // В реальном приложении здесь должна быть проверка токена // Для демо просто проверяем наличие next(); }; // Проверка rate limiting const createRateLimiter = (options = {}) => { const { windowMs = 15 * 60 * 1000, // 15 минут max = 100, // максимум 100 запросов message = 'Слишком много запросов. Пожалуйста, попробуйте позже.', skipSuccessfulRequests = false } = options; const requests = new Map(); return (req, res, next) => { const now = Date.now(); const ip = req.ip || req.connection.remoteAddress; // Очистка старых записей (раз в 1000 запросов) if (requests.size > 1000) { for (const [key, timestamps] of requests.entries()) { const validTimestamps = timestamps.filter(time => time > now - windowMs); if (validTimestamps.length === 0) { requests.delete(key); } else { requests.set(key, validTimestamps); } } } if (!requests.has(ip)) { requests.set(ip, []); } const timestamps = requests.get(ip); const validTimestamps = timestamps.filter(time => time > now - windowMs); if (validTimestamps.length >= max) { return res.status(429).json({ success: false, message, retryAfter: Math.ceil(windowMs / 1000) }); } validTimestamps.push(now); requests.set(ip, validTimestamps); // Устанавливаем заголовки rate limiting res.setHeader('X-RateLimit-Limit', max); res.setHeader('X-RateLimit-Remaining', max - validTimestamps.length); res.setHeader('X-RateLimit-Reset', Math.ceil((now + windowMs) / 1000)); next(); }; }; module.exports = { validateRegistration, validateLogin, validateReview, validateTicket, validateProfileUpdate, validateChangePassword, validateEmail, validateIdParam, validateAnimalIdParam, validateOrderIdParam, validateSearchQuery, validatePagination, validateAnimalFilters, validateSorting, validateResult, sanitizeString, sanitizeObject, sanitizeInput, validateCsrfToken, createRateLimiter };