Files
ridebus-admin/backend/src/index.js
Zakhar Timoshenko f7f0b039dd Add Telegram cancellation import functionality with tests and documentation
- 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`.
2026-10-03 00:45:41 +03:00

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;