- Implemented a comprehensive health check utility to monitor system dependencies including NocoDB, SMTP, Represent API, disk space, and memory usage. - Created a logger utility using Winston for structured logging with daily rotation and various log levels. - Developed a metrics utility using Prometheus client to track application performance metrics such as email sends, HTTP requests, and user activity. - Added a backup script for automated backups of NocoDB data, uploaded files, and environment configurations with optional S3 support. - Introduced a toggle script to switch between development (MailHog) and production (ProtonMail) SMTP configurations.
238 lines
6.7 KiB
JavaScript
238 lines
6.7 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 } = 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; |