147 lines
4.1 KiB
JavaScript
147 lines
4.1 KiB
JavaScript
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;
|