campverse-backend / Middleware /validation.js
CampVerse CI
Deploy backend from GitHub Actions
d792ca0
Raw History Blame Contribute Delete
14.3 kB
/**
* Input Validation Middleware
* Provides comprehensive validation for API requests with security enhancements
*/
const joi = require('joi');
const { ValidationError } = require('../Utils/errors');
const xss = require('xss');
// Enhanced sanitization functions
const sanitize = {
// HTML sanitization to prevent XSS
html: (text) => {
if (typeof text !== 'string') return text;
return xss(text, {
whiteList: {}, // No HTML allowed
stripIgnoreTag: true,
stripIgnoreTagBody: ['script', 'style', 'iframe', 'object', 'embed']
});
},
// SQL injection prevention
sql: (text) => {
if (typeof text !== 'string') return text;
// Remove dangerous SQL characters
return text.replace(/['";\\]/g, '').replace(/--/g, '').replace(/\/\*/g, '').replace(/\*\//g, '');
},
// NoSQL injection prevention
nosql: (text) => {
if (typeof text !== 'string') return text;
// Remove MongoDB operators
return text.replace(/\$/, '').replace(/\./, '');
},
// Trim and clean whitespace
trim: (obj) => {
if (typeof obj === 'string') {
return obj.trim();
}
if (Array.isArray(obj)) {
return obj.map(sanitize.trim);
}
if (obj && typeof obj === 'object') {
const sanitized = {};
for (const [key, value] of Object.entries(obj)) {
sanitized[key] = sanitize.trim(value);
}
return sanitized;
}
return obj;
},
// Comprehensive sanitization
deep: (obj) => {
if (typeof obj === 'string') {
return sanitize.html(sanitize.sql(sanitize.nosql(sanitize.trim(obj))));
}
if (Array.isArray(obj)) {
return obj.map(sanitize.deep);
}
if (obj && typeof obj === 'object') {
const sanitized = {};
for (const [key, value] of Object.entries(obj)) {
sanitized[key] = sanitize.deep(value);
}
return sanitized;
}
return obj;
}
};
// Common validation schemas with enhanced security
const commonSchemas = {
objectId: joi.string().pattern(/^[0-9a-fA-F]{24}$/).message('Invalid ID format'),
email: joi.string().email().pattern(/@([\w.-]+\.(ac|edu)\.in|[\w.-]+\.edu|[\w.-]+\.edu\.co\.in|[\w.-]*cuchd\.in)$/i).message('Must be a valid academic email or approved domain (e.g., cuchd.in)'),
url: joi.string().uri({ scheme: ['http', 'https'] }).max(500),
phone: joi.string().pattern(/^[+]?[1-9][\d]{0,15}$/).message('Invalid phone number format'),
password: joi.string().min(8).pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/)
.message('Password must be at least 8 characters with uppercase, lowercase, number, and special character'),
name: joi.string().min(2).max(100).trim().pattern(/^[a-zA-Z\s]+$/).message('Name can only contain letters and spaces'),
description: joi.string().max(2000).trim(),
tags: joi.array().items(joi.string().max(50)).max(10),
date: joi.date().iso().max('now'),
positiveInteger: joi.number().integer().positive(),
boolean: joi.boolean().strict(),
uuid: joi.string().uuid()
};
// Enhanced user validation schemas
const userSchemas = {
register: joi.object({
name: commonSchemas.name.required(),
email: commonSchemas.email.required(),
password: commonSchemas.password.required(),
phone: commonSchemas.phone.optional(),
location: joi.string().max(100).optional(),
bio: commonSchemas.description.optional(),
interests: commonSchemas.tags.optional(),
skills: commonSchemas.tags.optional(),
}).strict(), // Reject unknown fields
login: joi.object({
email: commonSchemas.email.required(),
password: joi.string().required(),
}).strict(),
updateProfile: joi.object({
name: commonSchemas.name.optional(),
phone: commonSchemas.phone.optional(),
location: joi.string().max(100).optional(),
bio: commonSchemas.description.optional(),
interests: commonSchemas.tags.optional(),
skills: commonSchemas.tags.optional(),
learningGoals: commonSchemas.tags.optional(),
}).strict(),
changePassword: joi.object({
currentPassword: joi.string().required(),
newPassword: commonSchemas.password.required(),
}).strict(),
forgotPassword: joi.object({
email: commonSchemas.email.required(),
}).strict(),
resetPassword: joi.object({
token: joi.string().required().min(32).max(64),
password: commonSchemas.password.required(),
}).strict(),
};
// Enhanced institution validation schemas
const institutionSchemas = {
create: joi.object({
name: commonSchemas.name.required(),
type: joi.string().valid('college', 'university', 'org', 'temporary').required(),
emailDomain: joi.string().domain().required(),
website: commonSchemas.url.optional(),
phone: commonSchemas.phone.optional(),
info: commonSchemas.description.optional(),
location: joi.object({
city: joi.string().max(100).optional(),
state: joi.string().max(100).optional(),
country: joi.string().max(100).optional(),
}).optional(),
}).strict(),
requestNew: joi.object({
name: commonSchemas.name.required(),
type: joi.string().valid('college', 'university', 'org', 'temporary').required(),
website: commonSchemas.url.optional(),
phone: commonSchemas.phone.optional(),
info: commonSchemas.description.optional(),
}).strict(),
// REMOVED: requestVerification schema (redundant workflow removed)
approveVerification: joi.object({
website: commonSchemas.url.optional(),
phone: commonSchemas.phone.optional(),
info: commonSchemas.description.optional(),
remarks: joi.string().max(500).optional(),
location: joi.object({
city: joi.string().max(100).optional(),
state: joi.string().max(100).optional(),
country: joi.string().max(100).optional(),
}).optional(),
}).strict(),
rejectVerification: joi.object({
remarks: joi.string().max(500).required(),
}).strict(),
};
// Enhanced event validation schemas
const eventSchemas = {
create: joi.object({
title: commonSchemas.name.required(),
description: commonSchemas.description.required(),
tags: commonSchemas.tags.optional(),
type: joi.string().max(50).required(),
organizer: commonSchemas.name.required(),
schedule: joi.object({
start: joi.date().iso().min('now').required(),
end: joi.date().iso().min(joi.ref('start')).required(),
}).required(),
isPaid: joi.boolean().default(false),
price: joi.number().min(0).when('isPaid', { is: true, then: joi.required() }),
capacity: commonSchemas.positiveInteger.max(10000).optional(),
features: joi.object({
certificateEnabled: joi.boolean().default(false),
chatEnabled: joi.boolean().default(false),
}).optional(),
location: joi.string().max(200).optional(),
maxParticipants: commonSchemas.positiveInteger.max(10000).optional(),
}).strict(),
update: joi.object({
title: commonSchemas.name.optional(),
description: commonSchemas.description.optional(),
tags: commonSchemas.tags.optional(),
type: joi.string().max(50).optional(),
organizer: commonSchemas.name.optional(),
schedule: joi.object({
start: joi.date().iso().optional(),
end: joi.date().iso().optional(),
}).optional(),
isPaid: joi.boolean().optional(),
price: joi.number().min(0).optional(),
capacity: commonSchemas.positiveInteger.max(10000).optional(),
features: joi.object({
certificateEnabled: joi.boolean().optional(),
chatEnabled: joi.boolean().optional(),
}).optional(),
location: joi.string().max(200).optional(),
maxParticipants: commonSchemas.positiveInteger.max(10000).optional(),
}).strict(),
rsvp: joi.object({
eventId: commonSchemas.objectId.required(),
}).strict(),
scanQr: joi.object({
eventId: commonSchemas.objectId.required(),
qrToken: joi.string().required().min(16).max(64),
}).strict(),
nominateCoHost: joi.object({
eventId: commonSchemas.objectId.required(),
userId: commonSchemas.objectId.required(),
}).strict(),
};
// Enhanced feedback validation schemas
const feedbackSchemas = {
submit: joi.object({
rating: joi.number().integer().min(1).max(5).required(),
category: joi.string().valid('ui', 'bug', 'feature', 'performance', 'events', 'other').required(),
subject: joi.string().max(200).required(),
message: commonSchemas.description.required(),
eventId: commonSchemas.objectId.optional(),
}).strict(),
update: joi.object({
status: joi.string().valid('open', 'in-progress', 'resolved', 'closed').required(),
adminNotes: joi.string().max(1000).optional(),
priority: joi.string().valid('low', 'medium', 'high', 'urgent').optional(),
}).strict(),
};
// Enhanced support validation schemas
const supportSchemas = {
createTicket: joi.object({
topic: joi.string().valid('account', 'events', 'technical', 'billing', 'other').required(),
subject: joi.string().max(200).required(),
message: commonSchemas.description.required(),
priority: joi.string().valid('low', 'medium', 'high', 'urgent').default('medium'),
}).strict(),
updateTicket: joi.object({
status: joi.string().valid('open', 'in-progress', 'waiting', 'resolved', 'closed').required(),
adminNotes: joi.string().max(1000).optional(),
priority: joi.string().valid('low', 'medium', 'high', 'urgent').optional(),
}).strict(),
};
// Query parameter validation schemas
const querySchemas = {
pagination: joi.object({
page: joi.number().integer().min(1).max(1000).default(1),
limit: joi.number().integer().min(1).max(100).default(20),
sort: joi.string().valid('createdAt', 'updatedAt', 'name', 'title', 'date').optional(),
order: joi.string().valid('asc', 'desc').default('desc'),
}).strict(),
search: joi.object({
q: joi.string().min(1).max(100).required(),
category: joi.string().optional(),
type: joi.string().optional(),
}).strict(),
// Date range validation (for filtering, not for event creation)
dateRange: joi.object({
startDate: joi.date().iso().optional(), // Used for filtering date ranges
endDate: joi.date().iso().min(joi.ref('startDate')).optional(), // Used for filtering date ranges
}).strict(),
};
// Validation middleware factory with enhanced security
function validate(schema, source = 'body') {
return (req, res, next) => {
try {
const data = req[source];
const { error, value } = schema.validate(data, {
abortEarly: false,
stripUnknown: true,
convert: false, // Don't convert types for security
allowUnknown: false, // Reject unknown fields
});
if (error) {
const details = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message,
value: detail.context?.value,
}));
throw new ValidationError('Validation failed', details);
}
// Apply deep sanitization to validated data
const sanitizedData = sanitize.deep(value);
// Replace the original data with validated and sanitized data
req[source] = sanitizedData;
next();
} catch (error) {
next(error);
}
};
}
// Enhanced sanitization middleware
function sanitizeInput(req, res, next) {
try {
// Apply sanitization to all input sources
if (req.body) req.body = sanitize.deep(req.body);
if (req.query) req.query = sanitize.deep(req.query);
if (req.params) req.params = sanitize.deep(req.params);
next();
} catch (error) {
next(error);
}
}
// File upload validation with enhanced security
const fileValidation = {
image: {
allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
maxSize: 5 * 1024 * 1024, // 5MB
maxFiles: 5,
allowedExtensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
},
document: {
allowedTypes: ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
maxSize: 10 * 1024 * 1024, // 10MB
maxFiles: 3,
allowedExtensions: ['.pdf', '.doc', '.docx'],
},
};
function validateFile(type = 'image') {
return (req, res, next) => {
try {
const config = fileValidation[type];
if (!config) {
throw new ValidationError('Invalid file validation type');
}
if (req.files) {
const files = Array.isArray(req.files) ? req.files : Object.values(req.files).flat();
if (files.length > config.maxFiles) {
throw new ValidationError(`Too many files. Maximum ${config.maxFiles} allowed`);
}
for (const file of files) {
// Check file type
if (!config.allowedTypes.includes(file.mimetype)) {
throw new ValidationError(`Invalid file type. Allowed: ${config.allowedTypes.join(', ')}`);
}
// Check file extension
const fileExtension = file.originalname.toLowerCase().substring(file.originalname.lastIndexOf('.'));
if (!config.allowedExtensions.includes(fileExtension)) {
throw new ValidationError(`Invalid file extension. Allowed: ${config.allowedExtensions.join(', ')}`);
}
// Check file size
if (file.size > config.maxSize) {
throw new ValidationError(`File too large. Maximum size: ${config.maxSize / (1024 * 1024)}MB`);
}
// Check for suspicious file names
if (file.originalname.includes('..') || file.originalname.includes('/') || file.originalname.includes('\\')) {
throw new ValidationError('Invalid file name');
}
}
}
next();
} catch (error) {
next(error);
}
};
}
// Rate limiting validation
function validateRateLimit(req, res, next) {
// Basic rate limiting check
if (req.rateLimit && req.rateLimit.remaining === 0) {
return res.status(429).json({
error: 'Rate limit exceeded',
retryAfter: Math.ceil(req.rateLimit.resetTime / 1000)
});
}
next();
}
module.exports = {
// Schemas
userSchemas,
institutionSchemas,
eventSchemas,
feedbackSchemas,
supportSchemas,
querySchemas,
commonSchemas,
// Middleware
validate,
sanitizeInput,
validateFile,
validateRateLimit,
// Utilities
sanitize,
fileValidation,
};