2025-11-03 20:35:01 +00:00
|
|
|
|
const crypto = require('crypto');
|
|
|
|
|
|
const User = require('../models/User');
|
2025-11-04 21:51:05 +00:00
|
|
|
|
const { validateTelegramId } = require('./validator');
|
|
|
|
|
|
const { logSecurityEvent } = require('./logger');
|
|
|
|
|
|
const config = require('../config');
|
2025-11-03 20:35:01 +00:00
|
|
|
|
|
2025-11-10 20:13:22 +00:00
|
|
|
|
const OFFICIAL_CLIENT_MESSAGE = 'Используйте официальный клиент. Сообщите об ошибке в https://t.me/NakamaReportbot';
|
|
|
|
|
|
const ALLOWED_SEARCH_PREFERENCES = ['furry', 'anime'];
|
2025-11-10 21:22:58 +00:00
|
|
|
|
const MAX_AUTH_AGE_SECONDS = 5 * 60; // 5 минут
|
2025-11-10 20:13:22 +00:00
|
|
|
|
|
|
|
|
|
|
const touchUserActivity = async (user) => {
|
|
|
|
|
|
if (!user) return;
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
const shouldUpdate =
|
|
|
|
|
|
!user.lastActiveAt ||
|
|
|
|
|
|
Math.abs(now - new Date(user.lastActiveAt).getTime()) > 5 * 60 * 1000;
|
|
|
|
|
|
|
|
|
|
|
|
if (shouldUpdate) {
|
|
|
|
|
|
user.lastActiveAt = new Date(now);
|
|
|
|
|
|
await user.save();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const ensureUserSettings = async (user) => {
|
|
|
|
|
|
if (!user) return;
|
|
|
|
|
|
let updated = false;
|
|
|
|
|
|
|
|
|
|
|
|
if (!user.settings) {
|
|
|
|
|
|
user.settings = {};
|
|
|
|
|
|
updated = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!ALLOWED_SEARCH_PREFERENCES.includes(user.settings.searchPreference)) {
|
|
|
|
|
|
user.settings.searchPreference = 'furry';
|
|
|
|
|
|
updated = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!user.settings.whitelist) {
|
|
|
|
|
|
user.settings.whitelist = { noNSFW: true };
|
|
|
|
|
|
updated = true;
|
|
|
|
|
|
} else if (user.settings.whitelist.noNSFW === undefined) {
|
|
|
|
|
|
user.settings.whitelist.noNSFW = true;
|
|
|
|
|
|
updated = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (updated) {
|
|
|
|
|
|
user.markModified('settings');
|
|
|
|
|
|
await user.save();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-03 20:35:01 +00:00
|
|
|
|
function validateTelegramWebAppData(initData, botToken) {
|
2025-11-10 21:22:58 +00:00
|
|
|
|
if (!botToken) {
|
|
|
|
|
|
throw new Error('TELEGRAM_BOT_TOKEN не настроен');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const params = new URLSearchParams(initData);
|
|
|
|
|
|
const hash = params.get('hash');
|
|
|
|
|
|
const authDate = Number(params.get('auth_date'));
|
|
|
|
|
|
|
|
|
|
|
|
if (!hash) {
|
|
|
|
|
|
throw new Error('Отсутствует hash в initData');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!authDate) {
|
|
|
|
|
|
throw new Error('Отсутствует auth_date в initData');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const dataCheck = [];
|
|
|
|
|
|
for (const [key, value] of params.entries()) {
|
|
|
|
|
|
if (key === 'hash') continue;
|
|
|
|
|
|
dataCheck.push(`${key}=${value}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
dataCheck.sort((a, b) => a.localeCompare(b));
|
|
|
|
|
|
const dataCheckString = dataCheck.join('\n');
|
|
|
|
|
|
|
|
|
|
|
|
const secretKey = crypto.createHmac('sha256', 'WebAppData').update(botToken).digest();
|
|
|
|
|
|
const calculatedHash = crypto.createHmac('sha256', secretKey).update(dataCheckString).digest('hex');
|
|
|
|
|
|
|
|
|
|
|
|
if (calculatedHash !== hash) {
|
|
|
|
|
|
throw new Error('Неверная подпись initData');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
|
|
|
|
|
if (Math.abs(now - authDate) > MAX_AUTH_AGE_SECONDS) {
|
|
|
|
|
|
throw new Error('Данные авторизации устарели');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return params;
|
2025-11-03 20:35:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Middleware для проверки авторизации
|
|
|
|
|
|
const authenticate = async (req, res, next) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const initData = req.headers['x-telegram-init-data'];
|
2025-11-10 21:22:58 +00:00
|
|
|
|
|
2025-11-03 20:35:01 +00:00
|
|
|
|
if (!initData) {
|
2025-11-10 21:22:58 +00:00
|
|
|
|
logSecurityEvent('MISSING_INITDATA', req);
|
2025-11-10 20:13:22 +00:00
|
|
|
|
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
|
2025-11-03 20:35:01 +00:00
|
|
|
|
}
|
2025-11-10 21:22:58 +00:00
|
|
|
|
|
|
|
|
|
|
if (!config.telegramBotToken) {
|
|
|
|
|
|
logSecurityEvent('MISSING_BOT_TOKEN', req);
|
|
|
|
|
|
if (config.isProduction()) {
|
|
|
|
|
|
return res.status(500).json({ error: 'Сервер некорректно настроен для авторизации через Telegram' });
|
|
|
|
|
|
}
|
|
|
|
|
|
console.warn('⚠️ TELEGRAM_BOT_TOKEN не установлен, пропускаем проверку подписи (dev режим)');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let params;
|
2025-11-03 22:41:34 +00:00
|
|
|
|
try {
|
2025-11-10 21:22:58 +00:00
|
|
|
|
if (config.telegramBotToken) {
|
|
|
|
|
|
params = validateTelegramWebAppData(initData, config.telegramBotToken);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
params = new URLSearchParams(initData);
|
2025-11-03 22:41:34 +00:00
|
|
|
|
}
|
2025-11-10 21:22:58 +00:00
|
|
|
|
} catch (validationError) {
|
|
|
|
|
|
logSecurityEvent('INVALID_INITDATA_SIGNATURE', req, { reason: validationError.message });
|
|
|
|
|
|
return res.status(401).json({ error: `${validationError.message}. ${OFFICIAL_CLIENT_MESSAGE}` });
|
2025-11-03 22:41:34 +00:00
|
|
|
|
}
|
2025-11-10 21:22:58 +00:00
|
|
|
|
|
|
|
|
|
|
const userParam = params.get('user');
|
|
|
|
|
|
|
2025-11-03 21:29:00 +00:00
|
|
|
|
if (!userParam) {
|
2025-11-10 21:22:58 +00:00
|
|
|
|
logSecurityEvent('MISSING_INITDATA_USER', req);
|
2025-11-10 20:13:22 +00:00
|
|
|
|
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
|
2025-11-03 20:35:01 +00:00
|
|
|
|
}
|
2025-11-10 21:22:58 +00:00
|
|
|
|
|
2025-11-03 22:41:34 +00:00
|
|
|
|
let telegramUser;
|
|
|
|
|
|
try {
|
|
|
|
|
|
telegramUser = JSON.parse(userParam);
|
2025-11-10 21:22:58 +00:00
|
|
|
|
} catch (parseError) {
|
|
|
|
|
|
logSecurityEvent('INVALID_INITDATA_USER_JSON', req, { error: parseError.message });
|
2025-11-10 20:13:22 +00:00
|
|
|
|
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
|
2025-11-03 22:41:34 +00:00
|
|
|
|
}
|
2025-11-10 21:22:58 +00:00
|
|
|
|
|
2025-11-03 20:35:01 +00:00
|
|
|
|
req.telegramUser = telegramUser;
|
2025-11-10 21:22:58 +00:00
|
|
|
|
|
2025-11-04 21:51:05 +00:00
|
|
|
|
if (!validateTelegramId(telegramUser.id)) {
|
|
|
|
|
|
logSecurityEvent('INVALID_TELEGRAM_ID', req, { telegramId: telegramUser.id });
|
|
|
|
|
|
return res.status(401).json({ error: 'Неверный ID пользователя' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-03 20:35:01 +00:00
|
|
|
|
// Найти или создать пользователя
|
|
|
|
|
|
let user = await User.findOne({ telegramId: telegramUser.id.toString() });
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
|
user = new User({
|
|
|
|
|
|
telegramId: telegramUser.id.toString(),
|
|
|
|
|
|
username: telegramUser.username || telegramUser.first_name,
|
|
|
|
|
|
firstName: telegramUser.first_name,
|
|
|
|
|
|
lastName: telegramUser.last_name,
|
|
|
|
|
|
photoUrl: telegramUser.photo_url
|
|
|
|
|
|
});
|
|
|
|
|
|
await user.save();
|
2025-11-03 21:29:00 +00:00
|
|
|
|
console.log(`✅ Создан новый пользователь: ${user.username}`);
|
2025-11-10 20:13:22 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
user.username = telegramUser.username || telegramUser.first_name;
|
|
|
|
|
|
user.firstName = telegramUser.first_name;
|
|
|
|
|
|
user.lastName = telegramUser.last_name;
|
|
|
|
|
|
if (telegramUser.photo_url) {
|
|
|
|
|
|
user.photoUrl = telegramUser.photo_url;
|
|
|
|
|
|
}
|
|
|
|
|
|
await user.save();
|
2025-11-03 20:35:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-10 20:13:22 +00:00
|
|
|
|
await ensureUserSettings(user);
|
|
|
|
|
|
await touchUserActivity(user);
|
2025-11-03 20:35:01 +00:00
|
|
|
|
req.user = user;
|
|
|
|
|
|
next();
|
|
|
|
|
|
} catch (error) {
|
2025-11-03 21:29:00 +00:00
|
|
|
|
console.error('❌ Ошибка авторизации:', error);
|
2025-11-10 21:22:58 +00:00
|
|
|
|
res.status(401).json({ error: `Ошибка авторизации. ${OFFICIAL_CLIENT_MESSAGE}` });
|
2025-11-03 20:35:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Middleware для проверки роли модератора
|
|
|
|
|
|
const requireModerator = (req, res, next) => {
|
|
|
|
|
|
if (req.user.role !== 'moderator' && req.user.role !== 'admin') {
|
|
|
|
|
|
return res.status(403).json({ error: 'Требуются права модератора' });
|
|
|
|
|
|
}
|
|
|
|
|
|
next();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Middleware для проверки роли админа
|
|
|
|
|
|
const requireAdmin = (req, res, next) => {
|
|
|
|
|
|
if (req.user.role !== 'admin') {
|
|
|
|
|
|
return res.status(403).json({ error: 'Требуются права администратора' });
|
|
|
|
|
|
}
|
|
|
|
|
|
next();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
|
|
authenticate,
|
|
|
|
|
|
requireModerator,
|
|
|
|
|
|
requireAdmin
|
|
|
|
|
|
};
|