mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-11-30 22:45:46 +01:00
added express payload validation
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# .env.production example
|
# .env.production example
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
JWT_SECRET=your-super-secure-minimum-32-character-secret-key-here
|
JWT_SECRET=your-secret-key
|
||||||
DATABASE_PATH=/app/data/production.db
|
DATABASE_PATH=/app/data/production.db
|
||||||
@@ -19,7 +19,10 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"sqlite3": "^5.1.6",
|
"sqlite3": "^5.1.6",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0",
|
||||||
|
"express-rate-limit": "8.1.0",
|
||||||
|
"helmet": "8.1.0",
|
||||||
|
"express-validator": "7.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
|
|||||||
16
backend/src/middleware/rateLimit.ts
Normal file
16
backend/src/middleware/rateLimit.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import rateLimit from 'express-rate-limit';
|
||||||
|
|
||||||
|
export const authLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 5, // Limit each IP to 5 login requests per windowMs
|
||||||
|
message: { error: 'Zu viele Login-Versuche, bitte versuchen Sie es später erneut' },
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 100, // Limit each IP to 100 requests per windowMs
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
});
|
||||||
457
backend/src/middleware/validation.ts
Normal file
457
backend/src/middleware/validation.ts
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
import { body, validationResult, param, query } from 'express-validator';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
// ===== AUTH VALIDATION =====
|
||||||
|
export const validateLogin = [
|
||||||
|
body('email')
|
||||||
|
.isEmail()
|
||||||
|
.withMessage('Must be a valid email')
|
||||||
|
.normalizeEmail(),
|
||||||
|
|
||||||
|
body('password')
|
||||||
|
.isLength({ min: 6 })
|
||||||
|
.withMessage('Password must be at least 6 characters')
|
||||||
|
.trim()
|
||||||
|
.escape()
|
||||||
|
];
|
||||||
|
|
||||||
|
export const validateRegister = [
|
||||||
|
body('firstname')
|
||||||
|
.isLength({ min: 1, max: 100 })
|
||||||
|
.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')
|
||||||
|
.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')
|
||||||
|
];
|
||||||
|
|
||||||
|
// ===== EMPLOYEE VALIDATION =====
|
||||||
|
export const validateEmployee = [
|
||||||
|
body('firstname')
|
||||||
|
.isLength({ min: 1, max: 100 })
|
||||||
|
.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'),
|
||||||
|
|
||||||
|
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()
|
||||||
|
.withMessage('isActive must be a boolean')
|
||||||
|
];
|
||||||
|
|
||||||
|
export const validateEmployeeUpdate = [
|
||||||
|
body('firstname')
|
||||||
|
.optional()
|
||||||
|
.isLength({ min: 1, max: 100 })
|
||||||
|
.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()
|
||||||
|
.withMessage('isActive must be a boolean')
|
||||||
|
];
|
||||||
|
|
||||||
|
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')
|
||||||
|
];
|
||||||
|
|
||||||
|
// ===== SHIFT PLAN VALIDATION =====
|
||||||
|
export const validateShiftPlan = [
|
||||||
|
body('name')
|
||||||
|
.isLength({ min: 1, max: 200 })
|
||||||
|
.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()
|
||||||
|
.withMessage('Color must be a valid hex color')
|
||||||
|
];
|
||||||
|
|
||||||
|
export const validateShiftPlanUpdate = [
|
||||||
|
body('name')
|
||||||
|
.optional()
|
||||||
|
.isLength({ min: 1, max: 200 })
|
||||||
|
.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()
|
||||||
|
.withMessage('Shifts must be an array')
|
||||||
|
];
|
||||||
|
|
||||||
|
export const validateCreateFromPreset = [
|
||||||
|
body('presetName')
|
||||||
|
.isLength({ min: 1 })
|
||||||
|
.withMessage('Preset name is required')
|
||||||
|
.isIn(['standardWeek', 'extendedWeek', 'weekendFocused', 'morningOnly', 'eveningOnly'])
|
||||||
|
.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()
|
||||||
|
.withMessage('isTemplate must be a boolean')
|
||||||
|
];
|
||||||
|
|
||||||
|
// ===== SCHEDULED SHIFTS VALIDATION =====
|
||||||
|
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 })
|
||||||
|
.withMessage('Required employees must be a positive integer')
|
||||||
|
];
|
||||||
|
|
||||||
|
// ===== SETUP VALIDATION =====
|
||||||
|
export const validateSetupAdmin = [
|
||||||
|
body('firstname')
|
||||||
|
.isLength({ min: 1, max: 100 })
|
||||||
|
.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')
|
||||||
|
.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')
|
||||||
|
];
|
||||||
|
|
||||||
|
// ===== SCHEDULING VALIDATION =====
|
||||||
|
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()
|
||||||
|
.withMessage('Constraints must be an array')
|
||||||
|
];
|
||||||
|
|
||||||
|
// ===== AVAILABILITY VALIDATION =====
|
||||||
|
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 })
|
||||||
|
.withMessage('Notes cannot exceed 500 characters')
|
||||||
|
.trim()
|
||||||
|
.escape()
|
||||||
|
];
|
||||||
|
|
||||||
|
// ===== COMMON VALIDATORS =====
|
||||||
|
export const validateId = [
|
||||||
|
param('id')
|
||||||
|
.isUUID()
|
||||||
|
.withMessage('Must be a valid UUID')
|
||||||
|
];
|
||||||
|
|
||||||
|
export const validateEmployeeId = [
|
||||||
|
param('employeeId')
|
||||||
|
.isUUID()
|
||||||
|
.withMessage('Must be a valid UUID')
|
||||||
|
];
|
||||||
|
|
||||||
|
export const validatePlanId = [
|
||||||
|
param('planId')
|
||||||
|
.isUUID()
|
||||||
|
.withMessage('Must be a valid UUID')
|
||||||
|
];
|
||||||
|
|
||||||
|
export const validatePagination = [
|
||||||
|
query('page')
|
||||||
|
.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()
|
||||||
|
.withMessage('includeInactive must be a boolean')
|
||||||
|
];
|
||||||
|
|
||||||
|
// ===== 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
|
||||||
|
}));
|
||||||
|
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Validation failed',
|
||||||
|
details: errorMessages
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
@@ -8,12 +8,13 @@ import {
|
|||||||
validateToken
|
validateToken
|
||||||
} from '../controllers/authController.js';
|
} from '../controllers/authController.js';
|
||||||
import { authMiddleware } from '../middleware/auth.js';
|
import { authMiddleware } from '../middleware/auth.js';
|
||||||
|
import { validateLogin, validateRegister, handleValidationErrors } from '../middleware/validation.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Public routes
|
// Public routes
|
||||||
router.post('/login', login);
|
router.post('/login', validateLogin, handleValidationErrors, login);
|
||||||
router.post('/register', register);
|
router.post('/register', validateRegister, handleValidationErrors, register);
|
||||||
router.get('/validate', validateToken);
|
router.get('/validate', validateToken);
|
||||||
|
|
||||||
// Protected routes (require authentication)
|
// Protected routes (require authentication)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// backend/src/routes/employees.ts
|
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { authMiddleware, requireRole } from '../middleware/auth.js';
|
import { authMiddleware, requireRole } from '../middleware/auth.js';
|
||||||
import {
|
import {
|
||||||
@@ -12,6 +11,16 @@ import {
|
|||||||
changePassword,
|
changePassword,
|
||||||
updateLastLogin
|
updateLastLogin
|
||||||
} from '../controllers/employeeController.js';
|
} from '../controllers/employeeController.js';
|
||||||
|
import {
|
||||||
|
handleValidationErrors,
|
||||||
|
validateEmployee,
|
||||||
|
validateEmployeeUpdate,
|
||||||
|
validateChangePassword,
|
||||||
|
validateId,
|
||||||
|
validateEmployeeId,
|
||||||
|
validateAvailabilities,
|
||||||
|
validatePagination
|
||||||
|
} from '../middleware/validation.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -19,16 +28,18 @@ const router = express.Router();
|
|||||||
router.use(authMiddleware);
|
router.use(authMiddleware);
|
||||||
|
|
||||||
// Employee CRUD Routes
|
// Employee CRUD Routes
|
||||||
router.get('/', authMiddleware, getEmployees);
|
router.get('/', validatePagination, handleValidationErrors, getEmployees);
|
||||||
router.get('/:id', requireRole(['admin', 'maintenance']), getEmployee);
|
router.get('/:id', validateId, handleValidationErrors, requireRole(['admin', 'maintenance']), getEmployee);
|
||||||
router.post('/', requireRole(['admin']), createEmployee);
|
router.post('/', validateEmployee, handleValidationErrors, requireRole(['admin']), createEmployee);
|
||||||
router.put('/:id', requireRole(['admin', 'maintenance']), updateEmployee);
|
router.put('/:id', validateId, validateEmployeeUpdate, handleValidationErrors, requireRole(['admin', 'maintenance']), updateEmployee);
|
||||||
router.delete('/:id', requireRole(['admin']), deleteEmployee);
|
router.delete('/:id', validateId, handleValidationErrors, requireRole(['admin']), deleteEmployee);
|
||||||
router.put('/:id/password', authMiddleware, changePassword);
|
|
||||||
router.put('/:id/last-login', authMiddleware, updateLastLogin);
|
// Password & Login Routes
|
||||||
|
router.put('/:id/password', validateId, validateChangePassword, handleValidationErrors, changePassword);
|
||||||
|
router.put('/:id/last-login', validateId, handleValidationErrors, updateLastLogin);
|
||||||
|
|
||||||
// Availability Routes
|
// Availability Routes
|
||||||
router.get('/:employeeId/availabilities', authMiddleware, getAvailabilities);
|
router.get('/:employeeId/availabilities', validateEmployeeId, handleValidationErrors, getAvailabilities);
|
||||||
router.put('/:employeeId/availabilities', authMiddleware, updateAvailabilities);
|
router.put('/:employeeId/availabilities', validateEmployeeId, validateAvailabilities, handleValidationErrors, updateAvailabilities);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
// backend/src/routes/scheduledShifts.ts
|
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { authMiddleware, requireRole } from '../middleware/auth.js';
|
import { authMiddleware, requireRole } from '../middleware/auth.js';
|
||||||
import {
|
import {
|
||||||
@@ -8,23 +7,21 @@ import {
|
|||||||
getScheduledShiftsFromPlan,
|
getScheduledShiftsFromPlan,
|
||||||
updateScheduledShift
|
updateScheduledShift
|
||||||
} from '../controllers/shiftPlanController.js';
|
} from '../controllers/shiftPlanController.js';
|
||||||
|
import {
|
||||||
|
validateId,
|
||||||
|
validatePlanId,
|
||||||
|
validateScheduledShiftUpdate,
|
||||||
|
handleValidationErrors
|
||||||
|
} from '../middleware/validation.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.use(authMiddleware);
|
router.use(authMiddleware);
|
||||||
|
|
||||||
|
router.post('/:id/generate-shifts', validateId, handleValidationErrors, requireRole(['admin', 'maintenance']), generateScheduledShiftsForPlan);
|
||||||
router.post('/:id/generate-shifts', requireRole(['admin', 'maintenance']), generateScheduledShiftsForPlan);
|
router.post('/:id/regenerate-shifts', validateId, handleValidationErrors, requireRole(['admin', 'maintenance']), regenerateScheduledShifts);
|
||||||
|
router.get('/plan/:planId', validatePlanId, handleValidationErrors, getScheduledShiftsFromPlan);
|
||||||
router.post('/:id/regenerate-shifts', requireRole(['admin', 'maintenance']), regenerateScheduledShifts);
|
router.get('/:id', validateId, handleValidationErrors, getScheduledShift);
|
||||||
|
router.put('/:id', validateId, validateScheduledShiftUpdate, handleValidationErrors, updateScheduledShift);
|
||||||
// GET all scheduled shifts for a plan
|
|
||||||
router.get('/plan/:planId', authMiddleware, getScheduledShiftsFromPlan);
|
|
||||||
|
|
||||||
// GET specific scheduled shift
|
|
||||||
router.get('/:id', authMiddleware, getScheduledShift);
|
|
||||||
|
|
||||||
// UPDATE scheduled shift
|
|
||||||
router.put('/:id', authMiddleware, updateScheduledShift);
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { SchedulingService } from '../services/SchedulingService.js';
|
import { SchedulingService } from '../services/SchedulingService.js';
|
||||||
|
import { validateSchedulingRequest, handleValidationErrors } from '../middleware/validation.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.post('/generate-schedule', async (req, res) => {
|
router.post('/generate-schedule', validateSchedulingRequest, handleValidationErrors, async (req: express.Request, res: express.Response) => {
|
||||||
try {
|
try {
|
||||||
const { shiftPlan, employees, availabilities, constraints } = req.body;
|
const { shiftPlan, employees, availabilities, constraints } = req.body;
|
||||||
|
|
||||||
@@ -14,18 +15,6 @@ router.post('/generate-schedule', async (req, res) => {
|
|||||||
constraintCount: constraints?.length
|
constraintCount: constraints?.length
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validate required data
|
|
||||||
if (!shiftPlan || !employees || !availabilities) {
|
|
||||||
return res.status(400).json({
|
|
||||||
error: 'Missing required data',
|
|
||||||
details: {
|
|
||||||
shiftPlan: !!shiftPlan,
|
|
||||||
employees: !!employees,
|
|
||||||
availabilities: !!availabilities
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const scheduler = new SchedulingService();
|
const scheduler = new SchedulingService();
|
||||||
const result = await scheduler.generateOptimalSchedule({
|
const result = await scheduler.generateOptimalSchedule({
|
||||||
shiftPlan,
|
shiftPlan,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// backend/src/routes/setup.ts
|
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { checkSetupStatus, setupAdmin } from '../controllers/setupController.js';
|
import { checkSetupStatus, setupAdmin } from '../controllers/setupController.js';
|
||||||
|
import { validateSetupAdmin, handleValidationErrors } from '../middleware/validation.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get('/status', checkSetupStatus);
|
router.get('/status', checkSetupStatus);
|
||||||
router.post('/admin', setupAdmin);
|
router.post('/admin', validateSetupAdmin, handleValidationErrors, setupAdmin);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
// backend/src/routes/shiftPlans.ts
|
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { authMiddleware, requireRole } from '../middleware/auth.js';
|
import { authMiddleware, requireRole } from '../middleware/auth.js';
|
||||||
import {
|
import {
|
||||||
@@ -10,32 +9,25 @@ import {
|
|||||||
createFromPreset,
|
createFromPreset,
|
||||||
clearAssignments
|
clearAssignments
|
||||||
} from '../controllers/shiftPlanController.js';
|
} from '../controllers/shiftPlanController.js';
|
||||||
|
import {
|
||||||
|
validateShiftPlan,
|
||||||
|
validateShiftPlanUpdate,
|
||||||
|
validateCreateFromPreset,
|
||||||
|
handleValidationErrors,
|
||||||
|
validateId
|
||||||
|
} from '../middleware/validation.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.use(authMiddleware);
|
router.use(authMiddleware);
|
||||||
|
|
||||||
// Combined routes for both shift plans and templates
|
// Combined routes for both shift plans and templates
|
||||||
|
router.get('/', getShiftPlans);
|
||||||
// GET all shift plans (including templates)
|
router.get('/:id', validateId, handleValidationErrors, getShiftPlan);
|
||||||
router.get('/' , authMiddleware, getShiftPlans);
|
router.post('/', validateShiftPlan, handleValidationErrors, requireRole(['admin', 'maintenance']), createShiftPlan);
|
||||||
|
router.post('/from-preset', validateCreateFromPreset, handleValidationErrors, requireRole(['admin', 'maintenance']), createFromPreset);
|
||||||
// GET specific shift plan or template
|
router.put('/:id', validateId, validateShiftPlanUpdate, handleValidationErrors, requireRole(['admin', 'maintenance']), updateShiftPlan);
|
||||||
router.get('/:id', authMiddleware, getShiftPlan);
|
router.delete('/:id', validateId, handleValidationErrors, requireRole(['admin', 'maintenance']), deleteShiftPlan);
|
||||||
|
router.post('/:id/clear-assignments', validateId, handleValidationErrors, requireRole(['admin', 'maintenance']), clearAssignments);
|
||||||
// POST create new shift plan
|
|
||||||
router.post('/', requireRole(['admin', 'maintenance']), createShiftPlan);
|
|
||||||
|
|
||||||
// POST create new plan from preset
|
|
||||||
router.post('/from-preset', requireRole(['admin', 'maintenance']), createFromPreset);
|
|
||||||
|
|
||||||
// PUT update shift plan or template
|
|
||||||
router.put('/:id', requireRole(['admin', 'maintenance']), updateShiftPlan);
|
|
||||||
|
|
||||||
// DELETE shift plan or template
|
|
||||||
router.delete('/:id', requireRole(['admin', 'maintenance']), deleteShiftPlan);
|
|
||||||
|
|
||||||
// POST clear assignments and reset to draft
|
|
||||||
router.post('/:id/clear-assignments', requireRole(['admin', 'maintenance']), clearAssignments);
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
// backend/src/server.ts
|
// backend/src/server.ts
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { initializeDatabase } from './scripts/initializeDatabase.js';
|
import { initializeDatabase } from './scripts/initializeDatabase.js';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import helmet from 'helmet';
|
||||||
|
|
||||||
// Route imports
|
// Route imports
|
||||||
import authRoutes from './routes/auth.js';
|
import authRoutes from './routes/auth.js';
|
||||||
@@ -12,16 +12,50 @@ import shiftPlanRoutes from './routes/shiftPlans.js';
|
|||||||
import setupRoutes from './routes/setup.js';
|
import setupRoutes from './routes/setup.js';
|
||||||
import scheduledShifts from './routes/scheduledShifts.js';
|
import scheduledShifts from './routes/scheduledShifts.js';
|
||||||
import schedulingRoutes from './routes/scheduling.js';
|
import schedulingRoutes from './routes/scheduling.js';
|
||||||
|
import { authLimiter, apiLimiter } from './middleware/rateLimit.js';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = 3002;
|
const PORT = 3002;
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
console.info('Checking for JWT_SECRET');
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET;
|
||||||
|
if (!JWT_SECRET || JWT_SECRET === 'your-secret-key') {
|
||||||
|
console.error('❌ Fatal: JWT_SECRET not set or using default value');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Security headers
|
||||||
|
app.use(helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||||
|
imgSrc: ["'self'", "data:", "https:"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
crossOriginEmbedderPolicy: false // Required for Vite dev
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Additional security headers
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||||
|
res.setHeader('X-Frame-Options', 'DENY');
|
||||||
|
res.setHeader('X-XSS-Protection', '1; mode=block');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// API Routes
|
// API Routes
|
||||||
|
app.use('/api/', apiLimiter);
|
||||||
|
|
||||||
app.use('/api/setup', setupRoutes);
|
app.use('/api/setup', setupRoutes);
|
||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authLimiter, authRoutes);
|
||||||
app.use('/api/employees', employeeRoutes);
|
app.use('/api/employees', employeeRoutes);
|
||||||
app.use('/api/shift-plans', shiftPlanRoutes);
|
app.use('/api/shift-plans', shiftPlanRoutes);
|
||||||
app.use('/api/scheduled-shifts', scheduledShifts);
|
app.use('/api/scheduled-shifts', scheduledShifts);
|
||||||
|
|||||||
1649
package-lock.json
generated
1649
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user