151 lines
5.0 KiB
JavaScript
151 lines
5.0 KiB
JavaScript
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();
|
|
});
|