UNPKG

backend-mcp

Version:

Generador automático de backends con Node.js, Express, Prisma y módulos configurables. Servidor MCP compatible con npx para agentes IA. Soporta PostgreSQL, MySQL, MongoDB y SQLite.

729 lines (606 loc) 20.3 kB
# 📦 Módulo validation **Versión:** 1.0.0 **Categoría:** security **Descripción:** Sistema completo de validación con class-validator y esquemas personalizados ## 📊 Estado del Módulo | Componente | Estado | |------------|--------| | Script de inicialización | ✅ Disponible | | Templates | ✅ Disponible | | Ejemplos | ❌ Faltante | ## 🔗 Dependencias ### Opcionales - `database` - `logging` - `auth` ## 🤖 Triggers para IA Este módulo se activa automáticamente cuando se detectan las siguientes palabras clave: - **user_wants_validation**: true - **needs_data_validation**: true - **requires_input_sanitization**: true - **has_forms**: true - **undefined**: undefined ## ✨ Características - class-validator-integration - custom-validators - dto-validation - schema-validation - input-sanitization - transformation - conditional-validation - nested-validation - array-validation - file-validation - async-validation - custom-decorators ## 📖 Documentación Completa # Validation Module Comprehensive data validation and sanitization module for MCP Backend framework. ## Features - ✅ Schema-based validation (Joi, Yup, Zod) - ✅ Request/response validation middleware - ✅ Data sanitization and transformation - ✅ Custom validation rules - ✅ Async validation support - ✅ File upload validation - ✅ Multi-language error messages - ✅ Conditional validation - ✅ Cross-field validation - ✅ Performance optimized validation ## Installation This module is automatically installed when using the MCP Backend Generator. ## Configuration ### Environment Variables **General Configuration:** - `VALIDATION_ENABLED` (optional) - Enable validation (default: true) - `VALIDATION_STRICT_MODE` (optional) - Enable strict validation (default: true) - `VALIDATION_CACHE_ENABLED` (optional) - Enable schema caching (default: true) - `VALIDATION_MAX_ERRORS` (optional) - Maximum validation errors to return (default: 10) **File Validation:** - `FILE_MAX_SIZE` (optional) - Maximum file size in bytes (default: 10MB) - `FILE_ALLOWED_TYPES` (optional) - Allowed MIME types (comma-separated) - `FILE_UPLOAD_PATH` (optional) - File upload directory (default: ./uploads) **Security:** - `VALIDATION_XSS_PROTECTION` (optional) - Enable XSS protection (default: true) - `VALIDATION_SQL_INJECTION_PROTECTION` (optional) - Enable SQL injection protection (default: true) - `VALIDATION_RATE_LIMIT` (optional) - Validation rate limit per IP (default: 1000/hour) ### Configuration File ```typescript // src/config/validation.ts export const validationConfig = { enabled: process.env.VALIDATION_ENABLED !== 'false', strictMode: process.env.VALIDATION_STRICT_MODE !== 'false', cacheEnabled: process.env.VALIDATION_CACHE_ENABLED !== 'false', maxErrors: parseInt(process.env.VALIDATION_MAX_ERRORS || '10'), files: { maxSize: parseInt(process.env.FILE_MAX_SIZE || '10485760'), // 10MB allowedTypes: process.env.FILE_ALLOWED_TYPES?.split(',') || [ 'image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'text/plain' ], uploadPath: process.env.FILE_UPLOAD_PATH || './uploads' }, security: { xssProtection: process.env.VALIDATION_XSS_PROTECTION !== 'false', sqlInjectionProtection: process.env.VALIDATION_SQL_INJECTION_PROTECTION !== 'false', rateLimit: parseInt(process.env.VALIDATION_RATE_LIMIT || '1000') } }; ``` ## Usage ### Basic Validation ```typescript import { validationService } from './services/validation'; import Joi from 'joi'; // Define schema const userSchema = Joi.object({ name: Joi.string().min(2).max(50).required(), email: Joi.string().email().required(), age: Joi.number().integer().min(18).max(120), password: Joi.string().min(8).pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/).required(), confirmPassword: Joi.string().valid(Joi.ref('password')).required(), phone: Joi.string().pattern(/^\+?[1-9]\d{1,14}$/), address: Joi.object({ street: Joi.string().required(), city: Joi.string().required(), zipCode: Joi.string().pattern(/^\d{5}(-\d{4})?$/), country: Joi.string().length(2).uppercase() }) }); // Validate data const userData = { name: 'John Doe', email: 'john@example.com', age: 25, password: 'SecurePass123', confirmPassword: 'SecurePass123', phone: '+1234567890', address: { street: '123 Main St', city: 'New York', zipCode: '10001', country: 'US' } }; try { const validatedData = await validationService.validate(userSchema, userData); console.log('Validation successful:', validatedData); } catch (error) { console.error('Validation failed:', error.details); } ``` ### Express Middleware ```typescript import { validationMiddleware } from './middleware/validation'; import express from 'express'; const app = express(); // Validation middleware for request body app.post('/api/users', validationMiddleware.body(userSchema), async (req, res) => { // req.body is now validated and sanitized const user = await userService.create(req.body); res.json(user); } ); // Validation middleware for query parameters const querySchema = Joi.object({ page: Joi.number().integer().min(1).default(1), limit: Joi.number().integer().min(1).max(100).default(10), sort: Joi.string().valid('name', 'email', 'createdAt').default('createdAt'), order: Joi.string().valid('asc', 'desc').default('desc'), search: Joi.string().max(100) }); app.get('/api/users', validationMiddleware.query(querySchema), async (req, res) => { const users = await userService.findMany(req.query); res.json(users); } ); // Validation middleware for URL parameters const paramsSchema = Joi.object({ id: Joi.string().uuid().required() }); app.get('/api/users/:id', validationMiddleware.params(paramsSchema), async (req, res) => { const user = await userService.findById(req.params.id); res.json(user); } ); ``` ### Custom Validation Rules ```typescript import Joi from 'joi'; // Custom validation for strong password const strongPassword = Joi.extend({ type: 'strongPassword', base: Joi.string(), messages: { 'strongPassword.base': 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character' }, validate(value, helpers) { const strongPasswordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/; if (!strongPasswordRegex.test(value)) { return { value, errors: helpers.error('strongPassword.base') }; } return { value }; } }); // Custom validation for unique email const uniqueEmail = Joi.extend({ type: 'uniqueEmail', base: Joi.string().email(), messages: { 'uniqueEmail.exists': 'Email address is already registered' }, validate: async (value, helpers) => { const existingUser = await userService.findByEmail(value); if (existingUser) { return { value, errors: helpers.error('uniqueEmail.exists') }; } return { value }; } }); // Usage const registrationSchema = Joi.object({ email: uniqueEmail.uniqueEmail().required(), password: strongPassword.strongPassword().min(8).required() }); ``` ### Data Sanitization ```typescript import { sanitizationService } from './services/sanitization'; // HTML sanitization const userInput = '<script>alert("XSS")</script><p>Hello World</p>'; const sanitized = sanitizationService.sanitizeHtml(userInput); console.log(sanitized); // '<p>Hello World</p>' // SQL injection protection const sqlQuery = "'; DROP TABLE users; --"; const safeSql = sanitizationService.escapeSql(sqlQuery); console.log(safeSql); // "\'; DROP TABLE users; --" // Email sanitization const email = ' JOHN@EXAMPLE.COM '; const sanitizedEmail = sanitizationService.sanitizeEmail(email); console.log(sanitizedEmail); // 'john@example.com' // Phone number sanitization const phone = '+1 (555) 123-4567'; const sanitizedPhone = sanitizationService.sanitizePhone(phone); console.log(sanitizedPhone); // '+15551234567' // URL sanitization const url = 'javascript:alert("XSS")'; const sanitizedUrl = sanitizationService.sanitizeUrl(url); console.log(sanitizedUrl); // null (dangerous URL removed) ``` ### File Upload Validation ```typescript import { fileValidationService } from './services/fileValidation'; import multer from 'multer'; // Configure multer with validation const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: validationConfig.files.maxSize }, fileFilter: (req, file, cb) => { const isValid = fileValidationService.validateFile(file); cb(null, isValid); } }); // File validation schema const fileSchema = Joi.object({ fieldname: Joi.string().required(), originalname: Joi.string().required(), mimetype: Joi.string().valid(...validationConfig.files.allowedTypes).required(), size: Joi.number().max(validationConfig.files.maxSize).required() }); // Upload endpoint with validation app.post('/api/upload', upload.single('file'), validationMiddleware.file(fileSchema), async (req, res) => { // Additional file content validation const isValidContent = await fileValidationService.validateFileContent(req.file); if (!isValidContent) { return res.status(400).json({ error: 'Invalid file content' }); } const savedFile = await fileService.save(req.file); res.json(savedFile); } ); // Image-specific validation const imageSchema = Joi.object({ mimetype: Joi.string().valid('image/jpeg', 'image/png', 'image/gif').required(), size: Joi.number().max(5 * 1024 * 1024).required(), // 5MB max dimensions: Joi.object({ width: Joi.number().min(100).max(4000), height: Joi.number().min(100).max(4000) }) }); app.post('/api/upload/image', upload.single('image'), async (req, res) => { // Get image dimensions const dimensions = await fileValidationService.getImageDimensions(req.file.buffer); req.file.dimensions = dimensions; // Validate with dimensions const { error } = imageSchema.validate(req.file); if (error) { return res.status(400).json({ error: error.details }); } const savedImage = await imageService.save(req.file); res.json(savedImage); } ); ``` ### Conditional Validation ```typescript // Conditional validation based on user role const userUpdateSchema = Joi.object({ name: Joi.string().min(2).max(50), email: Joi.string().email(), role: Joi.string().valid('user', 'admin', 'moderator'), permissions: Joi.when('role', { is: 'admin', then: Joi.array().items(Joi.string()).required(), otherwise: Joi.forbidden() }), department: Joi.when('role', { is: Joi.valid('admin', 'moderator'), then: Joi.string().required(), otherwise: Joi.optional() }) }); // Conditional validation based on payment method const paymentSchema = Joi.object({ amount: Joi.number().positive().required(), currency: Joi.string().length(3).uppercase().required(), method: Joi.string().valid('card', 'bank', 'paypal').required(), // Card-specific fields cardNumber: Joi.when('method', { is: 'card', then: Joi.string().creditCard().required(), otherwise: Joi.forbidden() }), expiryDate: Joi.when('method', { is: 'card', then: Joi.string().pattern(/^(0[1-9]|1[0-2])\/\d{2}$/).required(), otherwise: Joi.forbidden() }), cvv: Joi.when('method', { is: 'card', then: Joi.string().pattern(/^\d{3,4}$/).required(), otherwise: Joi.forbidden() }), // Bank-specific fields accountNumber: Joi.when('method', { is: 'bank', then: Joi.string().required(), otherwise: Joi.forbidden() }), routingNumber: Joi.when('method', { is: 'bank', then: Joi.string().required(), otherwise: Joi.forbidden() }), // PayPal-specific fields paypalEmail: Joi.when('method', { is: 'paypal', then: Joi.string().email().required(), otherwise: Joi.forbidden() }) }); ``` ### Cross-Field Validation ```typescript // Date range validation const eventSchema = Joi.object({ title: Joi.string().required(), startDate: Joi.date().required(), endDate: Joi.date().min(Joi.ref('startDate')).required(), registrationDeadline: Joi.date().max(Joi.ref('startDate')).required() }); // Password confirmation const passwordChangeSchema = Joi.object({ currentPassword: Joi.string().required(), newPassword: Joi.string().min(8).required(), confirmPassword: Joi.string().valid(Joi.ref('newPassword')).required().messages({ 'any.only': 'Password confirmation does not match' }) }); // Budget validation const budgetSchema = Joi.object({ totalBudget: Joi.number().positive().required(), categories: Joi.array().items( Joi.object({ name: Joi.string().required(), amount: Joi.number().positive().required() }) ).required() }).custom((value, helpers) => { const totalCategoryAmount = value.categories.reduce((sum, cat) => sum + cat.amount, 0); if (totalCategoryAmount > value.totalBudget) { return helpers.error('budget.exceeds', { total: value.totalBudget, categories: totalCategoryAmount }); } return value; }, 'Budget validation').messages({ 'budget.exceeds': 'Category amounts ({{#categories}}) exceed total budget ({{#total}})' }); ``` ### Async Validation ```typescript // Async validation for external API const addressValidationSchema = Joi.object({ street: Joi.string().required(), city: Joi.string().required(), zipCode: Joi.string().required(), country: Joi.string().length(2).required() }).external(async (value) => { // Validate address with external service const isValid = await addressValidationService.validateAddress(value); if (!isValid) { throw new Error('Invalid address'); } return value; }); // Async validation for database uniqueness const productSchema = Joi.object({ name: Joi.string().required(), sku: Joi.string().required(), price: Joi.number().positive().required() }).external(async (value) => { // Check if SKU is unique const existingProduct = await productService.findBySku(value.sku); if (existingProduct) { throw new Error('SKU already exists'); } return value; }); ``` ### Multi-Language Error Messages ```typescript // Define error messages in multiple languages const errorMessages = { en: { 'string.empty': 'Field cannot be empty', 'string.email': 'Please enter a valid email address', 'number.min': 'Value must be at least {{#limit}}', 'any.required': 'This field is required' }, es: { 'string.empty': 'El campo no puede estar vacío', 'string.email': 'Por favor ingrese una dirección de email válida', 'number.min': 'El valor debe ser al menos {{#limit}}', 'any.required': 'Este campo es obligatorio' }, fr: { 'string.empty': 'Le champ ne peut pas être vide', 'string.email': 'Veuillez saisir une adresse email valide', 'number.min': 'La valeur doit être au moins {{#limit}}', 'any.required': 'Ce champ est requis' } }; // Validation with language support const validateWithLanguage = async (schema, data, language = 'en') => { try { const result = await schema.validateAsync(data, { messages: errorMessages[language] || errorMessages.en }); return { success: true, data: result }; } catch (error) { return { success: false, errors: error.details.map(detail => ({ field: detail.path.join('.'), message: detail.message })) }; } }; // Usage in middleware const multiLanguageValidation = (schema) => { return async (req, res, next) => { const language = req.headers['accept-language']?.split(',')[0]?.split('-')[0] || 'en'; const result = await validateWithLanguage(schema, req.body, language); if (!result.success) { return res.status(400).json({ errors: result.errors }); } req.body = result.data; next(); }; }; ``` ### Performance Optimization ```typescript // Schema caching const schemaCache = new Map(); const getCachedSchema = (schemaKey, schemaFactory) => { if (!schemaCache.has(schemaKey)) { schemaCache.set(schemaKey, schemaFactory()); } return schemaCache.get(schemaKey); }; // Compiled schema for better performance const userSchemaCompiled = getCachedSchema('user', () => Joi.object({ name: Joi.string().min(2).max(50).required(), email: Joi.string().email().required(), age: Joi.number().integer().min(18).max(120) }).compile() ); // Batch validation const validateBatch = async (schema, dataArray) => { const results = await Promise.allSettled( dataArray.map(data => schema.validateAsync(data)) ); return results.map((result, index) => ({ index, success: result.status === 'fulfilled', data: result.status === 'fulfilled' ? result.value : null, errors: result.status === 'rejected' ? result.reason.details : null })); }; // Streaming validation for large datasets const validateStream = (schema) => { return new Transform({ objectMode: true, transform(chunk, encoding, callback) { schema.validateAsync(chunk) .then(validData => callback(null, validData)) .catch(error => callback(error)); } }); }; ``` ## Testing ```typescript // tests/validation.test.ts import { validationService } from '../src/services/validation'; import Joi from 'joi'; describe('Validation Service', () => { const testSchema = Joi.object({ name: Joi.string().required(), email: Joi.string().email().required(), age: Joi.number().integer().min(18) }); it('should validate correct data', async () => { const validData = { name: 'John Doe', email: 'john@example.com', age: 25 }; const result = await validationService.validate(testSchema, validData); expect(result).toEqual(validData); }); it('should reject invalid data', async () => { const invalidData = { name: '', email: 'invalid-email', age: 15 }; await expect(validationService.validate(testSchema, invalidData)) .rejects.toThrow(); }); it('should sanitize HTML content', () => { const dirtyHtml = '<script>alert("XSS")</script><p>Clean content</p>'; const clean = sanitizationService.sanitizeHtml(dirtyHtml); expect(clean).toBe('<p>Clean content</p>'); expect(clean).not.toContain('<script>'); }); it('should validate file uploads', () => { const validFile = { fieldname: 'upload', originalname: 'test.jpg', mimetype: 'image/jpeg', size: 1024 * 1024 // 1MB }; const isValid = fileValidationService.validateFile(validFile); expect(isValid).toBe(true); }); }); ``` ```bash npm test -- validation ``` ## Dependencies - joi (Schema validation) - yup (Alternative schema validation) - zod (TypeScript-first schema validation) - dompurify (HTML sanitization) - validator (String validation) - multer (File upload handling) - sharp (Image processing and validation) - mime-types (MIME type detection) ## Integration - Integrates with all modules requiring data validation - Provides middleware for Express applications - Works with database module for data integrity - Integrates with auth module for user input validation ## Error Handling - `ValidationError`: Data validation failed - `SanitizationError`: Data sanitization failed - `FileValidationError`: File validation failed - `SchemaCompilationError`: Schema compilation failed - `RateLimitExceededError`: Validation rate limit exceeded ## Best Practices 1. **Schema Design**: Design schemas to be strict but user-friendly 2. **Error Messages**: Provide clear, actionable error messages 3. **Performance**: Cache compiled schemas for better performance 4. **Security**: Always sanitize user input 5. **Testing**: Test validation rules thoroughly 6. **Documentation**: Document custom validation rules 7. **Localization**: Support multiple languages for error messages 8. **File Validation**: Always validate file uploads for security ## License MIT ## 🔗 Enlaces - [Volver al índice de módulos](./README.md) - [Documentación principal](../README.md) - [Código fuente](../../modules/validation/)