Files
turftracker/backend/src/utils/validation.js
Jake Kasper 7983503e5e multiarea
2025-08-26 07:46:37 -05:00

220 lines
7.9 KiB
JavaScript

const Joi = require('joi');
// User validation schemas
const registerSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#+])[A-Za-z\d@$!%*?&#+]/).required()
.messages({
'string.pattern.base': 'Password must contain at least one lowercase letter, one uppercase letter, one number, and one special character'
}),
firstName: Joi.string().max(100).required(),
lastName: Joi.string().max(100).required()
});
const loginSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().required()
});
const updateUserSchema = Joi.object({
firstName: Joi.string().max(100),
lastName: Joi.string().max(100),
email: Joi.string().email()
});
const changePasswordSchema = Joi.object({
currentPassword: Joi.string().required(),
newPassword: Joi.string().min(8).pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#+])[A-Za-z\d@$!%*?&#+]/).required()
.messages({
'string.pattern.base': 'Password must contain at least one lowercase letter, one uppercase letter, one number, and one special character'
})
});
// Property validation schemas
const propertySchema = Joi.object({
name: Joi.string().max(255).required(),
address: Joi.string().max(500),
latitude: Joi.number().min(-90).max(90),
longitude: Joi.number().min(-180).max(180),
totalArea: Joi.number().positive()
});
const lawnSectionSchema = Joi.object({
name: Joi.string().max(255).required(),
area: Joi.number().positive().required(),
polygonData: Joi.object(),
grassType: Joi.string().max(100).allow(null, '').optional(),
soilType: Joi.string().max(100).allow(null, '').optional()
});
// Equipment validation schemas
const equipmentSchema = Joi.object({
equipmentTypeId: Joi.number().integer().positive().required(),
customName: Joi.string().max(255),
tankSize: Joi.number().positive(),
pumpGpm: Joi.number().positive(),
nozzleGpm: Joi.number().positive(),
nozzleCount: Joi.number().integer().positive(),
spreaderWidth: Joi.number().positive()
});
// Product validation schemas
const productSchema = Joi.object({
name: Joi.string().max(255).required(),
brand: Joi.string().max(100),
categoryId: Joi.number().integer().positive().required(),
productType: Joi.string().valid('granular', 'liquid').required(),
activeIngredients: Joi.string(),
description: Joi.string()
});
const productRateSchema = Joi.object({
applicationType: Joi.string().max(100).required(),
rateAmount: Joi.number().positive().required(),
rateUnit: Joi.string().max(50).required(),
notes: Joi.string()
});
const userProductSchema = Joi.object({
productId: Joi.number().integer().positive().allow(null).optional(),
customName: Joi.string().max(255).allow(null).optional(),
customRateAmount: Joi.number().positive().allow(null).optional(),
customRateUnit: Joi.string().max(50).allow(null).optional(),
notes: Joi.string().allow(null, '').optional(),
// Additional fields for advanced editing
brand: Joi.string().max(100).allow(null).optional(),
categoryId: Joi.number().integer().positive().allow(null).optional(),
productType: Joi.string().valid('granular', 'liquid', 'seed', 'powder').allow(null).optional(),
activeIngredients: Joi.string().allow(null).optional(),
description: Joi.string().allow(null).optional(),
isAdvancedEdit: Joi.boolean().optional(),
// Spreader settings for granular products
spreaderSettings: Joi.array().items(
Joi.object({
id: Joi.number().optional(), // For existing settings
equipmentId: Joi.number().integer().positive().optional(), // Link to user_equipment
// Legacy fields for backward compatibility
spreaderBrand: Joi.string().max(100).allow(null, '').optional(),
spreaderModel: Joi.alternatives().try(
Joi.string().max(100).allow(''),
Joi.allow(null)
).optional(),
settingValue: Joi.string().max(20).required(),
rateDescription: Joi.alternatives().try(
Joi.string().max(200).allow(''),
Joi.allow(null)
).optional(),
notes: Joi.alternatives().try(
Joi.string().allow(''),
Joi.allow(null)
).optional()
}).custom((value, helpers) => {
// Custom validation: require either equipmentId OR spreaderBrand
if (value.equipmentId || value.spreaderBrand) {
return value; // Valid if either exists
}
return helpers.error('custom.equipmentOrBrand');
}, 'Equipment or Brand validation')
).optional()
.messages({
'custom.equipmentOrBrand': 'Either equipmentId or spreaderBrand must be provided'
})
});
// Application validation schemas
const applicationPlanSchema = Joi.object({
lawnSectionId: Joi.number().integer().positive().optional(), // Keep for backward compatibility
lawnSectionIds: Joi.array().items(Joi.number().integer().positive()).min(1).optional(), // New multi-area support
equipmentId: Joi.number().integer().positive().required(),
nozzleId: Joi.number().integer().positive().optional(),
plannedDate: Joi.date().required(),
notes: Joi.string().allow('').optional(),
areaSquareFeet: Joi.number().positive().optional(),
equipment: Joi.object({
id: Joi.number().integer().positive().optional(),
categoryName: Joi.string().optional(),
tankSizeGallons: Joi.number().positive().allow(null).optional(),
pumpGpm: Joi.number().positive().allow(null).optional(),
sprayWidthFeet: Joi.number().positive().allow(null).optional(),
capacityLbs: Joi.number().positive().allow(null).optional(),
spreadWidth: Joi.number().positive().allow(null).optional()
}).optional(),
nozzle: Joi.object({
id: Joi.number().integer().positive().optional(),
flowRateGpm: Joi.number().positive().allow(null).optional(),
sprayAngle: Joi.number().integer().allow(null).optional()
}).allow(null).optional(),
products: Joi.array().items(Joi.object({
productId: Joi.number().integer().positive().optional(),
userProductId: Joi.number().integer().positive().optional(),
rateAmount: Joi.number().positive().required(),
rateUnit: Joi.string().max(50).required(),
applicationType: Joi.string().valid('liquid', 'granular').optional()
})).min(1).required()
}).or('lawnSectionId', 'lawnSectionIds'); // At least one lawn section parameter is required
const applicationLogSchema = Joi.object({
planId: Joi.number().integer().positive(),
lawnSectionId: Joi.number().integer().positive().required(),
equipmentId: Joi.number().integer().positive().required(),
weatherConditions: Joi.object(),
gpsTrack: Joi.object(),
averageSpeed: Joi.number().positive(),
areaCovered: Joi.number().positive(),
notes: Joi.string(),
products: Joi.array().items(Joi.object({
productId: Joi.number().integer().positive(),
userProductId: Joi.number().integer().positive(),
rateAmount: Joi.number().positive().required(),
rateUnit: Joi.string().max(50).required(),
actualProductAmount: Joi.number().positive(),
actualWaterAmount: Joi.number().positive(),
actualSpeedMph: Joi.number().positive()
})).min(1).required()
});
// Validation middleware
const validateRequest = (schema) => {
return (req, res, next) => {
const { error } = schema.validate(req.body, { abortEarly: false });
if (error) {
return next(error);
}
next();
};
};
const validateParams = (schema) => {
return (req, res, next) => {
const { error } = schema.validate(req.params, { abortEarly: false });
if (error) {
return next(error);
}
next();
};
};
const idParamSchema = Joi.object({
id: Joi.number().integer().positive().required()
});
module.exports = {
// Schemas
registerSchema,
loginSchema,
updateUserSchema,
changePasswordSchema,
propertySchema,
lawnSectionSchema,
equipmentSchema,
productSchema,
productRateSchema,
userProductSchema,
applicationPlanSchema,
applicationLogSchema,
idParamSchema,
// Middleware
validateRequest,
validateParams
};