mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 15:05:45 +01:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fbd0f03eb2 | |||
| 86166048e8 | |||
| 0363505126 | |||
| 1231c8362f |
@@ -7,12 +7,13 @@ export const validateLogin = [
|
||||
.isEmail()
|
||||
.withMessage('Must be a valid email')
|
||||
.normalizeEmail(),
|
||||
|
||||
|
||||
body('password')
|
||||
.isLength({ min: 6 })
|
||||
.withMessage('Password must be at least 6 characters')
|
||||
.trim()
|
||||
.escape()
|
||||
.optional()
|
||||
.isLength({ min: 8 })
|
||||
.withMessage('Password must be at least 8 characters')
|
||||
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?])/)
|
||||
.withMessage('Password must contain uppercase, lowercase, number and special character'),
|
||||
];
|
||||
|
||||
export const validateRegister = [
|
||||
@@ -21,18 +22,19 @@ export const validateRegister = [
|
||||
.withMessage('First name must be between 1-100 characters')
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
|
||||
body('lastname')
|
||||
.isLength({ min: 1, max: 100 })
|
||||
.withMessage('Last name must be between 1-100 characters')
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
|
||||
body('password')
|
||||
.optional()
|
||||
.isLength({ min: 8 })
|
||||
.withMessage('Password must be at least 8 characters')
|
||||
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
|
||||
.withMessage('Password must contain uppercase, lowercase and number')
|
||||
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?])/)
|
||||
.withMessage('Password must contain uppercase, lowercase, number and special character'),
|
||||
];
|
||||
|
||||
// ===== EMPLOYEE VALIDATION =====
|
||||
@@ -42,49 +44,49 @@ export const validateEmployee = [
|
||||
.withMessage('First name must be between 1-100 characters')
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
|
||||
body('lastname')
|
||||
.isLength({ min: 1, max: 100 })
|
||||
.withMessage('Last name must be between 1-100 characters')
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
|
||||
body('password')
|
||||
.optional()
|
||||
.isLength({ min: 8 })
|
||||
.withMessage('Password must be at least 8 characters')
|
||||
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
|
||||
.withMessage('Password must contain uppercase, lowercase and number'),
|
||||
|
||||
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?])/)
|
||||
.withMessage('Password must contain uppercase, lowercase, number and special character'),
|
||||
|
||||
body('employeeType')
|
||||
.isIn(['manager', 'personell', 'apprentice', 'guest'])
|
||||
.withMessage('Employee type must be manager, personell, apprentice or guest'),
|
||||
|
||||
|
||||
body('contractType')
|
||||
.optional()
|
||||
.isIn(['small', 'large', 'flexible'])
|
||||
.withMessage('Contract type must be small, large or flexible'),
|
||||
|
||||
|
||||
body('roles')
|
||||
.optional()
|
||||
.isArray()
|
||||
.withMessage('Roles must be an array'),
|
||||
|
||||
|
||||
body('roles.*')
|
||||
.optional()
|
||||
.isIn(['admin', 'maintenance', 'user'])
|
||||
.withMessage('Invalid role. Allowed: admin, maintenance, user'),
|
||||
|
||||
|
||||
body('canWorkAlone')
|
||||
.optional()
|
||||
.isBoolean()
|
||||
.withMessage('canWorkAlone must be a boolean'),
|
||||
|
||||
|
||||
body('isTrainee')
|
||||
.optional()
|
||||
.isBoolean()
|
||||
.withMessage('isTrainee must be a boolean'),
|
||||
|
||||
|
||||
body('isActive')
|
||||
.optional()
|
||||
.isBoolean()
|
||||
@@ -98,44 +100,44 @@ export const validateEmployeeUpdate = [
|
||||
.withMessage('First name must be between 1-100 characters')
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
|
||||
body('lastname')
|
||||
.optional()
|
||||
.isLength({ min: 1, max: 100 })
|
||||
.withMessage('Last name must be between 1-100 characters')
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
|
||||
body('employeeType')
|
||||
.optional()
|
||||
.isIn(['manager', 'personell', 'apprentice', 'guest'])
|
||||
.withMessage('Employee type must be manager, personell, apprentice or guest'),
|
||||
|
||||
|
||||
body('contractType')
|
||||
.optional()
|
||||
.isIn(['small', 'large', 'flexible'])
|
||||
.withMessage('Contract type must be small, large or flexible'),
|
||||
|
||||
|
||||
body('roles')
|
||||
.optional()
|
||||
.isArray()
|
||||
.withMessage('Roles must be an array'),
|
||||
|
||||
|
||||
body('roles.*')
|
||||
.optional()
|
||||
.isIn(['admin', 'maintenance', 'user'])
|
||||
.withMessage('Invalid role. Allowed: admin, maintenance, user'),
|
||||
|
||||
|
||||
body('canWorkAlone')
|
||||
.optional()
|
||||
.isBoolean()
|
||||
.withMessage('canWorkAlone must be a boolean'),
|
||||
|
||||
|
||||
body('isTrainee')
|
||||
.optional()
|
||||
.isBoolean()
|
||||
.withMessage('isTrainee must be a boolean'),
|
||||
|
||||
|
||||
body('isActive')
|
||||
.optional()
|
||||
.isBoolean()
|
||||
@@ -145,14 +147,15 @@ export const validateEmployeeUpdate = [
|
||||
export const validateChangePassword = [
|
||||
body('currentPassword')
|
||||
.optional()
|
||||
.isLength({ min: 6 })
|
||||
.withMessage('Current password must be at least 6 characters'),
|
||||
|
||||
body('newPassword')
|
||||
.isLength({ min: 8 })
|
||||
.withMessage('New password must be at least 8 characters')
|
||||
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
|
||||
.withMessage('New password must contain uppercase, lowercase and number')
|
||||
.withMessage('Current password must be at least 8 characters'),
|
||||
|
||||
body('password')
|
||||
.optional()
|
||||
.isLength({ min: 8 })
|
||||
.withMessage('Password must be at least 8 characters')
|
||||
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?])/)
|
||||
.withMessage('Password must contain uppercase, lowercase, number and special character'),
|
||||
];
|
||||
|
||||
// ===== SHIFT PLAN VALIDATION =====
|
||||
@@ -162,77 +165,77 @@ export const validateShiftPlan = [
|
||||
.withMessage('Name must be between 1-200 characters')
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
|
||||
body('description')
|
||||
.optional()
|
||||
.isLength({ max: 1000 })
|
||||
.withMessage('Description cannot exceed 1000 characters')
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
|
||||
body('startDate')
|
||||
.optional()
|
||||
.isISO8601()
|
||||
.withMessage('Must be a valid date (ISO format)'),
|
||||
|
||||
|
||||
body('endDate')
|
||||
.optional()
|
||||
.isISO8601()
|
||||
.withMessage('Must be a valid date (ISO format)'),
|
||||
|
||||
|
||||
body('isTemplate')
|
||||
.optional()
|
||||
.isBoolean()
|
||||
.withMessage('isTemplate must be a boolean'),
|
||||
|
||||
|
||||
body('status')
|
||||
.optional()
|
||||
.isIn(['draft', 'published', 'archived', 'template'])
|
||||
.withMessage('Status must be draft, published, archived or template'),
|
||||
|
||||
|
||||
body('timeSlots')
|
||||
.optional()
|
||||
.isArray()
|
||||
.withMessage('Time slots must be an array'),
|
||||
|
||||
|
||||
body('timeSlots.*.name')
|
||||
.isLength({ min: 1, max: 100 })
|
||||
.withMessage('Time slot name must be between 1-100 characters')
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
|
||||
body('timeSlots.*.startTime')
|
||||
.matches(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/)
|
||||
.withMessage('Start time must be in HH:MM format'),
|
||||
|
||||
|
||||
body('timeSlots.*.endTime')
|
||||
.matches(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/)
|
||||
.withMessage('End time must be in HH:MM format'),
|
||||
|
||||
|
||||
body('timeSlots.*.description')
|
||||
.optional()
|
||||
.isLength({ max: 500 })
|
||||
.withMessage('Time slot description cannot exceed 500 characters')
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
|
||||
body('shifts')
|
||||
.optional()
|
||||
.isArray()
|
||||
.withMessage('Shifts must be an array'),
|
||||
|
||||
|
||||
body('shifts.*.dayOfWeek')
|
||||
.isInt({ min: 1, max: 7 })
|
||||
.withMessage('Day of week must be between 1-7 (Monday-Sunday)'),
|
||||
|
||||
|
||||
body('shifts.*.timeSlotId')
|
||||
.isUUID()
|
||||
.withMessage('Time slot ID must be a valid UUID'),
|
||||
|
||||
|
||||
body('shifts.*.requiredEmployees')
|
||||
.isInt({ min: 0 })
|
||||
.withMessage('Required employees must be a positive integer'),
|
||||
|
||||
|
||||
body('shifts.*.color')
|
||||
.optional()
|
||||
.isHexColor()
|
||||
@@ -246,34 +249,34 @@ export const validateShiftPlanUpdate = [
|
||||
.withMessage('Name must be between 1-200 characters')
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
|
||||
body('description')
|
||||
.optional()
|
||||
.isLength({ max: 1000 })
|
||||
.withMessage('Description cannot exceed 1000 characters')
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
|
||||
body('startDate')
|
||||
.optional()
|
||||
.isISO8601()
|
||||
.withMessage('Must be a valid date (ISO format)'),
|
||||
|
||||
|
||||
body('endDate')
|
||||
.optional()
|
||||
.isISO8601()
|
||||
.withMessage('Must be a valid date (ISO format)'),
|
||||
|
||||
|
||||
body('status')
|
||||
.optional()
|
||||
.isIn(['draft', 'published', 'archived', 'template'])
|
||||
.withMessage('Status must be draft, published, archived or template'),
|
||||
|
||||
|
||||
body('timeSlots')
|
||||
.optional()
|
||||
.isArray()
|
||||
.withMessage('Time slots must be an array'),
|
||||
|
||||
|
||||
body('shifts')
|
||||
.optional()
|
||||
.isArray()
|
||||
@@ -284,25 +287,25 @@ export const validateCreateFromPreset = [
|
||||
body('presetName')
|
||||
.isLength({ min: 1 })
|
||||
.withMessage('Preset name is required')
|
||||
.isIn(['standardWeek', 'extendedWeek', 'weekendFocused', 'morningOnly', 'eveningOnly', 'ZEBRA_STANDARD'])
|
||||
.isIn(['GENERAL_STANDARD', 'ZEBRA_STANDARD'])
|
||||
.withMessage('Invalid preset name'),
|
||||
|
||||
|
||||
body('name')
|
||||
.isLength({ min: 1, max: 200 })
|
||||
.withMessage('Name must be between 1-200 characters')
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
|
||||
body('startDate')
|
||||
.optional()
|
||||
.isISO8601()
|
||||
.withMessage('Must be a valid date (ISO format)'),
|
||||
|
||||
|
||||
body('endDate')
|
||||
.optional()
|
||||
.isISO8601()
|
||||
.withMessage('Must be a valid date (ISO format)'),
|
||||
|
||||
|
||||
body('isTemplate')
|
||||
.optional()
|
||||
.isBoolean()
|
||||
@@ -314,11 +317,11 @@ export const validateScheduledShiftUpdate = [
|
||||
body('assignedEmployees')
|
||||
.isArray()
|
||||
.withMessage('assignedEmployees must be an array'),
|
||||
|
||||
|
||||
body('assignedEmployees.*')
|
||||
.isUUID()
|
||||
.withMessage('Each assigned employee must be a valid UUID'),
|
||||
|
||||
|
||||
body('requiredEmployees')
|
||||
.optional()
|
||||
.isInt({ min: 0 })
|
||||
@@ -332,18 +335,19 @@ export const validateSetupAdmin = [
|
||||
.withMessage('First name must be between 1-100 characters')
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
|
||||
body('lastname')
|
||||
.isLength({ min: 1, max: 100 })
|
||||
.withMessage('Last name must be between 1-100 characters')
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
|
||||
body('password')
|
||||
.optional()
|
||||
.isLength({ min: 8 })
|
||||
.withMessage('Password must be at least 8 characters')
|
||||
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
|
||||
.withMessage('Password must contain uppercase, lowercase and number')
|
||||
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?])/)
|
||||
.withMessage('Password must contain uppercase, lowercase, number and special character'),
|
||||
];
|
||||
|
||||
// ===== SCHEDULING VALIDATION =====
|
||||
@@ -351,23 +355,23 @@ export const validateSchedulingRequest = [
|
||||
body('shiftPlan')
|
||||
.isObject()
|
||||
.withMessage('Shift plan is required'),
|
||||
|
||||
|
||||
body('shiftPlan.id')
|
||||
.isUUID()
|
||||
.withMessage('Shift plan ID must be a valid UUID'),
|
||||
|
||||
|
||||
body('employees')
|
||||
.isArray({ min: 1 })
|
||||
.withMessage('At least one employee is required'),
|
||||
|
||||
|
||||
body('employees.*.id')
|
||||
.isUUID()
|
||||
.withMessage('Each employee must have a valid UUID'),
|
||||
|
||||
|
||||
body('availabilities')
|
||||
.isArray()
|
||||
.withMessage('Availabilities must be an array'),
|
||||
|
||||
|
||||
body('constraints')
|
||||
.optional()
|
||||
.isArray()
|
||||
@@ -379,19 +383,19 @@ export const validateAvailabilities = [
|
||||
body('planId')
|
||||
.isUUID()
|
||||
.withMessage('Plan ID must be a valid UUID'),
|
||||
|
||||
|
||||
body('availabilities')
|
||||
.isArray()
|
||||
.withMessage('Availabilities must be an array'),
|
||||
|
||||
|
||||
body('availabilities.*.shiftId')
|
||||
.isUUID()
|
||||
.withMessage('Each shift ID must be a valid UUID'),
|
||||
|
||||
|
||||
body('availabilities.*.preferenceLevel')
|
||||
.isInt({ min: 0, max: 2 })
|
||||
.withMessage('Preference level must be 0 (unavailable), 1 (available), or 2 (preferred)'),
|
||||
|
||||
|
||||
body('availabilities.*.notes')
|
||||
.optional()
|
||||
.isLength({ max: 500 })
|
||||
@@ -424,12 +428,12 @@ export const validatePagination = [
|
||||
.optional()
|
||||
.isInt({ min: 1 })
|
||||
.withMessage('Page must be a positive integer'),
|
||||
|
||||
|
||||
query('limit')
|
||||
.optional()
|
||||
.isInt({ min: 1, max: 100 })
|
||||
.withMessage('Limit must be between 1-100'),
|
||||
|
||||
|
||||
query('includeInactive')
|
||||
.optional()
|
||||
.isBoolean()
|
||||
@@ -439,19 +443,19 @@ export const validatePagination = [
|
||||
// ===== MIDDLEWARE TO CHECK VALIDATION RESULTS =====
|
||||
export const handleValidationErrors = (req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
const errorMessages = errors.array().map(error => ({
|
||||
field: error.type === 'field' ? error.path : error.type,
|
||||
message: error.msg,
|
||||
value: error.msg
|
||||
}));
|
||||
|
||||
|
||||
return res.status(400).json({
|
||||
error: 'Validation failed',
|
||||
details: errorMessages
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
next();
|
||||
};
|
||||
@@ -22,6 +22,8 @@ const app = express();
|
||||
const PORT = 3002;
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
app.set('trust proxy', true);
|
||||
|
||||
// Security configuration
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.info('Checking for JWT_SECRET');
|
||||
@@ -34,14 +36,20 @@ if (process.env.NODE_ENV === 'production') {
|
||||
|
||||
// Security headers
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: isDevelopment ? false : {
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
imgSrc: ["'self'", "data:", "https:"],
|
||||
connectSrc: ["'self'"],
|
||||
fontSrc: ["'self'"],
|
||||
objectSrc: ["'none'"],
|
||||
mediaSrc: ["'self'"],
|
||||
frameSrc: ["'none'"],
|
||||
},
|
||||
},
|
||||
hsts: false,
|
||||
crossOriginEmbedderPolicy: false
|
||||
}));
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ interface AuthContextType {
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || '/api';
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api';
|
||||
|
||||
interface AuthProviderProps {
|
||||
children: ReactNode;
|
||||
@@ -66,7 +66,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
try {
|
||||
const token = getStoredToken();
|
||||
console.log('🔄 Refreshing user, token exists:', !!token);
|
||||
|
||||
|
||||
if (!token) {
|
||||
console.log('ℹ️ No token found, user not logged in');
|
||||
setUser(null);
|
||||
@@ -104,7 +104,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
const login = async (credentials: LoginRequest): Promise<void> => {
|
||||
try {
|
||||
console.log('🔐 Attempting login for:', credentials.email);
|
||||
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -120,7 +120,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
|
||||
const data = await response.json();
|
||||
console.log('✅ Login successful, storing token');
|
||||
|
||||
|
||||
setStoredToken(data.token);
|
||||
setUser(data.user);
|
||||
} catch (error) {
|
||||
@@ -137,13 +137,13 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
|
||||
const hasRole = (roles: string[]): boolean => {
|
||||
if (!user || !user.roles || user.roles.length === 0) return false;
|
||||
|
||||
|
||||
// Check if user has at least one of the required roles
|
||||
return roles.some(requiredRole =>
|
||||
return roles.some(requiredRole =>
|
||||
user.roles!.includes(requiredRole)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const initializeAuth = async () => {
|
||||
console.log('🚀 Initializing authentication...');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// frontend/src/services/authService.ts
|
||||
import { Employee } from '../models/Employee';
|
||||
const API_BASE = process.env.REACT_APP_API_BASE_URL || '/api';
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api';
|
||||
|
||||
export interface LoginRequest {
|
||||
email: string;
|
||||
@@ -24,7 +24,7 @@ class AuthService {
|
||||
private token: string | null = null;
|
||||
|
||||
async login(credentials: LoginRequest): Promise<AuthResponse> {
|
||||
const response = await fetch(`${API_BASE}/auth/login`, {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(credentials)
|
||||
@@ -39,12 +39,11 @@ class AuthService {
|
||||
this.token = data.token;
|
||||
localStorage.setItem('token', data.token);
|
||||
localStorage.setItem('employee', JSON.stringify(data.employee));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async register(userData: RegisterRequest): Promise<AuthResponse> {
|
||||
const response = await fetch(`${API_BASE}/employees`, {
|
||||
const response = await fetch(`${API_BASE_URL}/employees`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(userData)
|
||||
@@ -73,7 +72,7 @@ class AuthService {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/auth/me`, {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/me`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
|
||||
@@ -1,167 +1,59 @@
|
||||
// vite.config.ts
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { resolve } from 'path'
|
||||
|
||||
// Security-focused Vite configuration
|
||||
export default defineConfig(({ mode }) => {
|
||||
const isProduction = mode === 'production'
|
||||
const isDevelopment = mode === 'development'
|
||||
|
||||
// Load environment variables securely
|
||||
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
|
||||
// Strictly defined client-safe environment variables
|
||||
|
||||
// 🆕 WICHTIG: Relative Pfade für Production
|
||||
const clientEnv = {
|
||||
NODE_ENV: mode,
|
||||
ENABLE_PRO: env.ENABLE_PRO || 'false',
|
||||
VITE_APP_TITLE: env.APP_TITLE || 'Shift Planning App',
|
||||
VITE_API_URL: isProduction ? '/api' : 'http://localhost:3002/api',
|
||||
VITE_API_URL: isProduction ? '/api' : '/api',
|
||||
}
|
||||
|
||||
return {
|
||||
plugins: [
|
||||
react({
|
||||
// React specific security settings
|
||||
jsxRuntime: 'automatic',
|
||||
babel: {
|
||||
plugins: [
|
||||
// Remove console in production
|
||||
isProduction && ['babel-plugin-transform-remove-console', { exclude: ['error', 'warn'] }]
|
||||
].filter(Boolean)
|
||||
}
|
||||
})
|
||||
],
|
||||
|
||||
plugins: [react()],
|
||||
|
||||
server: {
|
||||
port: 3003,
|
||||
host: true,
|
||||
open: isDevelopment,
|
||||
// Security headers for dev server
|
||||
headers: {
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'X-Frame-Options': 'DENY',
|
||||
'X-XSS-Protection': '1; mode=block',
|
||||
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
||||
'Permissions-Policy': 'camera=(), microphone=(), location=()'
|
||||
},
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3002',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
}
|
||||
},
|
||||
// Security: disable HMR in non-dev environments
|
||||
hmr: isDevelopment
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
// Security: No source maps in production
|
||||
sourcemap: isDevelopment ? 'inline' : false,
|
||||
// Generate deterministic hashes for better caching and security
|
||||
assetsDir: 'assets',
|
||||
base: mode === 'production' ? '/' : '/',
|
||||
sourcemap: isDevelopment,
|
||||
base: isProduction ? '/' : '/',
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// Security: Use content hashes for cache busting and integrity
|
||||
chunkFileNames: 'assets/[name]-[hash].js',
|
||||
entryFileNames: 'assets/[name]-[hash].js',
|
||||
assetFileNames: 'assets/[name]-[hash].[ext]',
|
||||
// Security: Manual chunks to separate vendor code
|
||||
manualChunks: (id) => {
|
||||
if (id.includes('node_modules')) {
|
||||
if (id.includes('react') || id.includes('react-dom')) {
|
||||
return 'vendor-react'
|
||||
}
|
||||
if (id.includes('react-router-dom')) {
|
||||
return 'vendor-router'
|
||||
}
|
||||
return 'vendor'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// Minification with security-focused settings
|
||||
minify: isProduction ? 'terser' : false,
|
||||
terserOptions: isProduction ? {
|
||||
compress: {
|
||||
drop_console: true,
|
||||
drop_debugger: true,
|
||||
// Security: Remove potentially sensitive code
|
||||
pure_funcs: [
|
||||
'console.log',
|
||||
'console.info',
|
||||
'console.debug',
|
||||
'console.warn',
|
||||
'console.trace',
|
||||
'console.table',
|
||||
'debugger'
|
||||
],
|
||||
dead_code: true,
|
||||
if_return: true,
|
||||
comparisons: true,
|
||||
loops: true,
|
||||
hoist_funs: true,
|
||||
hoist_vars: true,
|
||||
reduce_vars: true,
|
||||
booleans: true,
|
||||
conditionals: true,
|
||||
evaluate: true,
|
||||
sequences: true,
|
||||
unused: true
|
||||
},
|
||||
mangle: {
|
||||
// Security: Obfuscate code
|
||||
toplevel: true,
|
||||
keep_classnames: false,
|
||||
keep_fnames: false,
|
||||
reserved: [
|
||||
'React',
|
||||
'ReactDOM',
|
||||
'useState',
|
||||
'useEffect',
|
||||
'useContext',
|
||||
'createElement'
|
||||
]
|
||||
},
|
||||
format: {
|
||||
comments: false,
|
||||
beautify: false,
|
||||
// Security: ASCII only to prevent encoding attacks
|
||||
ascii_only: true
|
||||
pure_funcs: ['console.log', 'console.debug', 'console.info']
|
||||
}
|
||||
} : undefined,
|
||||
// Security: Report bundle size issues
|
||||
reportCompressedSize: true,
|
||||
chunkSizeWarningLimit: 1000,
|
||||
// Security: Don't expose source paths
|
||||
assetsInlineLimit: 4096
|
||||
},
|
||||
|
||||
preview: {
|
||||
port: 3004,
|
||||
headers: {
|
||||
// Security headers for preview server
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'X-Frame-Options': 'DENY',
|
||||
'X-XSS-Protection': '1; mode=block',
|
||||
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
|
||||
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
||||
'Content-Security-Policy': `
|
||||
default-src 'self';
|
||||
script-src 'self' 'unsafe-inline';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: https:;
|
||||
font-src 'self';
|
||||
connect-src 'self';
|
||||
base-uri 'self';
|
||||
form-action 'self';
|
||||
frame-ancestors 'none';
|
||||
`.replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, './src'),
|
||||
@@ -174,31 +66,10 @@ export default defineConfig(({ mode }) => {
|
||||
'@/design': resolve(__dirname, './src/design')
|
||||
}
|
||||
},
|
||||
|
||||
// ✅ SICHER: Strict environment variable control
|
||||
|
||||
define: Object.keys(clientEnv).reduce((acc, key) => {
|
||||
acc[`import.meta.env.${key}`] = JSON.stringify(clientEnv[key])
|
||||
return acc
|
||||
}, {} as Record<string, string>),
|
||||
|
||||
// Security: Clear build directory
|
||||
emptyOutDir: true,
|
||||
|
||||
// Security: Optimize dependencies
|
||||
optimizeDeps: {
|
||||
include: ['react', 'react-dom', 'react-router-dom'],
|
||||
exclude: ['@vitejs/plugin-react']
|
||||
},
|
||||
|
||||
// Security: CSS configuration
|
||||
css: {
|
||||
devSourcemap: isDevelopment,
|
||||
modules: {
|
||||
localsConvention: 'camelCase',
|
||||
generateScopedName: isProduction
|
||||
? '[hash:base64:8]'
|
||||
: '[name]__[local]--[hash:base64:5]'
|
||||
}
|
||||
}
|
||||
}, {} as Record<string, string>)
|
||||
}
|
||||
})
|
||||
125
package-lock.json
generated
125
package-lock.json
generated
@@ -280,6 +280,7 @@
|
||||
"backend/node_modules/@types/node": {
|
||||
"version": "24.7.0",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.14.0"
|
||||
}
|
||||
@@ -349,17 +350,6 @@
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"backend/node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"backend/node_modules/acorn-walk": {
|
||||
"version": "8.3.4",
|
||||
"dev": true,
|
||||
@@ -2038,6 +2028,7 @@
|
||||
"frontend": {
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"date-fns": "4.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^6.28.0"
|
||||
@@ -2048,7 +2039,9 @@
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"babel-plugin-transform-remove-console": "6.9.4",
|
||||
"esbuild": "^0.21.0",
|
||||
"terser": "5.44.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.0.7"
|
||||
}
|
||||
@@ -2078,6 +2071,7 @@
|
||||
"version": "7.28.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -2338,6 +2332,17 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/source-map": {
|
||||
"version": "0.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
|
||||
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"dev": true,
|
||||
@@ -2388,10 +2393,6 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@schichtenplaner/premium": {
|
||||
"resolved": "premium",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"dev": true,
|
||||
@@ -2443,6 +2444,7 @@
|
||||
"version": "20.19.23",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -2451,6 +2453,7 @@
|
||||
"version": "19.2.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -2514,12 +2517,32 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/babel-plugin-transform-remove-console": {
|
||||
"version": "6.9.4",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-remove-console/-/babel-plugin-transform-remove-console-6.9.4.tgz",
|
||||
"integrity": "sha512-88blrUrMX3SPiGkT1GnvVY8E/7A+k6oj3MNvUtTIxJflFzXTw1bHkuJ/y039ouhFMp2prRn5cQGzokViYi1dsg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.20",
|
||||
"dev": true,
|
||||
@@ -2585,6 +2608,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.19",
|
||||
"caniuse-lite": "^1.0.30001751",
|
||||
@@ -2599,6 +2623,13 @@
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -2656,6 +2687,13 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
@@ -2702,6 +2740,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"devOptional": true,
|
||||
@@ -3368,6 +3416,7 @@
|
||||
"version": "4.0.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -3457,6 +3506,7 @@
|
||||
"node_modules/react": {
|
||||
"version": "19.2.0",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -3464,6 +3514,7 @@
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.0",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -3730,6 +3781,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"dev": true,
|
||||
@@ -3738,6 +3799,17 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support": {
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
@@ -3747,6 +3819,26 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.44.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
|
||||
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.15.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"bin": {
|
||||
"terser": "bin/terser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"dev": true,
|
||||
@@ -3788,6 +3880,7 @@
|
||||
"version": "5.9.3",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -3870,6 +3963,7 @@
|
||||
"version": "6.4.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
@@ -4002,6 +4096,7 @@
|
||||
"premium": {
|
||||
"name": "@schichtenplaner/premium",
|
||||
"version": "1.0.0",
|
||||
"extraneous": true,
|
||||
"workspaces": [
|
||||
"backendPRO",
|
||||
"frontendPRO"
|
||||
|
||||
Reference in New Issue
Block a user