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
Markdown
# 📦 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/)