- 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
215 lines
7.3 KiB
JavaScript
215 lines
7.3 KiB
JavaScript
/**
|
|
* 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(); |