Compare commits

...

4 Commits

6 changed files with 230 additions and 253 deletions

View File

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

View File

@@ -22,6 +22,8 @@ const app = express();
const PORT = 3002; const PORT = 3002;
const isDevelopment = process.env.NODE_ENV === 'development'; const isDevelopment = process.env.NODE_ENV === 'development';
app.set('trust proxy', true);
// Security configuration // Security configuration
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
console.info('Checking for JWT_SECRET'); console.info('Checking for JWT_SECRET');
@@ -34,14 +36,20 @@ if (process.env.NODE_ENV === 'production') {
// Security headers // Security headers
app.use(helmet({ app.use(helmet({
contentSecurityPolicy: isDevelopment ? false : { contentSecurityPolicy: {
directives: { directives: {
defaultSrc: ["'self'"], defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"], styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"], imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
}, },
}, },
hsts: false,
crossOriginEmbedderPolicy: false crossOriginEmbedderPolicy: false
})); }));

View File

@@ -20,7 +20,7 @@ interface AuthContextType {
} }
const AuthContext = createContext<AuthContextType | undefined>(undefined); 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 { interface AuthProviderProps {
children: ReactNode; children: ReactNode;
@@ -66,7 +66,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
try { try {
const token = getStoredToken(); const token = getStoredToken();
console.log('🔄 Refreshing user, token exists:', !!token); console.log('🔄 Refreshing user, token exists:', !!token);
if (!token) { if (!token) {
console.log(' No token found, user not logged in'); console.log(' No token found, user not logged in');
setUser(null); setUser(null);
@@ -104,7 +104,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const login = async (credentials: LoginRequest): Promise<void> => { const login = async (credentials: LoginRequest): Promise<void> => {
try { try {
console.log('🔐 Attempting login for:', credentials.email); console.log('🔐 Attempting login for:', credentials.email);
const response = await fetch(`${API_BASE_URL}/auth/login`, { const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -120,7 +120,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const data = await response.json(); const data = await response.json();
console.log('✅ Login successful, storing token'); console.log('✅ Login successful, storing token');
setStoredToken(data.token); setStoredToken(data.token);
setUser(data.user); setUser(data.user);
} catch (error) { } catch (error) {
@@ -137,13 +137,13 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const hasRole = (roles: string[]): boolean => { const hasRole = (roles: string[]): boolean => {
if (!user || !user.roles || user.roles.length === 0) return false; if (!user || !user.roles || user.roles.length === 0) return false;
// Check if user has at least one of the required roles // Check if user has at least one of the required roles
return roles.some(requiredRole => return roles.some(requiredRole =>
user.roles!.includes(requiredRole) user.roles!.includes(requiredRole)
); );
}; };
useEffect(() => { useEffect(() => {
const initializeAuth = async () => { const initializeAuth = async () => {
console.log('🚀 Initializing authentication...'); console.log('🚀 Initializing authentication...');

View File

@@ -1,6 +1,6 @@
// frontend/src/services/authService.ts // frontend/src/services/authService.ts
import { Employee } from '../models/Employee'; 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 { export interface LoginRequest {
email: string; email: string;
@@ -24,7 +24,7 @@ class AuthService {
private token: string | null = null; private token: string | null = null;
async login(credentials: LoginRequest): Promise<AuthResponse> { 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', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials) body: JSON.stringify(credentials)
@@ -39,12 +39,11 @@ class AuthService {
this.token = data.token; this.token = data.token;
localStorage.setItem('token', data.token); localStorage.setItem('token', data.token);
localStorage.setItem('employee', JSON.stringify(data.employee)); localStorage.setItem('employee', JSON.stringify(data.employee));
return data; return data;
} }
async register(userData: RegisterRequest): Promise<AuthResponse> { async register(userData: RegisterRequest): Promise<AuthResponse> {
const response = await fetch(`${API_BASE}/employees`, { const response = await fetch(`${API_BASE_URL}/employees`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData) body: JSON.stringify(userData)
@@ -73,7 +72,7 @@ class AuthService {
} }
try { try {
const response = await fetch(`${API_BASE}/auth/me`, { const response = await fetch(`${API_BASE_URL}/auth/me`, {
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
} }

View File

@@ -1,167 +1,59 @@
// vite.config.ts
import { defineConfig, loadEnv } from 'vite' import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import { resolve } from 'path' import { resolve } from 'path'
// Security-focused Vite configuration
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const isProduction = mode === 'production' const isProduction = mode === 'production'
const isDevelopment = mode === 'development' const isDevelopment = mode === 'development'
// Load environment variables securely
const env = loadEnv(mode, process.cwd(), '') const env = loadEnv(mode, process.cwd(), '')
// Strictly defined client-safe environment variables // 🆕 WICHTIG: Relative Pfade für Production
const clientEnv = { const clientEnv = {
NODE_ENV: mode, NODE_ENV: mode,
ENABLE_PRO: env.ENABLE_PRO || 'false', ENABLE_PRO: env.ENABLE_PRO || 'false',
VITE_APP_TITLE: env.APP_TITLE || 'Shift Planning App', 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 { return {
plugins: [ plugins: [react()],
react({
// React specific security settings
jsxRuntime: 'automatic',
babel: {
plugins: [
// Remove console in production
isProduction && ['babel-plugin-transform-remove-console', { exclude: ['error', 'warn'] }]
].filter(Boolean)
}
})
],
server: { server: {
port: 3003, port: 3003,
host: true, host: true,
open: isDevelopment, 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: { proxy: {
'/api': { '/api': {
target: 'http://localhost:3002', target: 'http://localhost:3002',
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
} }
}, }
// Security: disable HMR in non-dev environments
hmr: isDevelopment
}, },
build: { build: {
outDir: 'dist', outDir: 'dist',
// Security: No source maps in production sourcemap: isDevelopment,
sourcemap: isDevelopment ? 'inline' : false, base: isProduction ? '/' : '/',
// Generate deterministic hashes for better caching and security
assetsDir: 'assets',
base: mode === 'production' ? '/' : '/',
rollupOptions: { rollupOptions: {
output: { output: {
// Security: Use content hashes for cache busting and integrity
chunkFileNames: 'assets/[name]-[hash].js', chunkFileNames: 'assets/[name]-[hash].js',
entryFileNames: 'assets/[name]-[hash].js', entryFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]', 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, minify: isProduction ? 'terser' : false,
terserOptions: isProduction ? { terserOptions: isProduction ? {
compress: { compress: {
drop_console: true, drop_console: true,
drop_debugger: true, drop_debugger: true,
// Security: Remove potentially sensitive code pure_funcs: ['console.log', 'console.debug', 'console.info']
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
} }
} : undefined, } : 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: { resolve: {
alias: { alias: {
'@': resolve(__dirname, './src'), '@': resolve(__dirname, './src'),
@@ -174,31 +66,10 @@ export default defineConfig(({ mode }) => {
'@/design': resolve(__dirname, './src/design') '@/design': resolve(__dirname, './src/design')
} }
}, },
// ✅ SICHER: Strict environment variable control
define: Object.keys(clientEnv).reduce((acc, key) => { define: Object.keys(clientEnv).reduce((acc, key) => {
acc[`import.meta.env.${key}`] = JSON.stringify(clientEnv[key]) acc[`import.meta.env.${key}`] = JSON.stringify(clientEnv[key])
return acc return acc
}, {} as Record<string, string>), }, {} 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]'
}
}
} }
}) })

125
package-lock.json generated
View File

@@ -280,6 +280,7 @@
"backend/node_modules/@types/node": { "backend/node_modules/@types/node": {
"version": "24.7.0", "version": "24.7.0",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.14.0" "undici-types": "~7.14.0"
} }
@@ -349,17 +350,6 @@
"license": "ISC", "license": "ISC",
"optional": true "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": { "backend/node_modules/acorn-walk": {
"version": "8.3.4", "version": "8.3.4",
"dev": true, "dev": true,
@@ -2038,6 +2028,7 @@
"frontend": { "frontend": {
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"date-fns": "4.1.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-router-dom": "^6.28.0" "react-router-dom": "^6.28.0"
@@ -2048,7 +2039,9 @@
"@types/react-dom": "^19.0.0", "@types/react-dom": "^19.0.0",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^4.3.3", "@vitejs/plugin-react": "^4.3.3",
"babel-plugin-transform-remove-console": "6.9.4",
"esbuild": "^0.21.0", "esbuild": "^0.21.0",
"terser": "5.44.0",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"vite": "^6.0.7" "vite": "^6.0.7"
} }
@@ -2078,6 +2071,7 @@
"version": "7.28.5", "version": "7.28.5",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.5",
@@ -2338,6 +2332,17 @@
"node": ">=6.0.0" "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": { "node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5", "version": "1.5.5",
"dev": true, "dev": true,
@@ -2388,10 +2393,6 @@
"win32" "win32"
] ]
}, },
"node_modules/@schichtenplaner/premium": {
"resolved": "premium",
"link": true
},
"node_modules/@types/babel__core": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
"dev": true, "dev": true,
@@ -2443,6 +2444,7 @@
"version": "20.19.23", "version": "20.19.23",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
@@ -2451,6 +2453,7 @@
"version": "19.2.2", "version": "19.2.2",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@@ -2514,12 +2517,32 @@
"node": ">= 0.6" "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": { "node_modules/array-flatten": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT" "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": { "node_modules/baseline-browser-mapping": {
"version": "2.8.20", "version": "2.8.20",
"dev": true, "dev": true,
@@ -2585,6 +2608,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.19", "baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751", "caniuse-lite": "^1.0.30001751",
@@ -2599,6 +2623,13 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "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": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -2656,6 +2687,13 @@
], ],
"license": "CC-BY-4.0" "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": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -2702,6 +2740,16 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"devOptional": true, "devOptional": true,
@@ -3368,6 +3416,7 @@
"version": "4.0.3", "version": "4.0.3",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -3457,6 +3506,7 @@
"node_modules/react": { "node_modules/react": {
"version": "19.2.0", "version": "19.2.0",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -3464,6 +3514,7 @@
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "19.2.0", "version": "19.2.0",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@@ -3730,6 +3781,16 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"dev": true, "dev": true,
@@ -3738,6 +3799,17 @@
"node": ">=0.10.0" "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": { "node_modules/statuses": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@@ -3747,6 +3819,26 @@
"node": ">= 0.8" "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": { "node_modules/tinyglobby": {
"version": "0.2.15", "version": "0.2.15",
"dev": true, "dev": true,
@@ -3788,6 +3880,7 @@
"version": "5.9.3", "version": "5.9.3",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -3870,6 +3963,7 @@
"version": "6.4.1", "version": "6.4.1",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.4", "fdir": "^6.4.4",
@@ -4002,6 +4096,7 @@
"premium": { "premium": {
"name": "@schichtenplaner/premium", "name": "@schichtenplaner/premium",
"version": "1.0.0", "version": "1.0.0",
"extraneous": true,
"workspaces": [ "workspaces": [
"backendPRO", "backendPRO",
"frontendPRO" "frontendPRO"