const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const compression = require('compression'); const path = require('path'); const cron = require('node-cron'); require('dotenv').config(); const logger = require('./utils/logger'); const db = require('./models/db'); const apiRoutes = require('./routes/api'); const app = express(); const PORT = process.env.PORT || 3003; // Trust proxy for Docker/reverse proxy environments 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 app.use(compression()); // Security middleware - disable upgrade-insecure-requests and HSTS for HTTP-only access app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com", "https://fonts.googleapis.com"], scriptSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com", "https://cdn.jsdelivr.net"], imgSrc: ["'self'", "data:", "https:", "blob:"], connectSrc: ["'self'", "https://unpkg.com", "https://tile.openstreetmap.org", "https://*.tile.openstreetmap.org", "https://router.project-osrm.org"], fontSrc: ["'self'", "https://fonts.gstatic.com", "https://unpkg.com"], upgradeInsecureRequests: null, // Disable for HTTP-only access }, }, hsts: false, // Disable HSTS for HTTP-only access (will be enabled by Cloudflare tunnel) crossOriginOpenerPolicy: false, // Disable for HTTP access originAgentCluster: false, // Disable for consistent behavior across HTTP/Tailscale access })); // Middleware app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Request logging app.use((req, res, next) => { const start = Date.now(); res.on('finish', () => { const duration = Date.now() - start; logger.info(`${req.method} ${req.path}`, { status: res.statusCode, duration: `${duration}ms`, ip: req.ip }); }); next(); }); // Static files app.use(express.static(path.join(__dirname, 'public'), { maxAge: process.env.NODE_ENV === 'production' ? '1d' : 0, etag: true })); // Health check endpoint app.get('/api/health', async (req, res) => { try { await db.query('SELECT 1'); res.json({ status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime() }); } catch (error) { logger.error('Health check failed', { error: error.message }); res.status(503).json({ status: 'unhealthy', error: error.message, timestamp: new Date().toISOString() }); } }); // API routes app.use('/api', apiRoutes); // Serve main page app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // Resource detail page app.get('/resource/:id', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // Weekly scraper cron job (runs every Sunday at 2 AM) if (process.env.ENABLE_CRON === 'true') { cron.schedule('0 2 * * 0', async () => { logger.info('Starting weekly data scrape'); try { const { runAllScrapers } = require('./scrapers/run-all'); await runAllScrapers(); logger.info('Weekly data scrape completed'); } catch (error) { logger.error('Weekly data scrape failed', { error: error.message }); } }); logger.info('Weekly cron job scheduled for Sundays at 2 AM'); } // Error handling middleware app.use((err, req, res, next) => { logger.error('Application error', { error: err.message, stack: err.stack, path: req.path }); res.status(err.status || 500).json({ error: 'Something went wrong', message: process.env.NODE_ENV === 'development' ? err.message : 'Internal server error' }); }); // 404 handler app.use((req, res) => { res.status(404).json({ error: 'Route not found' }); }); // Graceful shutdown process.on('SIGTERM', () => { logger.info('SIGTERM received, shutting down'); server.close(() => { db.end(); process.exit(0); }); }); const server = app.listen(PORT, () => { logger.info('Server started', { port: PORT, environment: process.env.NODE_ENV }); }); module.exports = app;