diff --git a/backend/src/middleware/validation.ts b/backend/src/middleware/validation.ts index 28951b3..26b3c2d 100644 --- a/backend/src/middleware/validation.ts +++ b/backend/src/middleware/validation.ts @@ -7,12 +7,13 @@ export const validateLogin = [ .isEmail() .withMessage('Must be a valid email') .normalizeEmail(), - + body('password') - .isLength({ min: 6 }) - .withMessage('Password must be at least 6 characters') - .trim() - .escape() + .optional() + .isLength({ min: 8 }) + .withMessage('Password must be at least 8 characters') + .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/) + .withMessage('Password must contain uppercase, lowercase, number and special character (@$!%*?&)'), ]; export const validateRegister = [ @@ -21,18 +22,19 @@ export const validateRegister = [ .withMessage('First name must be between 1-100 characters') .trim() .escape(), - + body('lastname') .isLength({ min: 1, max: 100 }) .withMessage('Last name must be between 1-100 characters') .trim() .escape(), - + body('password') + .optional() .isLength({ min: 8 }) .withMessage('Password must be at least 8 characters') - .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/) - .withMessage('Password must contain uppercase, lowercase and number') + .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/) + .withMessage('Password must contain uppercase, lowercase, number and special character (@$!%*?&)'), ]; // ===== EMPLOYEE VALIDATION ===== @@ -42,49 +44,49 @@ export const validateEmployee = [ .withMessage('First name must be between 1-100 characters') .trim() .escape(), - + body('lastname') .isLength({ min: 1, max: 100 }) .withMessage('Last name must be between 1-100 characters') .trim() .escape(), - + body('password') .optional() .isLength({ min: 8 }) .withMessage('Password must be at least 8 characters') - .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/) - .withMessage('Password must contain uppercase, lowercase and number'), - + .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/) + .withMessage('Password must contain uppercase, lowercase, number and special character (@$!%*?&)'), + body('employeeType') .isIn(['manager', 'personell', 'apprentice', 'guest']) .withMessage('Employee type must be manager, personell, apprentice or guest'), - + body('contractType') .optional() .isIn(['small', 'large', 'flexible']) .withMessage('Contract type must be small, large or flexible'), - + body('roles') .optional() .isArray() .withMessage('Roles must be an array'), - + body('roles.*') .optional() .isIn(['admin', 'maintenance', 'user']) .withMessage('Invalid role. Allowed: admin, maintenance, user'), - + body('canWorkAlone') .optional() .isBoolean() .withMessage('canWorkAlone must be a boolean'), - + body('isTrainee') .optional() .isBoolean() .withMessage('isTrainee must be a boolean'), - + body('isActive') .optional() .isBoolean() @@ -98,44 +100,44 @@ export const validateEmployeeUpdate = [ .withMessage('First name must be between 1-100 characters') .trim() .escape(), - + body('lastname') .optional() .isLength({ min: 1, max: 100 }) .withMessage('Last name must be between 1-100 characters') .trim() .escape(), - + body('employeeType') .optional() .isIn(['manager', 'personell', 'apprentice', 'guest']) .withMessage('Employee type must be manager, personell, apprentice or guest'), - + body('contractType') .optional() .isIn(['small', 'large', 'flexible']) .withMessage('Contract type must be small, large or flexible'), - + body('roles') .optional() .isArray() .withMessage('Roles must be an array'), - + body('roles.*') .optional() .isIn(['admin', 'maintenance', 'user']) .withMessage('Invalid role. Allowed: admin, maintenance, user'), - + body('canWorkAlone') .optional() .isBoolean() .withMessage('canWorkAlone must be a boolean'), - + body('isTrainee') .optional() .isBoolean() .withMessage('isTrainee must be a boolean'), - + body('isActive') .optional() .isBoolean() @@ -145,14 +147,15 @@ export const validateEmployeeUpdate = [ export const validateChangePassword = [ body('currentPassword') .optional() - .isLength({ min: 6 }) - .withMessage('Current password must be at least 6 characters'), - - body('newPassword') .isLength({ min: 8 }) - .withMessage('New password must be at least 8 characters') - .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/) - .withMessage('New password must contain uppercase, lowercase and number') + .withMessage('Current password must be at least 8 characters'), + + body('password') + .optional() + .isLength({ min: 8 }) + .withMessage('Password must be at least 8 characters') + .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/) + .withMessage('Password must contain uppercase, lowercase, number and special character (@$!%*?&)'), ]; // ===== SHIFT PLAN VALIDATION ===== @@ -162,77 +165,77 @@ export const validateShiftPlan = [ .withMessage('Name must be between 1-200 characters') .trim() .escape(), - + body('description') .optional() .isLength({ max: 1000 }) .withMessage('Description cannot exceed 1000 characters') .trim() .escape(), - + body('startDate') .optional() .isISO8601() .withMessage('Must be a valid date (ISO format)'), - + body('endDate') .optional() .isISO8601() .withMessage('Must be a valid date (ISO format)'), - + body('isTemplate') .optional() .isBoolean() .withMessage('isTemplate must be a boolean'), - + body('status') .optional() .isIn(['draft', 'published', 'archived', 'template']) .withMessage('Status must be draft, published, archived or template'), - + body('timeSlots') .optional() .isArray() .withMessage('Time slots must be an array'), - + body('timeSlots.*.name') .isLength({ min: 1, max: 100 }) .withMessage('Time slot name must be between 1-100 characters') .trim() .escape(), - + body('timeSlots.*.startTime') .matches(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/) .withMessage('Start time must be in HH:MM format'), - + body('timeSlots.*.endTime') .matches(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/) .withMessage('End time must be in HH:MM format'), - + body('timeSlots.*.description') .optional() .isLength({ max: 500 }) .withMessage('Time slot description cannot exceed 500 characters') .trim() .escape(), - + body('shifts') .optional() .isArray() .withMessage('Shifts must be an array'), - + body('shifts.*.dayOfWeek') .isInt({ min: 1, max: 7 }) .withMessage('Day of week must be between 1-7 (Monday-Sunday)'), - + body('shifts.*.timeSlotId') .isUUID() .withMessage('Time slot ID must be a valid UUID'), - + body('shifts.*.requiredEmployees') .isInt({ min: 0 }) .withMessage('Required employees must be a positive integer'), - + body('shifts.*.color') .optional() .isHexColor() @@ -246,34 +249,34 @@ export const validateShiftPlanUpdate = [ .withMessage('Name must be between 1-200 characters') .trim() .escape(), - + body('description') .optional() .isLength({ max: 1000 }) .withMessage('Description cannot exceed 1000 characters') .trim() .escape(), - + body('startDate') .optional() .isISO8601() .withMessage('Must be a valid date (ISO format)'), - + body('endDate') .optional() .isISO8601() .withMessage('Must be a valid date (ISO format)'), - + body('status') .optional() .isIn(['draft', 'published', 'archived', 'template']) .withMessage('Status must be draft, published, archived or template'), - + body('timeSlots') .optional() .isArray() .withMessage('Time slots must be an array'), - + body('shifts') .optional() .isArray() @@ -284,25 +287,25 @@ export const validateCreateFromPreset = [ body('presetName') .isLength({ min: 1 }) .withMessage('Preset name is required') - .isIn(['standardWeek', 'extendedWeek', 'weekendFocused', 'morningOnly', 'eveningOnly', 'ZEBRA_STANDARD']) + .isIn(['GENERAL_STANDARD', 'ZEBRA_STANDARD']) .withMessage('Invalid preset name'), - + body('name') .isLength({ min: 1, max: 200 }) .withMessage('Name must be between 1-200 characters') .trim() .escape(), - + body('startDate') .optional() .isISO8601() .withMessage('Must be a valid date (ISO format)'), - + body('endDate') .optional() .isISO8601() .withMessage('Must be a valid date (ISO format)'), - + body('isTemplate') .optional() .isBoolean() @@ -314,11 +317,11 @@ export const validateScheduledShiftUpdate = [ body('assignedEmployees') .isArray() .withMessage('assignedEmployees must be an array'), - + body('assignedEmployees.*') .isUUID() .withMessage('Each assigned employee must be a valid UUID'), - + body('requiredEmployees') .optional() .isInt({ min: 0 }) @@ -332,18 +335,19 @@ export const validateSetupAdmin = [ .withMessage('First name must be between 1-100 characters') .trim() .escape(), - + body('lastname') .isLength({ min: 1, max: 100 }) .withMessage('Last name must be between 1-100 characters') .trim() .escape(), - + body('password') + .optional() .isLength({ min: 8 }) .withMessage('Password must be at least 8 characters') - .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/) - .withMessage('Password must contain uppercase, lowercase and number') + .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/) + .withMessage('Password must contain uppercase, lowercase, number and special character (@$!%*?&)'), ]; // ===== SCHEDULING VALIDATION ===== @@ -351,23 +355,23 @@ export const validateSchedulingRequest = [ body('shiftPlan') .isObject() .withMessage('Shift plan is required'), - + body('shiftPlan.id') .isUUID() .withMessage('Shift plan ID must be a valid UUID'), - + body('employees') .isArray({ min: 1 }) .withMessage('At least one employee is required'), - + body('employees.*.id') .isUUID() .withMessage('Each employee must have a valid UUID'), - + body('availabilities') .isArray() .withMessage('Availabilities must be an array'), - + body('constraints') .optional() .isArray() @@ -379,19 +383,19 @@ export const validateAvailabilities = [ body('planId') .isUUID() .withMessage('Plan ID must be a valid UUID'), - + body('availabilities') .isArray() .withMessage('Availabilities must be an array'), - + body('availabilities.*.shiftId') .isUUID() .withMessage('Each shift ID must be a valid UUID'), - + body('availabilities.*.preferenceLevel') .isInt({ min: 0, max: 2 }) .withMessage('Preference level must be 0 (unavailable), 1 (available), or 2 (preferred)'), - + body('availabilities.*.notes') .optional() .isLength({ max: 500 }) @@ -424,12 +428,12 @@ export const validatePagination = [ .optional() .isInt({ min: 1 }) .withMessage('Page must be a positive integer'), - + query('limit') .optional() .isInt({ min: 1, max: 100 }) .withMessage('Limit must be between 1-100'), - + query('includeInactive') .optional() .isBoolean() @@ -439,19 +443,19 @@ export const validatePagination = [ // ===== MIDDLEWARE TO CHECK VALIDATION RESULTS ===== export const handleValidationErrors = (req: Request, res: Response, next: NextFunction) => { const errors = validationResult(req); - + if (!errors.isEmpty()) { const errorMessages = errors.array().map(error => ({ field: error.type === 'field' ? error.path : error.type, message: error.msg, value: error.msg })); - + return res.status(400).json({ error: 'Validation failed', details: errorMessages }); } - + next(); }; \ No newline at end of file