const express = require('express'); const mongoose = require('mongoose'); const cors = require('cors'); const dotenv = require('dotenv'); const path = require('path'); const http = require('http'); // Загрузить переменные окружения ДО импорта config dotenv.config({ path: path.join(__dirname, '.env') }); const { generalLimiter } = require('./middleware/rateLimiter'); const { initRedis } = require('./utils/redis'); const { initWebSocket } = require('./websocket'); const config = require('./config'); // Security middleware const { helmetConfig, sanitizeMongo, xssProtection, hppProtection, ddosProtection } = require('./middleware/security'); const { sanitizeInput } = require('./middleware/validator'); const { requestLogger, logSecurityEvent } = require('./middleware/logger'); const { errorHandler, notFoundHandler } = require('./middleware/errorHandler'); const { scheduleAvatarUpdates } = require('./jobs/avatarUpdater'); const { startServerMonitorBot } = require('./bots/serverMonitor'); const ERROR_SUPPORT_SUFFIX = ' Сообщите об ошибке в https://t.me/NakamaReportbot'; const app = express(); const server = http.createServer(app); // Trust proxy для правильного IP (для rate limiting за nginx/cloudflare) if (config.isProduction()) { app.set('trust proxy', 1); } // Security headers (Helmet) app.use(helmetConfig); // CORS настройки const corsOptions = { origin: config.corsOrigin === '*' ? '*' : config.corsOrigin.split(','), credentials: true, optionsSuccessStatus: 200, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'x-telegram-init-data'], maxAge: 86400 // 24 часа }; app.use(cors(corsOptions)); // Body parsing с ограничениями app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); // Security middleware app.use(sanitizeMongo); // Защита от NoSQL injection app.use(xssProtection); // Защита от XSS app.use(hppProtection); // Защита от HTTP Parameter Pollution // Input sanitization app.use(sanitizeInput); // Request logging app.use(requestLogger); // Static files app.use('/uploads', express.static(path.join(__dirname, config.uploadsDir))); // Дополнение ошибок сообщением о канале связи app.use((req, res, next) => { const originalJson = res.json.bind(res); res.json = (body) => { const appendSuffix = (obj) => { if (!obj || typeof obj !== 'object') { return; } // Список ошибок, к которым НЕ нужно добавлять суффикс const skipSuffixMessages = [ 'Загрузите хотя бы одно изображение', 'Не удалось опубликовать в канал', 'Публиковать в канал могут только админы', 'Требуется авторизация', 'Требуются права', 'Неверный код подтверждения', 'Код подтверждения истёк', 'Номер админа уже занят', 'Пользователь не найден', 'Администратор не найден' ]; const shouldSkipSuffix = (text) => { if (!text || typeof text !== 'string') return false; return skipSuffixMessages.some(msg => text.includes(msg)); }; if (typeof obj.error === 'string' && !obj.error.includes(ERROR_SUPPORT_SUFFIX) && !shouldSkipSuffix(obj.error)) { obj.error += ERROR_SUPPORT_SUFFIX; } if (typeof obj.message === 'string' && res.statusCode >= 400 && !obj.message.includes(ERROR_SUPPORT_SUFFIX) && !shouldSkipSuffix(obj.message)) { obj.message += ERROR_SUPPORT_SUFFIX; } if (Array.isArray(obj.errors)) { obj.errors = obj.errors.map((item) => { if (typeof item === 'string') { if (shouldSkipSuffix(item)) return item; return item.includes(ERROR_SUPPORT_SUFFIX) ? item : `${item}${ERROR_SUPPORT_SUFFIX}`; } if (item && typeof item === 'object' && typeof item.message === 'string' && !item.message.includes(ERROR_SUPPORT_SUFFIX) && !shouldSkipSuffix(item.message)) { return { ...item, message: `${item.message}${ERROR_SUPPORT_SUFFIX}` }; } return item; }); } }; if (Array.isArray(body)) { body.forEach((item) => appendSuffix(item)); } else { appendSuffix(body); } return originalJson(body); }; next(); }); // DDoS защита (применяется перед другими rate limiters) app.use(ddosProtection); // Rate limiting app.use('/api', generalLimiter); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'ok', environment: config.nodeEnv, timestamp: new Date().toISOString() }); }); // MongoDB подключение mongoose.connect(config.mongoUri) .then(() => { console.log(`✅ MongoDB подключена: ${config.mongoUri.replace(/\/\/.*@/, '//***@')}`); // Инициализировать Redis (опционально) if (config.redisUrl) { initRedis().catch(err => console.log('⚠️ Redis недоступен, работаем без кэша')); } else { console.log('ℹ️ Redis не настроен, кэширование отключено'); } }) .catch(err => console.error('❌ Ошибка MongoDB:', err)); // Routes app.use('/api/auth', require('./routes/auth')); app.use('/api/posts', require('./routes/posts')); app.use('/api/users', require('./routes/users')); app.use('/api/notifications', require('./routes/notifications')); app.use('/api/search', require('./routes/search')); app.use('/api/search/posts', require('./routes/postSearch')); app.use('/api/moderation', require('./routes/moderation')); app.use('/api/statistics', require('./routes/statistics')); app.use('/api/bot', require('./routes/bot')); app.use('/api/mod-app', require('./routes/modApp')); // Базовый роут app.get('/', (req, res) => { res.json({ message: 'Nakama API работает' }); }); // 404 handler app.use(notFoundHandler); // Error handler (должен быть последним) app.use(errorHandler); // Инициализировать WebSocket initWebSocket(server); scheduleAvatarUpdates(); startServerMonitorBot(); // Graceful shutdown process.on('SIGTERM', () => { console.log('SIGTERM получен, закрываем сервер...'); server.close(() => { console.log('Сервер закрыт'); mongoose.connection.close(false, () => { console.log('MongoDB соединение закрыто'); process.exit(0); }); }); }); server.listen(config.port, '0.0.0.0', () => { console.log(`🚀 Сервер запущен`); console.log(` Порт: ${config.port}`); console.log(` Окружение: ${config.nodeEnv}`); console.log(` API: http://0.0.0.0:${config.port}/api`); if (config.isDevelopment()) { console.log(` Frontend: ${config.frontendUrl}`); } if (!config.telegramBotToken) { console.warn('⚠️ TELEGRAM_BOT_TOKEN не установлен!'); console.warn(' Установите переменную окружения: TELEGRAM_BOT_TOKEN=ваш_токен'); console.warn(' Получите токен от @BotFather в Telegram'); console.warn(` Проверьте .env файл в: ${path.join(__dirname, '.env')}`); console.warn(` Текущий process.env.TELEGRAM_BOT_TOKEN: ${process.env.TELEGRAM_BOT_TOKEN ? 'установлен' : 'НЕ установлен'}`); } else { console.log(`✅ Telegram Bot инициализирован`); console.log(` Токен: ${config.telegramBotToken.substring(0, 10)}...`); } });