const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const compression = require('compression'); require('dotenv').config(); const logger = require('./config/logger'); const pool = require('./config/database'); const apiRoutes = require('./routes/api'); const { authenticateApiKey } = require('./middleware/auth'); const createRateLimiter = require('./middleware/rateLimiter'); const { ensureRealtimeSchema } = require('./services/realtimeSchema'); const { attachRealtimeServer, closeRealtimeServer } = require('./services/realtimeService'); const { ensureBootstrapAdmin } = require('./utils/bootstrapAdmin'); const { startAlertScheduler } = require('./services/alertSchedulerService'); const { startTelegramImporter } = require('./services/telegramImportService'); const app = express(); const PORT = process.env.PORT || 3000; let server; let stopAlertScheduler = () => {}; let stopTelegramImporter = () => Promise.resolve(); // Security middleware app.use(helmet({ crossOriginResourcePolicy: false, })); app.use(cors({ origin: process.env.CORS_ORIGIN || '*', credentials: true })); // Body parsing app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Compression app.use(compression()); // Static files for avatars const path = require('path'); app.use('/uploads', express.static(path.join(__dirname, '../public/uploads'))); // Logging middleware app.use((req, res, next) => { logger.info(`${req.method} ${req.path}`, { ip: req.ip, userAgent: req.get('user-agent') }); next(); }); // Health check (no auth required) app.get('/health', (req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString(), version: '1.0.0' }); }); // Auth routes (no API key required) const authRouter = require('./routes/auth'); app.use('/api/v1/auth', createRateLimiter(100, 3600000), // 100 requests per hour for auth authRouter ); // Upload routes (JWT auth required, no API key) - TEMPORARILY DISABLED // Uncomment after installing multer: npm install multer try { const uploadRouter = require('./routes/upload'); app.use('/api/v1/upload', createRateLimiter(50, 3600000), // 50 uploads per hour uploadRouter ); logger.info('Upload routes enabled'); } catch (error) { logger.warn('Upload routes disabled - multer not installed. Run: npm install multer'); } // Passenger telemetry has its own Firebase ID-token authentication inside apiRoutes. // All other API routes keep the existing API-key/custom-JWT contract. app.use('/api/v1', (req, res, next) => { if (req.path === '/passenger-telemetry/ingest') { return next(); } return authenticateApiKey(req, res, next); }, createRateLimiter(1000, 3600000), // 1000 requests per hour apiRoutes ); // 404 handler app.use((req, res) => { res.status(404).json({ error: 'Not Found', message: `Route ${req.method} ${req.path} not found` }); }); // Global error handler app.use((err, req, res, next) => { logger.error('Unhandled error:', err); // Multer errors if (err.code === 'LIMIT_FILE_SIZE') { return res.status(400).json({ error: 'Размер файла не должен превышать 5 МБ.' }); } if (err.message && (err.message.includes('Only image files are allowed') || err.message.includes('Only JPEG, PNG, and WEBP images are allowed'))) { return res.status(400).json({ error: 'Поддерживаются только изображения JPG, PNG и WEBP.' }); } res.status(500).json({ error: 'Internal Server Error', message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong' }); }); // Graceful shutdown function shutdown(signal) { logger.info('SIGTERM signal received: closing HTTP server'); logger.info(`${signal} signal received: closing HTTP server`); closeRealtimeServer(); stopAlertScheduler(); stopTelegramImporter().catch(() => {}); if (!server) { process.exit(0); } server.close(() => { logger.info('HTTP server closed'); process.exit(0); }); } process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT')); function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function waitForDatabase({ attempts = 30, delayMs = 1000 } = {}) { for (let attempt = 1; attempt <= attempts; attempt += 1) { try { await pool.query('SELECT 1'); return; } catch (error) { if (attempt === attempts) { throw error; } logger.warn(`Database is not ready yet (${attempt}/${attempts}); retrying...`); await sleep(delayMs); } } } async function startServer() { try { await waitForDatabase(); await ensureRealtimeSchema(); await ensureBootstrapAdmin(); stopAlertScheduler = startAlertScheduler(); stopTelegramImporter = startTelegramImporter(); server = app.listen(PORT, () => { logger.info(`Server is running on port ${PORT}`); logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`); logger.info(`API Base: http://localhost:${PORT}/api/v1`); }); attachRealtimeServer(server); } catch (error) { logger.error('Failed to start server:', error); process.exit(1); } } startServer(); module.exports = app;