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'); // Import configuration and utilities const config = require('./config'); const logger = require('./utils/logger'); const { getCookieConfig } = require('./utils/helpers'); const { apiLimiter } = require('./middleware/rateLimiter'); // Initialize Express app const app = express(); // Trust proxy for Cloudflare app.set('trust proxy', true); // Cookie parser app.use(cookieParser()); // Session configuration app.use(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'); } })); // 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://unpkg.com"], connectSrc: ["'self'"] } } })); // CORS configuration app.use(cors({ origin: function(origin, callback) { // Allow requests with no origin (like mobile apps or curl requests) if (!origin) return callback(null, true); const allowedOrigins = config.cors.allowedOrigins; if (allowedOrigins.includes(origin) || allowedOrigins.includes('*')) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } }, credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'] })); // Body parser middleware app.use(express.json({ limit: '10mb' })); // Apply rate limiting to API routes app.use('/api/', apiLimiter); // Import and setup routes require('./routes')(app); // Error handling middleware app.use((err, req, res, next) => { logger.error('Unhandled error:', err); // Don't leak error details in production const message = config.isProduction ? 'Internal server error' : err.message || 'Internal server error'; res.status(err.status || 500).json({ success: false, error: message }); }); // Start server const server = app.listen(config.port, () => { logger.info(` ╔════════════════════════════════════════╗ ║ BNKops Map Server ║ ╠════════════════════════════════════════╣ ║ Status: Running ║ ║ Port: ${config.port} ║ ║ Environment: ${config.nodeEnv} ║ ║ Project ID: ${config.nocodb.projectId} ║ ║ Table ID: ${config.nocodb.tableId} ║ ║ Login Sheet: ${config.nocodb.loginSheetId || 'Not Configured'} ║ ║ 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;