const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const morgan = require('morgan'); const compression = require('compression'); const rateLimit = require('express-rate-limit'); require('dotenv').config(); const authRoutes = require('./routes/auth'); const userRoutes = require('./routes/users'); const propertyRoutes = require('./routes/properties'); const equipmentRoutes = require('./routes/equipment'); const nozzleRoutes = require('./routes/nozzles'); const productRoutes = require('./routes/products'); const applicationRoutes = require('./routes/applications'); const spreaderSettingsRoutes = require('./routes/spreaderSettings'); const productSpreaderSettingsRoutes = require('./routes/productSpreaderSettings'); const weatherRoutes = require('./routes/weather'); const weatherPublicRoutes = require('./routes/weatherPublic'); const adminRoutes = require('./routes/admin'); const mowingRoutes = require('./routes/mowing'); const { errorHandler } = require('./middleware/errorHandler'); const { authenticateToken } = require('./middleware/auth'); const app = express(); const PORT = process.env.PORT || 5000; // Trust proxy for rate limiting app.set('trust proxy', 1); // Security middleware app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'", "https://maps.googleapis.com"], styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"], fontSrc: ["'self'", "https://fonts.gstatic.com"], imgSrc: [ "'self'", "data:", "https:", "https://maps.googleapis.com", "https://maps.gstatic.com", "https://openweathermap.org", "https://*.openweathermap.org" ], connectSrc: ["'self'", "https://api.openweathermap.org"] } } })); // Rate limiting - relaxed for development const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 1000, // Increased to 1000 requests per 15 minutes for development message: 'Too many requests from this IP, please try again later.', standardHeaders: true, legacyHeaders: false, }); app.use(limiter); // Stricter rate limiting for auth routes, but skip low-risk polling endpoint const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 200, // dev-friendly message: 'Too many authentication attempts, please try again later.', standardHeaders: true, legacyHeaders: false, // Skip low-risk polling endpoint regardless of mount path skip: (req) => { const p = req.originalUrl || req.url || req.path || ''; return p.endsWith('/registration-status'); } }); // Middleware app.use(compression()); app.use(cors({ origin: process.env.FRONTEND_URL || 'http://localhost:3000', credentials: true })); app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); app.use(morgan('combined')); // Health check endpoint app.get('/health', (req, res) => { res.status(200).json({ status: 'OK', timestamp: new Date().toISOString(), uptime: process.uptime(), environment: process.env.NODE_ENV || 'development' }); }); // Routes // Public icon proxy must be mounted before protected weather routes app.use('/api/weather/icon', weatherPublicRoutes); app.use('/api/auth', authLimiter, authRoutes); app.use('/api/users', authenticateToken, userRoutes); app.use('/api/properties', authenticateToken, propertyRoutes); app.use('/api/equipment', authenticateToken, equipmentRoutes); app.use('/api/nozzles', authenticateToken, nozzleRoutes); app.use('/api/products', authenticateToken, productRoutes); app.use('/api/applications', authenticateToken, applicationRoutes); app.use('/api/mowing', authenticateToken, mowingRoutes); app.use('/api/spreader-settings', authenticateToken, spreaderSettingsRoutes); app.use('/api/product-spreader-settings', authenticateToken, productSpreaderSettingsRoutes); app.use('/api/weather', authenticateToken, weatherRoutes); app.use('/api/admin', authenticateToken, adminRoutes); // 404 handler app.use('*', (req, res) => { res.status(404).json({ success: false, message: 'API endpoint not found' }); }); // Global error handler app.use(errorHandler); // Graceful shutdown process.on('SIGTERM', () => { console.log('SIGTERM received, shutting down gracefully'); process.exit(0); }); process.on('SIGINT', () => { console.log('SIGINT received, shutting down gracefully'); process.exit(0); }); app.listen(PORT, '0.0.0.0', () => { console.log(`TurfTracker API server running on port ${PORT}`); console.log(`Environment: ${process.env.NODE_ENV || 'development'}`); }); // Disable etags and set no-store by default to avoid stale cached API responses app.set('etag', false); app.use((req, res, next) => { // Allow icon proxy to control its own caching if (req.path && req.path.startsWith('/api/weather/icon')) return next(); res.setHeader('Cache-Control', 'no-store'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); next(); });