admin 4d8b9effd0 feat(blog): add detailed update on Influence and Map app developments since August
A bunch of udpates to the listmonk sync to add influence to it
2025-10-25 12:45:35 -06:00

249 lines
7.2 KiB
JavaScript

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, csrfProtection } = 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
// CORS configuration - Allow credentials for cookie-based CSRF
app.use(cors({
origin: true, // Allow requests from same origin
credentials: true // Allow cookies to be sent
}));
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' });
}
});
// Config endpoint - expose APP_URL to client
app.get('/api/config', (req, res) => {
res.json({
appUrl: process.env.APP_URL || `http://localhost:${PORT}`
});
});
// CSRF token endpoint - Needs CSRF middleware to generate token
app.get('/api/csrf-token', csrfProtection, (req, res) => {
logger.info('CSRF token endpoint hit');
getCsrfToken(req, res);
});
logger.info('CSRF token endpoint registered at /api/csrf-token');
// Auth routes must come before generic API routes
app.use('/api/auth', authRoutes);
// Generic API routes (catches all /api/*)
app.use('/api', apiRoutes);
// 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;