// At the very top of the file, before any requires const startTime = Date.now(); // Use a more robust check for duplicate execution if (global.__serverInitialized) { console.log(`[INIT] Server already initialized - EXITING`); return; } global.__serverInitialized = true; // Prevent duplicate execution if (require.main !== module) { console.log('[INIT] Server.js being imported, not executed directly - EXITING'); return; } const express = require('express'); const helmet = require('helmet'); const cors = require('cors'); const session = require('express-session'); const cookieParser = require('cookie-parser'); const crypto = require('crypto'); const fetch = require('node-fetch'); // Import configuration and utilities const config = require('./config'); const logger = require('./utils/logger'); const { getCookieConfig } = require('./utils/helpers'); const { apiLimiter } = require('./middleware/rateLimiter'); const { cacheBusting } = require('./utils/cacheBusting'); const { initializeEmailService } = require('./services/email'); // Initialize Express app - only create once if (global.__expressApp) { console.log('[INIT] Express app already created - EXITING'); return; } const app = express(); global.__expressApp = app; // Trust proxy for Cloudflare app.set('trust proxy', true); // Cookie parser app.use(cookieParser()); // Session configuration - only initialize once const sessionMiddleware = session({ secret: config.session.secret, resave: false, saveUninitialized: false, cookie: getCookieConfig(), name: 'nocodb-map-session', genid: (req) => { // Use a custom session ID generator to avoid conflicts return crypto.randomBytes(16).toString('hex'); } }); app.use(sessionMiddleware); // Build dynamic CSP configuration const buildConnectSrc = () => { const sources = ["'self'"]; // Add NocoDB API URL if (config.nocodb.apiUrl) { try { const nocodbUrl = new URL(config.nocodb.apiUrl); sources.push(`${nocodbUrl.protocol}//${nocodbUrl.host}`); } catch (e) { // Invalid URL, skip } } // Add Edmonton Open Data Portal sources.push('https://data.edmonton.ca'); // Add Nominatim for geocoding sources.push('https://nominatim.openstreetmap.org'); // Add localhost for development if (!config.isProduction) { sources.push('http://localhost:*'); sources.push('ws://localhost:*'); } return sources; }; // Security middleware app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com"], scriptSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com", "https://cdn.jsdelivr.net"], imgSrc: ["'self'", "data:", "https://*.tile.openstreetmap.org", "https://tiles.stadiamaps.com", "https://unpkg.com"], connectSrc: buildConnectSrc() } } })); // CORS configuration app.use(cors({ origin: function(origin, callback) { // Allow requests with no origin (like Postman or server-to-server) if (!origin) return callback(null, true); // In production, be more restrictive if (config.isProduction) { const allowedOrigins = [ `https://${config.domain}`, `https://map.${config.domain}`, config.mkdocs.url, // Use configured MkDocs URL instead of hardcoded subdomain `https://admin.${config.domain}` ]; if (allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } } else { // In development, allow localhost callback(null, true); } }, credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'] })); // Body parser middleware app.use(express.json({ limit: '10mb' })); // Add cache busting middleware for HTML content app.use(cacheBusting.htmlMiddleware()); // Add cache headers middleware for static files app.use(cacheBusting.staticCacheMiddleware()); // Serve static files with proper cache headers app.use(express.static('public')); // Apply rate limiting to API routes app.use('/api/', apiLimiter); // Cache busting version endpoint app.get('/api/version', (req, res) => { res.json({ version: cacheBusting.getVersion(), timestamp: new Date().toISOString() }); }); // Proxy endpoint for MkDocs search app.get('/api/docs-search', async (req, res) => { try { const docsUrl = config.isProduction ? `${config.mkdocs.searchUrl}/search/search_index.json` : 'http://localhost:8000/search/search_index.json'; const response = await fetch(docsUrl); const data = await response.json(); res.json(data); } catch (error) { logger.error('Failed to fetch docs search index:', error); res.status(500).json({ error: 'Failed to fetch search index' }); } }); // Initialize email service - only once if (!global.__emailInitialized) { initializeEmailService(); global.__emailInitialized = true; } // Import and setup routes require('./routes')(app); // Error handling middleware app.use((err, req, res, next) => { logger.error('Unhandled error:', err); res.status(500).json({ error: 'Internal server error', message: config.isProduction ? 'An error occurred' : err.message }); }); // Add a simple health check endpoint early in the middleware stack (before other middlewares) app.get('/health', (req, res) => { res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime() }); }); // Only start server if not already started if (!global.__serverStarted) { global.__serverStarted = true; const server = app.listen(config.port, () => { logger.info(` ╔════════════════════════════════════════╗ ║ BNKops Map Server ║ ╠════════════════════════════════════════╣ ║ Status: Running ║ ║ Port: ${config.port} ║ ║ Environment: ${config.isProduction ? 'production' : 'development'} ║ ║ Project ID: ${config.nocodb.projectId} ║ ║ Table ID: ${config.nocodb.tableId} ║ ║ Login Sheet: ${config.nocodb.loginSheetId} ║ ║ PID: ${process.pid} ║ ║ Time: ${new Date().toISOString()} ║ ╚════════════════════════════════════════╝ `); }); // Graceful shutdown process.on('SIGTERM', () => { logger.info('SIGTERM signal received: closing HTTP server'); server.close(() => { logger.info('HTTP server closed'); process.exit(0); }); }); process.on('SIGINT', () => { logger.info('SIGINT signal received: closing HTTP server'); server.close(() => { logger.info('HTTP server closed'); process.exit(0); }); }); // Handle uncaught exceptions process.on('uncaughtException', (err) => { logger.error('Uncaught Exception:', err); process.exit(1); }); // Handle unhandled promise rejections process.on('unhandledRejection', (reason, promise) => { logger.error('Unhandled Rejection at:', promise, 'reason:', reason); process.exit(1); }); } module.exports = app;