mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 06:55:45 +01:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1231c8362f | |||
| 663eb61352 | |||
| 23f1dd7aa0 | |||
| 5319ed5d7a |
@@ -1,4 +0,0 @@
|
|||||||
# .env.production example
|
|
||||||
NODE_ENV=production
|
|
||||||
JWT_SECRET=your-secret-key
|
|
||||||
DATABASE_PATH=/app/data/production.db
|
|
||||||
16
.env.template
Normal file
16
.env.template
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# === SCHICHTPLANER DOCKER COMPOSE ENVIRONMENT VARIABLES ===
|
||||||
|
# Diese Datei wird von docker-compose automatisch geladen
|
||||||
|
|
||||||
|
# Security
|
||||||
|
JWT_SECRET=${JWT_SECRET:-your-secret-key-please-change}
|
||||||
|
NODE_ENV=${NODE_ENV:-production}
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DB_PATH=${DB_PATH:-/app/data/database.db}
|
||||||
|
|
||||||
|
# Server
|
||||||
|
PORT=${PORT:-3002}
|
||||||
|
|
||||||
|
# App Configuration
|
||||||
|
APP_TITLE="Shift Planning App"
|
||||||
|
ENABLE_PRO=${ENABLE_PRO:-false}
|
||||||
24
Dockerfile
24
Dockerfile
@@ -35,14 +35,19 @@ RUN npm run build --workspace=frontend
|
|||||||
# Verify Python and OR-Tools installation
|
# Verify Python and OR-Tools installation
|
||||||
RUN python -c "from ortools.sat.python import cp_model; print('OR-Tools installed successfully')"
|
RUN python -c "from ortools.sat.python import cp_model; print('OR-Tools installed successfully')"
|
||||||
|
|
||||||
# Production stage (same as above)
|
# Production stage
|
||||||
FROM node:20-bookworm
|
FROM node:20-bookworm
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies including gettext-base for envsubst
|
||||||
|
RUN apt-get update && apt-get install -y gettext-base && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN npm install -g pm2
|
RUN npm install -g pm2
|
||||||
RUN mkdir -p /app/data
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
COPY --from=builder /app/backend/dist/ ./dist/
|
COPY --from=builder /app/backend/dist/ ./dist/
|
||||||
COPY --from=builder /app/backend/package*.json ./
|
COPY --from=builder /app/backend/package*.json ./
|
||||||
|
|
||||||
@@ -54,6 +59,14 @@ COPY --from=builder /app/ecosystem.config.cjs ./
|
|||||||
COPY --from=builder /app/backend/src/database/ ./dist/database/
|
COPY --from=builder /app/backend/src/database/ ./dist/database/
|
||||||
COPY --from=builder /app/backend/src/database/ ./database/
|
COPY --from=builder /app/backend/src/database/ ./database/
|
||||||
|
|
||||||
|
# Copy init script and env template
|
||||||
|
COPY docker-init.sh /usr/local/bin/
|
||||||
|
COPY .env.template ./
|
||||||
|
|
||||||
|
# Set execute permissions for init script
|
||||||
|
RUN chmod +x /usr/local/bin/docker-init.sh
|
||||||
|
|
||||||
|
# Create user and set permissions
|
||||||
RUN groupadd -g 1001 nodejs && \
|
RUN groupadd -g 1001 nodejs && \
|
||||||
useradd -m -u 1001 -s /bin/bash -g nodejs schichtplan && \
|
useradd -m -u 1001 -s /bin/bash -g nodejs schichtplan && \
|
||||||
chown -R schichtplan:nodejs /app && \
|
chown -R schichtplan:nodejs /app && \
|
||||||
@@ -61,10 +74,13 @@ RUN groupadd -g 1001 nodejs && \
|
|||||||
chmod 775 /app/data
|
chmod 775 /app/data
|
||||||
|
|
||||||
ENV PM2_HOME=/app/.pm2
|
ENV PM2_HOME=/app/.pm2
|
||||||
|
|
||||||
|
# Set entrypoint to init script and keep existing cmd
|
||||||
|
ENTRYPOINT ["/usr/local/bin/docker-init.sh"]
|
||||||
|
CMD ["pm2-runtime", "ecosystem.config.cjs"]
|
||||||
|
|
||||||
USER schichtplan
|
USER schichtplan
|
||||||
EXPOSE 3002
|
EXPOSE 3002
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3002/api/health || exit 1
|
CMD wget --no-verbose --tries=1 --spider http://localhost:3002/api/health || exit 1
|
||||||
|
|
||||||
CMD ["pm2-runtime", "ecosystem.config.cjs"]
|
|
||||||
@@ -22,11 +22,13 @@ 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');
|
||||||
const JWT_SECRET = process.env.JWT_SECRET;
|
const JWT_SECRET = process.env.JWT_SECRET;
|
||||||
if (!JWT_SECRET || JWT_SECRET === 'your-secret-key') {
|
if (!JWT_SECRET || JWT_SECRET === 'your-secret-key-please-change') {
|
||||||
console.error('❌ Fatal: JWT_SECRET not set or using default value');
|
console.error('❌ Fatal: JWT_SECRET not set or using default value');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,19 @@ services:
|
|||||||
schichtplaner:
|
schichtplaner:
|
||||||
container_name: schichtplaner
|
container_name: schichtplaner
|
||||||
image: ghcr.io/donpat1to/schichtenplaner:v1.0.0
|
image: ghcr.io/donpat1to/schichtenplaner:v1.0.0
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- JWT_SECRET=${JWT_SECRET:-your-secret-key-please-change}
|
||||||
ports:
|
ports:
|
||||||
- "3002:3002"
|
- "3002:3002"
|
||||||
volumes:
|
volumes:
|
||||||
- app_data:/app/data
|
- app_data:/app/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:3002/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
app_data:
|
app_data:
|
||||||
50
docker-init.sh
Normal file
50
docker-init.sh
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🚀 Container Initialisierung gestartet..."
|
||||||
|
|
||||||
|
# Funktion zum Generieren eines sicheren Secrets
|
||||||
|
generate_secret() {
|
||||||
|
length=$1
|
||||||
|
tr -dc 'A-Za-z0-9!@#$%^&*()_+-=' < /dev/urandom | head -c $length
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prüfe ob .env existiert
|
||||||
|
if [ ! -f /app/.env ]; then
|
||||||
|
echo "📝 Erstelle .env Datei..."
|
||||||
|
|
||||||
|
# Verwende vorhandenes JWT_SECRET oder generiere ein neues
|
||||||
|
if [ -z "$JWT_SECRET" ] || [ "$JWT_SECRET" = "your-secret-key-please-change" ]; then
|
||||||
|
export JWT_SECRET=$(generate_secret 64)
|
||||||
|
echo "🔑 Automatisch sicheres JWT Secret generiert"
|
||||||
|
else
|
||||||
|
echo "🔑 Verwende vorhandenes JWT Secret aus Umgebungsvariable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Erstelle .env aus Template mit envsubst
|
||||||
|
envsubst < /app/.env.template > /app/.env
|
||||||
|
echo "✅ .env Datei erstellt"
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "ℹ️ .env Datei existiert bereits"
|
||||||
|
|
||||||
|
# Wenn .env existiert, aber JWT_SECRET Umgebungsvariable gesetzt ist, aktualisiere sie
|
||||||
|
if [ -n "$JWT_SECRET" ] && [ "$JWT_SECRET" != "your-secret-key-please-change" ]; then
|
||||||
|
echo "🔑 Aktualisiere JWT Secret in .env Datei"
|
||||||
|
# Aktualisiere nur das JWT_SECRET in der .env Datei
|
||||||
|
sed -i "s/^JWT_SECRET=.*/JWT_SECRET=$JWT_SECRET/" /app/.env
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validiere dass JWT_SECERT nicht der Standardwert ist
|
||||||
|
if grep -q "JWT_SECRET=your-secret-key-please-change" /app/.env; then
|
||||||
|
echo "❌ FEHLER: Standard JWT Secret in .env gefunden!"
|
||||||
|
echo "❌ Bitte setzen Sie JWT_SECRET Umgebungsvariable"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Setze sichere Berechtigungen
|
||||||
|
chmod 600 /app/.env
|
||||||
|
|
||||||
|
echo "🔧 Starte Anwendung..."
|
||||||
|
exec "$@"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// frontend/src/pages/Features/Features.tsx
|
// frontend/src/components/Layou/FooterLinks/Features/Features.tsx
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const Features: React.FC = () => {
|
const Features: React.FC = () => {
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
|
|
||||||
.createButton {
|
.createButton {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background-color: #2ecc71;
|
background-color: #51258f;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.createButton:hover {
|
.createButton:hover {
|
||||||
background-color: #27ae60;
|
background-color: #51258f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.createButton:disabled {
|
.createButton:disabled {
|
||||||
|
|||||||
@@ -1,164 +1,57 @@
|
|||||||
|
// 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.VITE_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' : 'http://localhost:3002/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',
|
|
||||||
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: {
|
||||||
@@ -174,30 +67,9 @@ export default defineConfig(({ mode }) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// ✅ 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]'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user