nakama/backend/server.js

224 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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: 'NakamaHost 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)}...`);
}
});