- Implemented Telegram cancellation parsing logic in `telegramCancellationParser.js`. - Created unit tests for cancellation parsing in `telegramCancellationParser.test.js`. - Developed integration tests for Telegram import service in `telegramImport.integration.test.js`. - Added API tests for Telegram import endpoints in `telegramImportApi.test.js`. - Introduced service tests for Telegram import logic in `telegramImportService.test.js`. - Documented the Telegram cancellation import process in `telegram-cancellation-import.md`. - Built a React component for managing Telegram imports in `TelegramImportPanel.jsx`.
192 lines
5.3 KiB
JavaScript
192 lines
5.3 KiB
JavaScript
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;
|