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 { initMinioClient, checkConnection: checkMinioConnection } = require('./utils/minio'); const { printMinioConfig } = require('./utils/minioDebug'); const config = require('./config'); // Security middleware const { helmetConfig, sanitizeMongo, xssProtection, hppProtection, ddosProtection } = require('./middleware/security'); const { sanitizeInput } = require('./middleware/validator'); const { requestLogger, logSecurityEvent, log, logSuccess, logError } = 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 подключение log('info', 'Подключение к MongoDB...', { uri: config.mongoUri.replace(/\/\/.*@/, '//***@') }); mongoose.connect(config.mongoUri) .then(async () => { const dbHost = config.mongoUri.includes('localhost') || config.mongoUri.includes('127.0.0.1') ? 'Local' : config.mongoUri.match(/\/\/([^:\/]+)/)?.[1] || 'Remote'; logSuccess('MongoDB успешно подключена', { host: dbHost, database: mongoose.connection.name, uri: config.mongoUri.replace(/\/\/.*@/, '//***@') }); // MongoDB события mongoose.connection.on('error', (err) => { logError('MongoDB connection error', err); }); mongoose.connection.on('disconnected', () => { log('warn', 'MongoDB отключена'); }); mongoose.connection.on('reconnected', () => { logSuccess('MongoDB переподключена'); }); // Инициализировать Redis (опционально) if (config.redisUrl) { try { log('info', 'Подключение к Redis...'); await initRedis(); logSuccess('Redis подключен', { url: config.redisUrl.replace(/\/\/.*@/, '//***@') }); } catch (err) { log('warn', 'Redis недоступен, работаем без кэша', { error: err.message }); } } else { log('info', 'Redis не настроен, кэширование отключено'); } // Инициализировать MinIO (опционально) if (config.minio.enabled) { try { log('info', 'Инициализация MinIO...'); // Вывести конфигурацию и проверки printMinioConfig(); initMinioClient(); const minioOk = await checkMinioConnection(); if (minioOk) { logSuccess('MinIO успешно подключен', { endpoint: `${config.minio.endpoint}:${config.minio.port}`, bucket: config.minio.bucket, ssl: config.minio.useSSL }); } else { log('warn', 'MinIO недоступен, используется локальное хранилище'); } } catch (err) { logError('MinIO initialization failed', err, { endpoint: `${config.minio.endpoint}:${config.minio.port}` }); log('warn', 'Используется локальное хранилище'); } } else { log('info', 'MinIO отключен, используется локальное хранилище'); } }) .catch(err => { logError('MongoDB connection failed', err, { uri: config.mongoUri.replace(/\/\/.*@/, '//***@'), critical: true }); console.error('\n❌ Не удалось подключиться к MongoDB!'); console.error(` Проверьте MONGODB_URI в .env: ${config.mongoUri.replace(/\/\/.*@/, '//***@')}`); console.error(` Убедитесь что MongoDB запущена и доступна\n`); process.exit(1); }); // 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.use('/api/minio', require('./routes/minio-test')); // Базовый роут app.get('/', (req, res) => { res.json({ message: 'Nakama API работает' }); }); // 404 handler app.use(notFoundHandler); // Error handler (должен быть последним) app.use(errorHandler); // Инициализировать WebSocket initWebSocket(server); // Автообновление аватарок отключено - обновление происходит только при перезаходе // scheduleAvatarUpdates(); startServerMonitorBot(); const { startMainBot } = require('./bots/mainBot'); startMainBot(); // Обработка необработанных ошибок process.on('uncaughtException', (error) => { logError('Uncaught Exception', error, { critical: true }); // Дать время записать логи setTimeout(() => process.exit(1), 1000); }); process.on('unhandledRejection', (reason, promise) => { logError('Unhandled Rejection', new Error(reason), { promise: promise.toString(), critical: true }); }); // Graceful shutdown process.on('SIGTERM', () => { log('warn', 'SIGTERM получен, закрываем сервер...'); server.close(() => { logSuccess('Сервер закрыт'); mongoose.connection.close(false, () => { logSuccess('MongoDB соединение закрыто'); process.exit(0); }); }); }); process.on('SIGINT', () => { log('warn', 'SIGINT получен (Ctrl+C), закрываем сервер...'); server.close(() => { logSuccess('Сервер закрыт'); mongoose.connection.close(false, () => { logSuccess('MongoDB соединение закрыто'); process.exit(0); }); }); }); server.listen(config.port, '0.0.0.0', () => { console.log('\n' + '='.repeat(60)); logSuccess('Сервер успешно запущен', { port: config.port, environment: config.nodeEnv, api: `http://0.0.0.0:${config.port}/api`, frontend: config.frontendUrl, mongodb: config.mongoUri.replace(/\/\/.*@/, '//***@'), // Скрыть пароль minioEnabled: config.minio.enabled, redisEnabled: !!config.redisUrl }); console.log(` 🌐 API: http://0.0.0.0:${config.port}/api`); console.log(` 📦 MongoDB: ${config.mongoUri.includes('localhost') ? 'Local' : 'Remote'}`); console.log(` ⚙️ Environment: ${config.nodeEnv}`); if (config.minio.enabled) { console.log(` 🗄️ MinIO: ${config.minio.endpoint}:${config.minio.port}`); } if (config.redisUrl) { console.log(` 🔴 Redis: Connected`); } if (!config.telegramBotToken) { log('warn', 'TELEGRAM_BOT_TOKEN не установлен!', { path: path.join(__dirname, '.env'), envSet: !!process.env.TELEGRAM_BOT_TOKEN }); } else { logSuccess('Telegram Bot инициализирован', { token: config.telegramBotToken.substring(0, 10) + '...' }); } console.log('='.repeat(60) + '\n'); });