const express = require('express'); const path = require('path'); const cors = require('cors'); const helmet = require('helmet'); const morgan = require('morgan'); const compression = require('compression'); const rateLimit = require('express-rate-limit'); const { createServer } = require('http'); const { Server } = require('socket.io'); require('dotenv').config(); const { sequelize } = require('./models'); const routes = require('./routes'); const { initializeSocketHandlers } = require('./services/socketService'); const AlertService = require('./services/alertService'); const DeviceHealthService = require('./services/deviceHealthService'); const { initializeHealthService } = require('./routes/deviceHealth'); const seedDatabase = require('./seedDatabase'); const errorHandler = require('./middleware/errorHandler'); const { apiDebugMiddleware } = require('./utils/apiDebugLogger'); const IPRestrictionMiddleware = require('./middleware/ip-restriction'); const { Umzug, SequelizeStorage } = require('umzug'); const app = express(); // Trust proxy headers for getting real client IPs behind nginx // Trust only the first proxy (nginx) for security app.set('trust proxy', 1); const server = createServer(app); const io = new Server(server, { path: '/ws', cors: { origin: [ process.env.CORS_ORIGIN || "http://localhost:3000", "https://selfservice.cqers.com", "http://localhost:3001" ], methods: ["GET", "POST", "PUT", "DELETE"] } }); // Rate limiting (exclude detections endpoint for testing) const limiter = rateLimit({ windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000, // 15 minutes max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 1000, // Increased from 100 to 1000 message: 'Too many requests from this IP, please try again later.', // Custom key generator to handle proxy headers properly keyGenerator: (req) => { // Get the real IP from proxy headers, with fallback return req.ip || req.connection.remoteAddress || 'unknown'; }, skip: (req) => { // Skip rate limiting for drone detection endpoints during testing return req.path.includes('/detections') || req.path.includes('/detectors'); }, // Skip failed requests (don't count them against the limit) skipFailedRequests: true, // Skip successful requests (only count errors/abuse) skipSuccessfulRequests: false }); // Middleware app.use(helmet()); app.use(compression()); app.use(morgan('combined')); app.use(cors({ origin: [ process.env.CORS_ORIGIN || "http://localhost:3000", "https://selfservice.cqers.com", "http://localhost:3001" ], credentials: true })); app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true })); // Serve uploaded files app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); // API Debug logging (only when API_DEBUG=true) if (process.env.API_DEBUG === 'true') { console.log('šŸ› API Debug logging enabled'); app.use('/api', apiDebugMiddleware); } app.use('/api/', limiter); // IP Restriction middleware (before routes) const ipRestriction = new IPRestrictionMiddleware(); app.use((req, res, next) => ipRestriction.checkIPRestriction(req, res, next)); // Make io available to routes app.use((req, res, next) => { req.io = io; next(); }); // Routes app.use('/api', routes); // Health check endpoints app.get('/health', (req, res) => { const response = { status: 'OK', timestamp: new Date().toISOString(), environment: process.env.NODE_ENV }; // Log health check if debugging enabled if (process.env.API_DEBUG === 'true') { const { ApiDebugLogger } = require('./utils/apiDebugLogger'); const logger = new ApiDebugLogger(); logger.log('GET', '/health', 200, {}, response); } res.status(200).json(response); }); app.use('/api/health', require('./routes/health')); // Error handling app.use(errorHandler); // Socket.IO initialization initializeSocketHandlers(io); const PORT = process.env.PORT || 3001; // Migration runner const runMigrations = async () => { const umzug = new Umzug({ migrations: { glob: 'migrations/*.js', resolve: ({ name, path }) => { const migration = require(path); return { name, up: async () => migration.up(sequelize.getQueryInterface(), require('sequelize')), down: async () => migration.down && migration.down(sequelize.getQueryInterface(), require('sequelize')) }; } }, storage: new SequelizeStorage({ sequelize }), logger: console, }); console.log('Running database migrations...'); await umzug.up(); console.log('Migrations completed successfully.'); }; // Database connection and server startup async function startServer() { try { await sequelize.authenticate(); console.log('Database connected successfully.'); // STEP 1: Sync database first to create base tables try { // Use alter: false to prevent destructive changes in production await sequelize.sync({ force: false, alter: false }); console.log('Database synchronized.'); } catch (syncError) { console.error('Database sync error:', syncError); // If sync fails, try force sync (this will drop and recreate tables) console.log('Attempting force sync...'); await sequelize.sync({ force: true }); console.log('Database force synchronized.'); } // STEP 2: Run migrations after tables exist try { await runMigrations(); } catch (migrationError) { console.error('Migration error:', migrationError); throw migrationError; // Fatal error - don't continue } // STEP 3: Seed database with initial data try { await seedDatabase(); } catch (seedError) { console.error('Seeding error:', seedError); throw seedError; // Fatal error - don't continue } server.listen(PORT, () => { console.log('\nšŸš€ Drone Detection System Started Successfully!'); console.log('================================================'); console.log(`šŸ“Š Environment: ${process.env.NODE_ENV || 'development'}`); console.log(`🌐 Server Port: ${PORT}`); console.log(`šŸ’¾ Database: ${process.env.DB_HOST}:${process.env.DB_PORT}`); console.log(`šŸ”“ Redis: ${process.env.REDIS_HOST || 'localhost'}:${process.env.REDIS_PORT || 6379}`); // Initialize AlertService to check SMS status const alertService = new AlertService(); if (alertService.twilioEnabled) { console.log('šŸ“± SMS Alerts: āœ… Enabled'); } else { console.log('šŸ“± SMS Alerts: āš ļø Disabled (no Twilio credentials)'); } // Start periodic clear alert checking (every 2 minutes) setInterval(async () => { try { await alertService.checkClearedAlerts(); } catch (error) { console.error('Error checking cleared alerts:', error); } }, 2 * 60 * 1000); // Check every 2 minutes console.log('šŸ”„ Clear alert monitoring: āœ… Started'); // Initialize Device Health Monitoring Service const deviceHealthService = new DeviceHealthService(); initializeHealthService(deviceHealthService); // Make it available to API routes deviceHealthService.start(); console.log('šŸ„ Device health monitoring: āœ… Started'); // Graceful shutdown for device health service process.on('SIGTERM', () => { console.log('SIGTERM received, shutting down gracefully'); deviceHealthService.stop(); process.exit(0); }); process.on('SIGINT', () => { console.log('SIGINT received, shutting down gracefully'); deviceHealthService.stop(); process.exit(0); }); console.log(`šŸ“Š Health check: http://localhost:${PORT}/health`); console.log(`🌐 API endpoint: http://localhost:${PORT}/api`); console.log('================================================\n'); }); } catch (error) { console.error('Unable to start server:', error); process.exit(1); } } startServer(); module.exports = { app, server, io };