DEV1.0: Initial commit - Workflow Portal with security fixes

- Backend: Express.js + PostgreSQL/SQLite with LDAP/AD integration
- Frontend: React 18 + Vite + TailwindCSS/DaisyUI
- Security fixes applied (2026-07 + 2026-08):
  - LDAP injection prevention, CSRF protection, HttpOnly cookies
  - Session hashing (SHA-256), account lockout, rate limiting
  - Input validation (zod), file upload security, CSP/HSTS headers
  - V3: express-rate-limit updated (ip-address SSRF fix)
  - V4: postcss updated (nanoid DoS fix)
  - V5: Rate-limit on /health endpoint
  - V6: Session rotation on login (session fixation prevention)
  - V9: Task values array limit (DoS prevention)
  - V10: Frontend XSS audit completed
- Docker: Multi-stage build, non-root user, PostgreSQL + backup service
This commit is contained in:
Kühn
2026-08-24 09:45:28 +02:00
commit 6be1791c62
103 changed files with 12253 additions and 0 deletions

215
backend/server.js Normal file
View File

@@ -0,0 +1,215 @@
/**
* Workflow Portal Backend - Modular Architecture
*
* Punkt 1: Modularized from monolithic server.js into route modules
* Punkt 2: Removed unused Prisma (no longer needed)
* Punkt 3: Proper migration tracking via _migrations table
* Punkt 4: Session tokens hashed with SHA-256
* Punkt 5: Register returns correct 'inaktiv' status
* Punkt 6: better-sqlite3 (synchronous, no callback hell)
* Punkt 7: Single aggregated stats query
* Punkt 8: Transactions for template updates and task creation
* Punkt 9: LDAP sync lock
* Punkt 10: express-async-errors for global error handling
* Punkt 19: Configurable CORS via env
* Punkt 22: Prisma removed (was unused)
* Punkt 23: User-level rate limiting
*/
const express = require('express');
require('express-async-errors');
const cors = require('cors');
const helmet = require('helmet');
const cookieParser = require('cookie-parser');
const path = require('path');
// Initialize database (better-sqlite3, WAL mode, migrations)
const db = require('./db');
const { initDatabase } = require('./migrations');
// Auth middleware
const { authMiddleware, csrfMiddleware } = require('./middleware/auth');
// Rate limiters
const rateLimit = require('express-rate-limit');
const { apiLimiter } = require('./middleware/rateLimit');
// Route modules
const authRoutes = require('./routes/auth');
const usersRoutes = require('./routes/users');
const templatesRoutes = require('./routes/templates');
const tasksRoutes = require('./routes/tasks');
const adRoutes = require('./routes/ad');
const statsRoutes = require('./routes/stats');
const uploadRoutes = require('./routes/upload');
// LDAP sync
const { startLDAPSync, isLDAPConfigured } = require('./ldapSync');
const app = express();
const PORT = process.env.PORT || 5000;
// Trust proxy for correct IP in rate limiting (Docker/Reverse Proxy)
app.set('trust proxy', 1);
// ============ Security Middleware ============
// P14: Validate CORS_ORIGIN - filter empty/invalid entries before using in CSP
const rawCorsOrigin = process.env.CORS_ORIGIN || '';
const validCorsOrigins = rawCorsOrigin
.split(',')
.map(o => o.trim())
.filter(o => o && /^https?:\/\/.+/.test(o));
const cspConnectSrc = ["'self'", ...validCorsOrigins];
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:"],
connectSrc: cspConnectSrc,
fontSrc: ["'self'", "data:"],
},
},
// P18: HSTS - enforce HTTPS in production
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
crossOriginEmbedderPolicy: false,
}));
// Punkt 19: Configurable CORS via env variable
// Single container: Frontend served from same origin, CORS only needed for external access
const allowedOrigins = validCorsOrigins.length > 0
? validCorsOrigins
: ['http://localhost:5000', 'http://localhost:5173', 'http://127.0.0.1:5000', 'http://127.0.0.1:5173'];
app.use(cors({ origin: allowedOrigins, credentials: true }));
// Punkt 14: Body-Size-Limit to prevent DoS via large payloads
app.use(express.json({ limit: process.env.BODY_LIMIT || '1mb' }));
// Punkt 8: Cookie-Parser für HttpOnly-Cookie Auth
app.use(cookieParser());
// P4: CSRF protection for state-changing requests (Double-Submit-Cookie)
// Skip CSRF check for login/register (no session yet, no CSRF token available)
app.use('/api', (req, res, next) => {
if (req.path.startsWith('/auth/login') || req.path.startsWith('/auth/register') || req.path.startsWith('/v1/auth/login') || req.path.startsWith('/v1/auth/register')) {
return next();
}
csrfMiddleware(req, res, next);
});
// ============ Rate Limiting ============
app.use('/api', apiLimiter);
// ============ Health Check (Punkt 6) ============
// V5: Rate-limit /health to prevent DoS/amplification abuse
const healthLimiter = rateLimit({
windowMs: 60 * 1000,
max: 30,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Zu viele Health-Check-Anfragen.' },
});
app.get('/health', healthLimiter, (req, res) => {
res.json({ status: 'ok', uptime: Math.floor(process.uptime()), timestamp: new Date().toISOString() });
});
// ============ Auth Middleware for all /api/ routes except /api/auth/ ============
app.use('/api', (req, res, next) => {
// Skip auth for login, register, and status endpoints
if (req.path.startsWith('/auth/') || req.path === '/ad/status') {
return next();
}
authMiddleware(req, res, next);
});
// ============ Routes (Punkt 13: API-Versionierung /api/v1) ============
app.use('/api/v1/auth', authRoutes);
app.use('/api/v1/users', usersRoutes);
app.use('/api/v1/templates', templatesRoutes);
app.use('/api/v1/tasks', tasksRoutes);
app.use('/api/v1/ad', adRoutes);
app.use('/api/v1', statsRoutes);
app.use('/api/v1/upload', uploadRoutes);
// ============ Backward Compatibility: /api/ → /api/v1/ ============
app.use('/api/auth', authRoutes);
app.use('/api/users', usersRoutes);
app.use('/api/templates', templatesRoutes);
app.use('/api/tasks', tasksRoutes);
app.use('/api/ad', adRoutes);
app.use('/api', statsRoutes);
app.use('/api/upload', uploadRoutes);
// ============ Serve Frontend (Single Container) ============
const frontendPath = path.join(__dirname, 'frontend', 'dist');
app.use(express.static(frontendPath));
// SPA fallback: serve index.html for all non-API routes
app.get('*', (req, res, next) => {
if (req.path.startsWith('/api') || req.path.startsWith('/health')) return next();
res.sendFile(path.join(frontendPath, 'index.html'));
});
// ============ Global Error Handler (Punkt 10) ============
app.use((err, req, res, next) => {
console.error('[ERROR]', req.method, req.path, '-', err.message);
if (res.headersSent) return next(err);
res.status(500).json({ error: 'Interner Serverfehler.' });
});
// ============ Initialize & Start ============
async function start() {
try {
await initDatabase();
startLDAPSync(db);
const server = app.listen(PORT, () => {
console.log(`Workflow Portal Backend gestartet auf Port ${PORT}`);
});
// ============ Graceful Shutdown (Punkt 4) ============
function gracefulShutdown(signal) {
console.log(`\n[SHUTDOWN] ${signal} empfangen, fahre herunter...`);
// Stop LDAP sync timer
const { stopLDAPSync } = require('./ldapSync');
stopLDAPSync();
// Stop accepting new connections
server.close(async () => {
console.log('[SHUTDOWN] HTTP-Server gestoppt.');
// Close database connection
try {
if (db._type === 'postgres') {
await db.close();
} else {
db.close();
}
console.log('[SHUTDOWN] Datenbankverbindung geschlossen.');
} catch (err) {
console.error('[SHUTDOWN] Fehler beim Schließen der Datenbank:', err.message);
}
console.log('[SHUTDOWN] Erfolgreich heruntergefahren.');
process.exit(0);
});
// Force shutdown after 10 seconds if connections don't close
setTimeout(() => {
console.error('[SHUTDOWN] Erzwinge Shutdown nach Timeout.');
process.exit(1);
}, 10000);
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
} catch (err) {
console.error('[FATAL] Start fehlgeschlagen:', err.message);
process.exit(1);
}
}
start();