mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 06:55:45 +01:00
added express payload validation
This commit is contained in:
@@ -19,7 +19,10 @@
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"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": {
|
||||
"@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
|
||||
} from '../controllers/authController.js';
|
||||
import { authMiddleware } from '../middleware/auth.js';
|
||||
import { validateLogin, validateRegister, handleValidationErrors } from '../middleware/validation.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Public routes
|
||||
router.post('/login', login);
|
||||
router.post('/register', register);
|
||||
router.post('/login', validateLogin, handleValidationErrors, login);
|
||||
router.post('/register', validateRegister, handleValidationErrors, register);
|
||||
router.get('/validate', validateToken);
|
||||
|
||||
// Protected routes (require authentication)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// backend/src/routes/employees.ts
|
||||
import express from 'express';
|
||||
import { authMiddleware, requireRole } from '../middleware/auth.js';
|
||||
import {
|
||||
@@ -12,6 +11,16 @@ import {
|
||||
changePassword,
|
||||
updateLastLogin
|
||||
} from '../controllers/employeeController.js';
|
||||
import {
|
||||
handleValidationErrors,
|
||||
validateEmployee,
|
||||
validateEmployeeUpdate,
|
||||
validateChangePassword,
|
||||
validateId,
|
||||
validateEmployeeId,
|
||||
validateAvailabilities,
|
||||
validatePagination
|
||||
} from '../middleware/validation.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -19,16 +28,18 @@ const router = express.Router();
|
||||
router.use(authMiddleware);
|
||||
|
||||
// Employee CRUD Routes
|
||||
router.get('/', authMiddleware, getEmployees);
|
||||
router.get('/:id', requireRole(['admin', 'maintenance']), getEmployee);
|
||||
router.post('/', requireRole(['admin']), createEmployee);
|
||||
router.put('/:id', requireRole(['admin', 'maintenance']), updateEmployee);
|
||||
router.delete('/:id', requireRole(['admin']), deleteEmployee);
|
||||
router.put('/:id/password', authMiddleware, changePassword);
|
||||
router.put('/:id/last-login', authMiddleware, updateLastLogin);
|
||||
router.get('/', validatePagination, handleValidationErrors, getEmployees);
|
||||
router.get('/:id', validateId, handleValidationErrors, requireRole(['admin', 'maintenance']), getEmployee);
|
||||
router.post('/', validateEmployee, handleValidationErrors, requireRole(['admin']), createEmployee);
|
||||
router.put('/:id', validateId, validateEmployeeUpdate, handleValidationErrors, requireRole(['admin', 'maintenance']), updateEmployee);
|
||||
router.delete('/:id', validateId, handleValidationErrors, requireRole(['admin']), deleteEmployee);
|
||||
|
||||
// Password & Login Routes
|
||||
router.put('/:id/password', validateId, validateChangePassword, handleValidationErrors, changePassword);
|
||||
router.put('/:id/last-login', validateId, handleValidationErrors, updateLastLogin);
|
||||
|
||||
// Availability Routes
|
||||
router.get('/:employeeId/availabilities', authMiddleware, getAvailabilities);
|
||||
router.put('/:employeeId/availabilities', authMiddleware, updateAvailabilities);
|
||||
router.get('/:employeeId/availabilities', validateEmployeeId, handleValidationErrors, getAvailabilities);
|
||||
router.put('/:employeeId/availabilities', validateEmployeeId, validateAvailabilities, handleValidationErrors, updateAvailabilities);
|
||||
|
||||
export default router;
|
||||
@@ -1,4 +1,3 @@
|
||||
// backend/src/routes/scheduledShifts.ts
|
||||
import express from 'express';
|
||||
import { authMiddleware, requireRole } from '../middleware/auth.js';
|
||||
import {
|
||||
@@ -8,23 +7,21 @@ import {
|
||||
getScheduledShiftsFromPlan,
|
||||
updateScheduledShift
|
||||
} from '../controllers/shiftPlanController.js';
|
||||
import {
|
||||
validateId,
|
||||
validatePlanId,
|
||||
validateScheduledShiftUpdate,
|
||||
handleValidationErrors
|
||||
} from '../middleware/validation.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
|
||||
router.post('/:id/generate-shifts', requireRole(['admin', 'maintenance']), generateScheduledShiftsForPlan);
|
||||
|
||||
router.post('/:id/regenerate-shifts', requireRole(['admin', 'maintenance']), regenerateScheduledShifts);
|
||||
|
||||
// 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);
|
||||
router.post('/:id/generate-shifts', validateId, handleValidationErrors, requireRole(['admin', 'maintenance']), generateScheduledShiftsForPlan);
|
||||
router.post('/:id/regenerate-shifts', validateId, handleValidationErrors, requireRole(['admin', 'maintenance']), regenerateScheduledShifts);
|
||||
router.get('/plan/:planId', validatePlanId, handleValidationErrors, getScheduledShiftsFromPlan);
|
||||
router.get('/:id', validateId, handleValidationErrors, getScheduledShift);
|
||||
router.put('/:id', validateId, validateScheduledShiftUpdate, handleValidationErrors, updateScheduledShift);
|
||||
|
||||
export default router;
|
||||
@@ -1,9 +1,10 @@
|
||||
import express from 'express';
|
||||
import { SchedulingService } from '../services/SchedulingService.js';
|
||||
import { validateSchedulingRequest, handleValidationErrors } from '../middleware/validation.js';
|
||||
|
||||
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 {
|
||||
const { shiftPlan, employees, availabilities, constraints } = req.body;
|
||||
|
||||
@@ -14,18 +15,6 @@ router.post('/generate-schedule', async (req, res) => {
|
||||
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 result = await scheduler.generateOptimalSchedule({
|
||||
shiftPlan,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// backend/src/routes/setup.ts
|
||||
import express from 'express';
|
||||
import { checkSetupStatus, setupAdmin } from '../controllers/setupController.js';
|
||||
import { validateSetupAdmin, handleValidationErrors } from '../middleware/validation.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/status', checkSetupStatus);
|
||||
router.post('/admin', setupAdmin);
|
||||
router.post('/admin', validateSetupAdmin, handleValidationErrors, setupAdmin);
|
||||
|
||||
export default router;
|
||||
@@ -1,4 +1,3 @@
|
||||
// backend/src/routes/shiftPlans.ts
|
||||
import express from 'express';
|
||||
import { authMiddleware, requireRole } from '../middleware/auth.js';
|
||||
import {
|
||||
@@ -10,32 +9,25 @@ import {
|
||||
createFromPreset,
|
||||
clearAssignments
|
||||
} from '../controllers/shiftPlanController.js';
|
||||
import {
|
||||
validateShiftPlan,
|
||||
validateShiftPlanUpdate,
|
||||
validateCreateFromPreset,
|
||||
handleValidationErrors,
|
||||
validateId
|
||||
} from '../middleware/validation.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
// Combined routes for both shift plans and templates
|
||||
|
||||
// GET all shift plans (including templates)
|
||||
router.get('/' , authMiddleware, getShiftPlans);
|
||||
|
||||
// GET specific shift plan or template
|
||||
router.get('/:id', authMiddleware, getShiftPlan);
|
||||
|
||||
// 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);
|
||||
router.get('/', getShiftPlans);
|
||||
router.get('/:id', validateId, handleValidationErrors, getShiftPlan);
|
||||
router.post('/', validateShiftPlan, handleValidationErrors, requireRole(['admin', 'maintenance']), createShiftPlan);
|
||||
router.post('/from-preset', validateCreateFromPreset, handleValidationErrors, requireRole(['admin', 'maintenance']), createFromPreset);
|
||||
router.put('/:id', validateId, validateShiftPlanUpdate, handleValidationErrors, requireRole(['admin', 'maintenance']), updateShiftPlan);
|
||||
router.delete('/:id', validateId, handleValidationErrors, requireRole(['admin', 'maintenance']), deleteShiftPlan);
|
||||
router.post('/:id/clear-assignments', validateId, handleValidationErrors, requireRole(['admin', 'maintenance']), clearAssignments);
|
||||
|
||||
export default router;
|
||||
@@ -1,9 +1,9 @@
|
||||
// backend/src/server.ts
|
||||
import express from 'express';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { initializeDatabase } from './scripts/initializeDatabase.js';
|
||||
import fs from 'fs';
|
||||
import helmet from 'helmet';
|
||||
|
||||
// Route imports
|
||||
import authRoutes from './routes/auth.js';
|
||||
@@ -12,16 +12,50 @@ import shiftPlanRoutes from './routes/shiftPlans.js';
|
||||
import setupRoutes from './routes/setup.js';
|
||||
import scheduledShifts from './routes/scheduledShifts.js';
|
||||
import schedulingRoutes from './routes/scheduling.js';
|
||||
import { authLimiter, apiLimiter } from './middleware/rateLimit.js';
|
||||
|
||||
const app = express();
|
||||
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
|
||||
app.use(express.json());
|
||||
|
||||
// API Routes
|
||||
app.use('/api/', apiLimiter);
|
||||
|
||||
app.use('/api/setup', setupRoutes);
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/auth', authLimiter, authRoutes);
|
||||
app.use('/api/employees', employeeRoutes);
|
||||
app.use('/api/shift-plans', shiftPlanRoutes);
|
||||
app.use('/api/scheduled-shifts', scheduledShifts);
|
||||
|
||||
Reference in New Issue
Block a user