251 lines
8.2 KiB
JavaScript
251 lines
8.2 KiB
JavaScript
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 (skip if DB_INITIALIZED is set)
|
||
if (!process.env.DB_INITIALIZED) {
|
||
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
|
||
}
|
||
} else {
|
||
console.log('ℹ️ Database already initialized by setup script, skipping migrations and seeding');
|
||
}
|
||
|
||
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 };
|