const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const session = require('express-session'); const compression = require('compression'); const cookieParser = require('cookie-parser'); const path = require('path'); require('dotenv').config(); const logger = require('./utils/logger'); const metrics = require('./utils/metrics'); const healthCheck = require('./utils/health-check'); const { conditionalCsrfProtection, getCsrfToken } = require('./middleware/csrf'); const apiRoutes = require('./routes/api'); const authRoutes = require('./routes/auth'); const { requireAdmin, requireAuth } = require('./middleware/auth'); const app = express(); const PORT = process.env.PORT || 3333; // Trust proxy for Docker/reverse proxy environments // Only trust Docker internal networks for better security app.set('trust proxy', ['127.0.0.1', '::1', '172.16.0.0/12', '192.168.0.0/16', '10.0.0.0/8']); // Compression middleware for better performance app.use(compression({ filter: (req, res) => { if (req.headers['x-no-compression']) { return false; } return compression.filter(req, res); }, level: 6 // Balance between speed and compression ratio })); // Security middleware app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com"], scriptSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com", "https://static.cloudflareinsights.com"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'", "https://cloudflareinsights.com"], }, }, })); // Middleware app.use(cors()); app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); app.use(cookieParser()); // Metrics middleware - track all HTTP requests app.use(metrics.middleware); // Request logging middleware app.use((req, res, next) => { const start = Date.now(); res.on('finish', () => { const duration = Date.now() - start; logger.logRequest(req, res, duration); }); next(); }); // Session configuration - PRODUCTION HARDENED app.use(session({ secret: process.env.SESSION_SECRET || 'influence-campaign-secret-key-change-in-production', resave: false, saveUninitialized: false, name: 'influence.sid', // Custom session name for security cookie: { secure: process.env.NODE_ENV === 'production' && process.env.HTTPS === 'true', // HTTPS only in production httpOnly: true, // Prevent JavaScript access maxAge: 3600000, // 1 hour (reduced from 24 hours) sameSite: 'strict' // CSRF protection } })); // CSRF Protection - Applied conditionally app.use(conditionalCsrfProtection); // Static files with proper caching app.use(express.static(path.join(__dirname, 'public'), { maxAge: process.env.NODE_ENV === 'production' ? '1d' : 0, etag: true, lastModified: true, setHeaders: (res, filePath) => { // Cache images and assets longer if (filePath.match(/\.(jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$/)) { res.setHeader('Cache-Control', 'public, max-age=604800'); // 7 days } // Cache CSS and JS for 1 day else if (filePath.match(/\.(css|js)$/)) { res.setHeader('Cache-Control', 'public, max-age=86400'); // 1 day } } })); // Health check endpoint - COMPREHENSIVE app.get('/api/health', async (req, res) => { try { const health = await healthCheck.checkAll(); const statusCode = health.status === 'healthy' ? 200 : health.status === 'degraded' ? 200 : 503; res.status(statusCode).json(health); } catch (error) { logger.error('Health check failed', { error: error.message }); res.status(503).json({ status: 'unhealthy', error: error.message, timestamp: new Date().toISOString() }); } }); // Metrics endpoint for Prometheus app.get('/api/metrics', async (req, res) => { try { res.set('Content-Type', metrics.getContentType()); const metricsData = await metrics.getMetrics(); res.end(metricsData); } catch (error) { logger.error('Metrics endpoint failed', { error: error.message }); res.status(500).json({ error: 'Failed to generate metrics' }); } }); // CSRF token endpoint app.get('/api/csrf-token', getCsrfToken); // Routes app.use('/api/auth', authRoutes); app.use('/api', apiRoutes); // Config endpoint - expose APP_URL to client app.get('/api/config', (req, res) => { res.json({ appUrl: process.env.APP_URL || `http://localhost:${PORT}` }); }); // Serve the main page app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // Serve login page app.get('/login.html', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'login.html')); }); // Serve admin panel (protected) app.get('/admin.html', requireAdmin, (req, res) => { res.sendFile(path.join(__dirname, 'public', 'admin.html')); }); // Serve the admin page (protected) app.get('/admin', requireAdmin, (req, res) => { res.sendFile(path.join(__dirname, 'public', 'admin.html')); }); // Serve user dashboard (protected) app.get('/dashboard.html', requireAuth, (req, res) => { res.sendFile(path.join(__dirname, 'public', 'dashboard.html')); }); app.get('/dashboard', requireAuth, (req, res) => { res.sendFile(path.join(__dirname, 'public', 'dashboard.html')); }); // Serve campaign landing pages app.get('/campaign/:slug', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'campaign.html')); }); // Serve campaign pages app.get('/campaign/:slug', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'campaign.html')); }); // Error handling middleware app.use((err, req, res, next) => { logger.error('Application error', { error: err.message, stack: err.stack, path: req.path, method: req.method, ip: req.ip }); res.status(err.status || 500).json({ error: 'Something went wrong!', message: process.env.NODE_ENV === 'development' ? err.message : 'Internal server error', ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) }); }); // 404 handler app.use((req, res) => { logger.warn('Route not found', { path: req.path, method: req.method, ip: req.ip }); res.status(404).json({ error: 'Route not found' }); }); // Graceful shutdown process.on('SIGTERM', () => { logger.info('SIGTERM received, shutting down gracefully'); server.close(() => { logger.info('Server closed'); process.exit(0); }); }); process.on('SIGINT', () => { logger.info('SIGINT received, shutting down gracefully'); server.close(() => { logger.info('Server closed'); process.exit(0); }); }); const server = app.listen(PORT, () => { logger.info('Server started', { port: PORT, environment: process.env.NODE_ENV, nodeVersion: process.version }); }); module.exports = app;