added express payload validation

This commit is contained in:
2025-10-28 18:58:58 +01:00
parent a838ba44e8
commit 5f8a6bef31
12 changed files with 1490 additions and 793 deletions

View File

@@ -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

View File

@@ -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",

View 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,
});

View 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();
};

View File

@@ -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)

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

File diff suppressed because it is too large Load Diff