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-21 01:14:56 +00:00
|
|
|
|
const { printMinioConfig } = require('./utils/minioDebug');
|
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');
|
2025-11-21 01:14:56 +00:00
|
|
|
|
const { requestLogger, logSecurityEvent, log, logSuccess, logError } = require('./middleware/logger');
|
2025-11-04 21:51:05 +00:00
|
|
|
|
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-21 01:14:56 +00:00
|
|
|
|
log('info', 'Подключение к MongoDB...', { uri: config.mongoUri.replace(/\/\/.*@/, '//***@') });
|
|
|
|
|
|
|
2025-11-04 21:51:05 +00:00
|
|
|
|
mongoose.connect(config.mongoUri)
|
2025-11-20 22:07:37 +00:00
|
|
|
|
.then(async () => {
|
2025-11-21 01:14:56 +00:00
|
|
|
|
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 переподключена');
|
|
|
|
|
|
});
|
2025-11-20 22:07:37 +00:00
|
|
|
|
|
2025-11-03 20:35:01 +00:00
|
|
|
|
// Инициализировать Redis (опционально)
|
|
|
|
|
|
if (config.redisUrl) {
|
2025-11-21 01:14:56 +00:00
|
|
|
|
try {
|
|
|
|
|
|
log('info', 'Подключение к Redis...');
|
|
|
|
|
|
await initRedis();
|
|
|
|
|
|
logSuccess('Redis подключен', { url: config.redisUrl.replace(/\/\/.*@/, '//***@') });
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
log('warn', 'Redis недоступен, работаем без кэша', {
|
|
|
|
|
|
error: err.message
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-11-03 20:35:01 +00:00
|
|
|
|
} else {
|
2025-11-21 01:14:56 +00:00
|
|
|
|
log('info', 'Redis не настроен, кэширование отключено');
|
2025-11-03 20:35:01 +00:00
|
|
|
|
}
|
2025-11-20 22:07:37 +00:00
|
|
|
|
|
|
|
|
|
|
// Инициализировать MinIO (опционально)
|
|
|
|
|
|
if (config.minio.enabled) {
|
|
|
|
|
|
try {
|
2025-11-21 01:14:56 +00:00
|
|
|
|
log('info', 'Инициализация MinIO...');
|
|
|
|
|
|
|
|
|
|
|
|
// Вывести конфигурацию и проверки
|
|
|
|
|
|
printMinioConfig();
|
|
|
|
|
|
|
2025-11-20 22:07:37 +00:00
|
|
|
|
initMinioClient();
|
|
|
|
|
|
const minioOk = await checkMinioConnection();
|
|
|
|
|
|
if (minioOk) {
|
2025-11-21 01:14:56 +00:00
|
|
|
|
logSuccess('MinIO успешно подключен', {
|
|
|
|
|
|
endpoint: `${config.minio.endpoint}:${config.minio.port}`,
|
|
|
|
|
|
bucket: config.minio.bucket,
|
|
|
|
|
|
ssl: config.minio.useSSL
|
|
|
|
|
|
});
|
2025-11-20 22:07:37 +00:00
|
|
|
|
} else {
|
2025-11-21 01:14:56 +00:00
|
|
|
|
log('warn', 'MinIO недоступен, используется локальное хранилище');
|
2025-11-20 22:07:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
2025-11-21 01:14:56 +00:00
|
|
|
|
logError('MinIO initialization failed', err, {
|
|
|
|
|
|
endpoint: `${config.minio.endpoint}:${config.minio.port}`
|
|
|
|
|
|
});
|
|
|
|
|
|
log('warn', 'Используется локальное хранилище');
|
2025-11-20 22:07:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2025-11-21 01:14:56 +00:00
|
|
|
|
log('info', 'MinIO отключен, используется локальное хранилище');
|
2025-11-20 22:07:37 +00:00
|
|
|
|
}
|
2025-11-03 20:35:01 +00:00
|
|
|
|
})
|
2025-11-21 01:14:56 +00:00
|
|
|
|
.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);
|
|
|
|
|
|
});
|
2025-11-03 20:35:01 +00:00
|
|
|
|
|
|
|
|
|
|
// 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-12-01 00:51:23 +00:00
|
|
|
|
// Автообновление аватарок отключено - обновление происходит только при перезаходе
|
|
|
|
|
|
// scheduleAvatarUpdates();
|
2025-11-10 20:13:22 +00:00
|
|
|
|
startServerMonitorBot();
|
2025-12-04 17:44:05 +00:00
|
|
|
|
const { startMainBot } = require('./bots/mainBot');
|
|
|
|
|
|
startMainBot();
|
2025-11-03 20:35:01 +00:00
|
|
|
|
|
2025-11-21 01:14:56 +00:00
|
|
|
|
// Обработка необработанных ошибок
|
|
|
|
|
|
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
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-03 20:35:01 +00:00
|
|
|
|
// Graceful shutdown
|
|
|
|
|
|
process.on('SIGTERM', () => {
|
2025-11-21 01:14:56 +00:00
|
|
|
|
log('warn', 'SIGTERM получен, закрываем сервер...');
|
|
|
|
|
|
server.close(() => {
|
|
|
|
|
|
logSuccess('Сервер закрыт');
|
|
|
|
|
|
mongoose.connection.close(false, () => {
|
|
|
|
|
|
logSuccess('MongoDB соединение закрыто');
|
|
|
|
|
|
process.exit(0);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
process.on('SIGINT', () => {
|
|
|
|
|
|
log('warn', 'SIGINT получен (Ctrl+C), закрываем сервер...');
|
2025-11-03 20:35:01 +00:00
|
|
|
|
server.close(() => {
|
2025-11-21 01:14:56 +00:00
|
|
|
|
logSuccess('Сервер закрыт');
|
2025-11-03 20:35:01 +00:00
|
|
|
|
mongoose.connection.close(false, () => {
|
2025-11-21 01:14:56 +00:00
|
|
|
|
logSuccess('MongoDB соединение закрыто');
|
2025-11-03 20:35:01 +00:00
|
|
|
|
process.exit(0);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
server.listen(config.port, '0.0.0.0', () => {
|
2025-11-21 01:14:56 +00:00
|
|
|
|
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`);
|
2025-11-03 20:35:01 +00:00
|
|
|
|
}
|
2025-11-21 01:14:56 +00:00
|
|
|
|
|
2025-11-04 21:51:05 +00:00
|
|
|
|
if (!config.telegramBotToken) {
|
2025-11-21 01:14:56 +00:00
|
|
|
|
log('warn', 'TELEGRAM_BOT_TOKEN не установлен!', {
|
|
|
|
|
|
path: path.join(__dirname, '.env'),
|
|
|
|
|
|
envSet: !!process.env.TELEGRAM_BOT_TOKEN
|
|
|
|
|
|
});
|
2025-11-04 21:51:05 +00:00
|
|
|
|
} else {
|
2025-11-21 01:14:56 +00:00
|
|
|
|
logSuccess('Telegram Bot инициализирован', {
|
|
|
|
|
|
token: config.telegramBotToken.substring(0, 10) + '...'
|
|
|
|
|
|
});
|
2025-11-04 21:51:05 +00:00
|
|
|
|
}
|
2025-11-21 01:14:56 +00:00
|
|
|
|
|
|
|
|
|
|
console.log('='.repeat(60) + '\n');
|
2025-11-03 20:35:01 +00:00
|
|
|
|
});
|
|
|
|
|
|
|