224 lines
7.2 KiB
JavaScript
224 lines
7.2 KiB
JavaScript
// Prevent duplicate execution
|
|
if (require.main !== module) {
|
|
console.log('Server.js being imported, not executed directly');
|
|
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');
|
|
|
|
// Debug: Check if server.js is being loaded multiple times
|
|
const serverInstanceId = Math.random().toString(36).substr(2, 9);
|
|
console.log(`[DEBUG] Server.js PID:${process.pid} instance ${serverInstanceId} loading at ${new Date().toISOString()}`);
|
|
|
|
// 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
|
|
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');
|
|
}
|
|
}));
|
|
|
|
// Build dynamic CSP configuration
|
|
const buildConnectSrc = () => {
|
|
const sources = ["'self'"];
|
|
|
|
// Add MkDocs URLs from config
|
|
if (config.mkdocs?.url) {
|
|
sources.push(config.mkdocs.url);
|
|
}
|
|
|
|
// Add localhost ports from environment
|
|
const mkdocsPort = process.env.MKDOCS_PORT || '4000';
|
|
const mkdocsSitePort = process.env.MKDOCS_SITE_SERVER_PORT || '4002';
|
|
|
|
sources.push(`http://localhost:${mkdocsPort}`);
|
|
sources.push(`http://localhost:${mkdocsSitePort}`);
|
|
|
|
// Add City of Edmonton Socrata API
|
|
sources.push('https://data.edmonton.ca');
|
|
|
|
// Add Stadia Maps for better tile coverage
|
|
sources.push('https://tiles.stadiamaps.com');
|
|
|
|
// Add production domains if in production
|
|
if (config.isProduction || process.env.NODE_ENV === 'production') {
|
|
// Add the main domain from environment
|
|
const mainDomain = process.env.DOMAIN || 'cmlite.org';
|
|
sources.push(`https://${mainDomain}`);
|
|
sources.push('https://cmlite.org'); // Fallback
|
|
}
|
|
|
|
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 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' }));
|
|
|
|
// 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 mkdocsUrl = config.mkdocs?.url || `http://localhost:${process.env.MKDOCS_SITE_SERVER_PORT || '4002'}`;
|
|
logger.info(`Fetching search index from: ${mkdocsUrl}/search/search_index.json`);
|
|
|
|
const response = await fetch(`${mkdocsUrl}/search/search_index.json`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch search index: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
res.json(data);
|
|
} catch (error) {
|
|
logger.error('Error fetching search index:', error);
|
|
res.status(500).json({ error: 'Failed to fetch search index' });
|
|
}
|
|
});
|
|
|
|
// Initialize email service
|
|
initializeEmailService();
|
|
|
|
// 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'} ║
|
|
║ 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; |