password requirements more strict

This commit is contained in:
2025-10-29 11:05:05 +01:00
parent 0363505126
commit 86166048e8

View File

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