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:
215
backend/server.js
Normal file
215
backend/server.js
Normal 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();
|
||||
Reference in New Issue
Block a user