nakama/backend/server.js

246 lines
8.8 KiB
JavaScript
Raw Normal View History

2025-11-03 20:35:01 +00:00
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const dotenv = require('dotenv');
const path = require('path');
const http = require('http');
2025-11-04 21:51:05 +00:00
// Загрузить переменные окружения ДО импорта config
dotenv.config({ path: path.join(__dirname, '.env') });
2025-11-03 20:35:01 +00:00
const { generalLimiter } = require('./middleware/rateLimiter');
const { initRedis } = require('./utils/redis');
const { initWebSocket } = require('./websocket');
2025-11-20 22:07:37 +00:00
const { initMinioClient, checkConnection: checkMinioConnection } = require('./utils/minio');
2025-11-03 20:35:01 +00:00
const config = require('./config');
2025-11-04 21:51:05 +00:00
// 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');
2025-11-10 20:13:22 +00:00
const { scheduleAvatarUpdates } = require('./jobs/avatarUpdater');
const { startServerMonitorBot } = require('./bots/serverMonitor');
const ERROR_SUPPORT_SUFFIX = ' Сообщите об ошибке в https://t.me/NakamaReportbot';
2025-11-03 20:35:01 +00:00
const app = express();
const server = http.createServer(app);
2025-11-04 21:51:05 +00:00
// Trust proxy для правильного IP (для rate limiting за nginx/cloudflare)
if (config.isProduction()) {
app.set('trust proxy', 1);
}
// Security headers (Helmet)
app.use(helmetConfig);
2025-11-03 20:35:01 +00:00
// CORS настройки
const corsOptions = {
origin: config.corsOrigin === '*' ? '*' : config.corsOrigin.split(','),
credentials: true,
2025-11-04 21:51:05 +00:00
optionsSuccessStatus: 200,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-telegram-init-data'],
maxAge: 86400 // 24 часа
2025-11-03 20:35:01 +00:00
};
app.use(cors(corsOptions));
2025-11-04 21:51:05 +00:00
// 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
2025-11-03 20:35:01 +00:00
app.use('/uploads', express.static(path.join(__dirname, config.uploadsDir)));
2025-11-10 20:13:22 +00:00
// Дополнение ошибок сообщением о канале связи
app.use((req, res, next) => {
const originalJson = res.json.bind(res);
res.json = (body) => {
const appendSuffix = (obj) => {
if (!obj || typeof obj !== 'object') {
return;
}
2025-11-11 00:33:22 +00:00
// Список ошибок, к которым НЕ нужно добавлять суффикс
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)) {
2025-11-10 20:13:22 +00:00
obj.error += ERROR_SUPPORT_SUFFIX;
}
2025-11-11 00:33:22 +00:00
if (typeof obj.message === 'string' && res.statusCode >= 400 && !obj.message.includes(ERROR_SUPPORT_SUFFIX) && !shouldSkipSuffix(obj.message)) {
2025-11-10 20:13:22 +00:00
obj.message += ERROR_SUPPORT_SUFFIX;
}
if (Array.isArray(obj.errors)) {
obj.errors = obj.errors.map((item) => {
if (typeof item === 'string') {
2025-11-11 00:33:22 +00:00
if (shouldSkipSuffix(item)) return item;
2025-11-10 20:13:22 +00:00
return item.includes(ERROR_SUPPORT_SUFFIX) ? item : `${item}${ERROR_SUPPORT_SUFFIX}`;
}
2025-11-11 00:33:22 +00:00
if (item && typeof item === 'object' && typeof item.message === 'string' && !item.message.includes(ERROR_SUPPORT_SUFFIX) && !shouldSkipSuffix(item.message)) {
2025-11-10 20:13:22 +00:00
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();
});
2025-11-04 21:51:05 +00:00
// DDoS защита (применяется перед другими rate limiters)
app.use(ddosProtection);
2025-11-03 20:35:01 +00:00
// 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 подключение
2025-11-04 21:51:05 +00:00
mongoose.connect(config.mongoUri)
2025-11-20 22:07:37 +00:00
.then(async () => {
2025-11-03 20:35:01 +00:00
console.log(`✅ MongoDB подключена: ${config.mongoUri.replace(/\/\/.*@/, '//***@')}`);
2025-11-20 22:07:37 +00:00
2025-11-03 20:35:01 +00:00
// Инициализировать Redis (опционально)
if (config.redisUrl) {
initRedis().catch(err => console.log('⚠️ Redis недоступен, работаем без кэша'));
} else {
console.log(' Redis не настроен, кэширование отключено');
}
2025-11-20 22:07:37 +00:00
// Инициализировать MinIO (опционально)
if (config.minio.enabled) {
try {
initMinioClient();
const minioOk = await checkMinioConnection();
if (minioOk) {
console.log(`✅ MinIO подключен: ${config.minio.endpoint}:${config.minio.port}`);
console.log(` Bucket: ${config.minio.bucket}`);
} else {
console.log('⚠️ MinIO недоступен, используется локальное хранилище');
}
} catch (err) {
console.log('⚠️ MinIO ошибка инициализации:', err.message);
console.log(' Используется локальное хранилище');
}
} else {
console.log(' MinIO отключен, используется локальное хранилище');
}
2025-11-03 20:35:01 +00:00
})
.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'));
2025-11-03 22:17:25 +00:00
app.use('/api/bot', require('./routes/bot'));
2025-11-10 20:13:22 +00:00
app.use('/api/mod-app', require('./routes/modApp'));
2025-11-20 22:07:37 +00:00
app.use('/api/minio', require('./routes/minio-test'));
2025-11-03 20:35:01 +00:00
// Базовый роут
app.get('/', (req, res) => {
2025-11-20 21:32:48 +00:00
res.json({ message: 'Nakama API работает' });
2025-11-03 20:35:01 +00:00
});
2025-11-04 21:51:05 +00:00
// 404 handler
app.use(notFoundHandler);
// Error handler (должен быть последним)
app.use(errorHandler);
2025-11-03 20:35:01 +00:00
// Инициализировать WebSocket
initWebSocket(server);
2025-11-10 20:13:22 +00:00
scheduleAvatarUpdates();
startServerMonitorBot();
2025-11-03 20:35:01 +00:00
// 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}`);
}
2025-11-04 21:51:05 +00:00
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)}...`);
}
2025-11-03 20:35:01 +00:00
});