const User = require('../models/User'); const { validateTelegramId } = require('./validator'); const { logSecurityEvent } = require('./logger'); const { validateAndParseInitData } = require('../utils/telegram'); const { fetchLatestAvatar } = require('../jobs/avatarUpdater'); const OFFICIAL_CLIENT_MESSAGE = 'Используйте официальный клиент. Сообщите об ошибке в https://t.me/NakamaReportbot'; const ALLOWED_SEARCH_PREFERENCES = ['furry', 'anime']; 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, noHomo: true }; updated = true; } else { if (user.settings.whitelist.noNSFW === undefined) { user.settings.whitelist.noNSFW = true; updated = true; } if (user.settings.whitelist.noHomo === undefined) { user.settings.whitelist.noHomo = true; updated = true; } } if (updated) { user.markModified('settings'); await user.save(); } }; // Нормализовать данные пользователя из Telegram (поддержка camelCase и snake_case) const normalizeTelegramUser = (telegramUser) => { return { id: telegramUser.id, username: telegramUser.username || telegramUser.userName, firstName: telegramUser.firstName || telegramUser.first_name || '', lastName: telegramUser.lastName || telegramUser.last_name || '', photoUrl: telegramUser.photoUrl || telegramUser.photo_url || null }; }; // Подтянуть отсутствующие данные пользователя из Telegram const ensureUserData = async (user, telegramUser) => { if (!user || !telegramUser) return; // Нормализовать данные (поддержка camelCase и snake_case) const normalized = normalizeTelegramUser(telegramUser); let updated = false; // Обновить username, если отсутствует или пустой if (!user.username || user.username.trim() === '') { if (normalized.username) { user.username = normalized.username; updated = true; } else if (normalized.firstName) { user.username = normalized.firstName; updated = true; } } // Обновить firstName, если отсутствует if (!user.firstName && normalized.firstName) { user.firstName = normalized.firstName; updated = true; } // Обновить lastName, если отсутствует if (user.lastName === undefined || user.lastName === null) { user.lastName = normalized.lastName; updated = true; } // Обновить аватарку, если отсутствует if (!user.photoUrl) { // Сначала проверить photoUrl из initData if (normalized.photoUrl) { user.photoUrl = normalized.photoUrl; updated = true; } else { // Если нет в initData, попробовать получить через Bot API try { const avatarUrl = await fetchLatestAvatar(user.telegramId); if (avatarUrl) { user.photoUrl = avatarUrl; updated = true; } } catch (error) { // Игнорируем ошибки получения аватарки console.log('Не удалось получить аватарку через Bot API:', error.message); } } } if (updated) { await user.save(); } }; const authenticate = async (req, res, next) => { try { const authHeader = req.headers.authorization || ''; let initDataRaw = null; if (authHeader.startsWith('tma ')) { initDataRaw = authHeader.slice(4).trim(); } if (!initDataRaw) { const headerInitData = req.headers['x-telegram-init-data']; if (headerInitData && typeof headerInitData === 'string') { initDataRaw = headerInitData.trim(); } } if (!initDataRaw) { logSecurityEvent('AUTH_TOKEN_MISSING', req); return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE }); } if (!initDataRaw) { logSecurityEvent('EMPTY_INITDATA', req); return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE }); } let payload; try { payload = validateAndParseInitData(initDataRaw); } catch (error) { logSecurityEvent('INVALID_INITDATA', req, { reason: error.message }); return res.status(401).json({ error: `${error.message}. ${OFFICIAL_CLIENT_MESSAGE}` }); } const telegramUser = payload.user; const startParam = payload.start_param || payload.startParam; if (!validateTelegramId(telegramUser.id)) { logSecurityEvent('INVALID_TELEGRAM_ID', req, { telegramId: telegramUser.id }); return res.status(401).json({ error: 'Неверный ID пользователя' }); } // Нормализовать данные пользователя (библиотека возвращает camelCase, но может быть и snake_case) const normalizedUser = normalizeTelegramUser(telegramUser); let user = await User.findOne({ telegramId: normalizedUser.id.toString() }); if (!user) { // Обработка реферального кода из start_param let referredBy = null; if (startParam) { const trimmedParam = startParam.trim(); // Проверяем регистронезависимо (может быть ref_ или REF_) const normalizedStartParam = trimmedParam.toLowerCase(); if (normalizedStartParam.startsWith('ref_')) { // Убираем все префиксы ref_/REF_ (может быть двойной префикс ref_REF_) let codeToSearch = trimmedParam; while (codeToSearch.toLowerCase().startsWith('ref_')) { codeToSearch = codeToSearch.substring(4); // Убираем "ref_" или "REF_" } // referralCode в базе хранится с префиксом "REF_" (в верхнем регистре) // Ищем по коду с префиксом REF_ const escapedCode = codeToSearch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const referrer = await User.findOne({ referralCode: { $regex: new RegExp(`^REF_${escapedCode}$`, 'i') } }); if (referrer) { referredBy = referrer._id; console.log(`🔗 Реферальная ссылка: пользователь ${normalizedUser.username || normalizedUser.id} зарегистрирован по ссылке от ${referrer.username} (${referrer._id}), код: ${trimmedParam} -> REF_${codeToSearch}`); } else { // Дополнительная проверка: посмотрим все referralCode в базе для отладки const allCodes = await User.find({ referralCode: { $exists: true } }, { referralCode: 1, username: 1 }).limit(5); console.warn(`⚠️ Реферальный код не найден: ${trimmedParam} (искали: REF_${codeToSearch})`); console.warn(` Примеры кодов в базе: ${allCodes.map(u => u.referralCode).join(', ')}`); } } else { console.log(`ℹ️ startParam не содержит ref_: ${trimmedParam}`); } } user = new User({ telegramId: normalizedUser.id.toString(), username: normalizedUser.username || normalizedUser.firstName || 'user', firstName: normalizedUser.firstName, lastName: normalizedUser.lastName, photoUrl: normalizedUser.photoUrl, referredBy: referredBy }); await user.save(); if (referredBy) { console.log(`✅ Создан новый пользователь ${user.username} (${user._id}) с referredBy: ${referredBy}`); } else { console.log(`✅ Создан новый пользователь ${user.username} (${user._id}) без реферала`); } // Счетчик рефералов увеличивается только когда пользователь создаст первый пост // (см. routes/posts.js) } else { // Для существующих пользователей тоже можно установить referredBy, // если они еще не были засчитаны как реферал и пришли по реферальной ссылке if (startParam && !user.referredBy && !user.referralCounted) { const trimmedParam = startParam.trim(); const normalizedStartParam = trimmedParam.toLowerCase(); if (normalizedStartParam.startsWith('ref_')) { // Ищем по полному коду (с префиксом ref_) const referrer = await User.findOne({ referralCode: { $regex: new RegExp(`^${trimmedParam.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i') } }); if (referrer) { user.referredBy = referrer._id; await user.save(); console.log(`🔗 Установлен referredBy для существующего пользователя ${user.username} от ${referrer.username} (код: ${trimmedParam})`); } else { // Попробуем альтернативный поиск const codeWithoutPrefix = trimmedParam.substring(4); const alternativeSearch = await User.findOne({ $or: [ { referralCode: { $regex: new RegExp(`^ref_${codeWithoutPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i') } }, { referralCode: { $regex: new RegExp(`^REF_${codeWithoutPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i') } } ] }); if (alternativeSearch) { user.referredBy = alternativeSearch._id; await user.save(); console.log(`🔗 Установлен referredBy (альтернативный поиск) для существующего пользователя ${user.username} от ${alternativeSearch.username}`); } else { console.warn(`⚠️ Реферальный код не найден для существующего пользователя ${user.username}: ${trimmedParam}`); } } } } // Обновлять только если есть новые данные, не перезаписывать существующие пустыми значениями if (normalizedUser.username) { user.username = normalizedUser.username; } else if (!user.username && normalizedUser.firstName) { // Если username пустой, использовать firstName как fallback user.username = normalizedUser.firstName; } if (normalizedUser.firstName) { user.firstName = normalizedUser.firstName; } if (normalizedUser.lastName !== undefined) { user.lastName = normalizedUser.lastName; } // Обновлять аватарку только если есть новая if (normalizedUser.photoUrl) { user.photoUrl = normalizedUser.photoUrl; } await user.save(); } if (user.banned) { return res.status(403).json({ error: 'Пользователь заблокирован' }); } // Подтянуть отсутствующие данные из Telegram (используем нормализованные данные) await ensureUserData(user, normalizedUser); await ensureUserSettings(user); await touchUserActivity(user); // Реферальная система: отслеживание входов в разные дни // Останавливаем отслеживание после засчета реферала, чтобы не заполнять БД if (user.referredBy && !user.referralCounted) { // Инициализировать loginDates если его нет if (!user.loginDates) { user.loginDates = []; } const { getMoscowStartOfDay, getMoscowDate } = require('../utils/moscowTime'); const today = getMoscowDate(); const todayNormalized = getMoscowStartOfDay(today); const todayTime = todayNormalized.getTime(); // Получить уникальные даты из существующего массива (только даты, без времени по московскому времени) const uniqueDates = new Set(); if (user.loginDates && Array.isArray(user.loginDates)) { user.loginDates.forEach(date => { if (!date) return; try { // Нормализуем дату: получаем начало дня по московскому времени const dateObj = new Date(date); const normalizedDate = getMoscowStartOfDay(dateObj); const normalizedTime = normalizedDate.getTime(); uniqueDates.add(normalizedTime); } catch (error) { console.error(`Ошибка обработки даты ${date}:`, error); } }); } // Добавить сегодняшнюю дату, если её еще нет const todayExists = uniqueDates.has(todayTime); if (!todayExists) { if (!user.loginDates) { user.loginDates = []; } user.loginDates.push(today); uniqueDates.add(todayTime); console.log(`📅 Реферал ${user.username} (${user._id}): добавлена дата входа. Уникальных дат: ${uniqueDates.size}/2`); } // Если уже есть 2 или более уникальные даты, засчитать реферала if (uniqueDates.size >= 2) { const User = require('../models/User'); const referrer = await User.findById(user.referredBy); if (!referrer) { console.error(`❌ Реферер не найден для пользователя ${user.username} (${user._id}), referredBy: ${user.referredBy}`); } else { // Увеличить счетчик рефералов const oldCount = referrer.referralsCount || 0; referrer.referralsCount = oldCount + 1; await referrer.save(); console.log(`✅ Реферал засчитан: пользователь ${user.username} (${user._id}) засчитан для ${referrer.username} (${referrer._id}). Счетчик: ${oldCount} -> ${referrer.referralsCount}`); // Начислить баллы за реферала try { const { awardReferral } = require('../utils/tickets'); await awardReferral(user.referredBy); console.log(` ✅ Баллы начислены рефереру ${referrer.username}`); } catch (error) { console.error(` ⚠️ Ошибка начисления баллов:`, error.message); } } user.referralCounted = true; // Очистить loginDates после засчета, чтобы не хранить лишние данные user.loginDates = []; await user.save(); } else { // Если еще нет 2 уникальных дат, сохранить обновленный массив loginDates await user.save(); } } req.user = user; req.telegramUser = normalizedUser; next(); } catch (error) { console.error('❌ Ошибка авторизации:', error); res.status(401).json({ error: `Ошибка авторизации. ${OFFICIAL_CLIENT_MESSAGE}` }); } }; // 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(); }; // Middleware для модерации (использует MODERATION_BOT_TOKEN) const authenticateModeration = async (req, res, next) => { const config = require('../config'); try { const authHeader = req.headers.authorization || ''; let initDataRaw = null; if (authHeader.startsWith('tma ')) { initDataRaw = authHeader.slice(4).trim(); } if (!initDataRaw) { const headerInitData = req.headers['x-telegram-init-data']; if (headerInitData && typeof headerInitData === 'string') { initDataRaw = headerInitData.trim(); } } if (!initDataRaw) { logSecurityEvent('AUTH_TOKEN_MISSING', req); return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE }); } let payload; try { // Use MODERATION_BOT_TOKEN for validation payload = validateAndParseInitData(initDataRaw, config.moderationBotToken); } catch (error) { logSecurityEvent('INVALID_INITDATA', req, { reason: error.message }); return res.status(401).json({ error: `${error.message}. ${OFFICIAL_CLIENT_MESSAGE}` }); } const telegramUser = payload.user; if (!validateTelegramId(telegramUser.id)) { logSecurityEvent('INVALID_TELEGRAM_ID', req, { telegramId: telegramUser.id }); return res.status(401).json({ error: 'Неверный ID пользователя' }); } // Нормализовать данные пользователя (библиотека возвращает camelCase, но может быть и snake_case) const normalizedUser = normalizeTelegramUser(telegramUser); let user = await User.findOne({ telegramId: normalizedUser.id.toString() }); if (!user) { user = new User({ telegramId: normalizedUser.id.toString(), username: normalizedUser.username || normalizedUser.firstName || 'user', firstName: normalizedUser.firstName, lastName: normalizedUser.lastName, photoUrl: normalizedUser.photoUrl }); await user.save(); } else { // Обновлять только если есть новые данные, не перезаписывать существующие пустыми значениями if (normalizedUser.username) { user.username = normalizedUser.username; } else if (!user.username && normalizedUser.firstName) { // Если username пустой, использовать firstName как fallback user.username = normalizedUser.firstName; } if (normalizedUser.firstName) { user.firstName = normalizedUser.firstName; } if (normalizedUser.lastName !== undefined) { user.lastName = normalizedUser.lastName; } // Обновлять аватарку только если есть новая if (normalizedUser.photoUrl) { user.photoUrl = normalizedUser.photoUrl; } await user.save(); } if (user.banned) { return res.status(403).json({ error: 'Пользователь заблокирован' }); } // Подтянуть отсутствующие данные из Telegram (используем нормализованные данные) await ensureUserData(user, normalizedUser); await ensureUserSettings(user); await touchUserActivity(user); req.user = user; req.telegramUser = normalizedUser; next(); } catch (error) { console.error('❌ Ошибка авторизации модерации:', error); res.status(401).json({ error: `Ошибка авторизации. ${OFFICIAL_CLIENT_MESSAGE}` }); } }; // Middleware для проверки JWT токена (для модерации через логин/пароль) const authenticateJWT = async (req, res, next) => { try { // Получить токен из заголовка или cookie let token = null; const authHeader = req.headers.authorization; if (authHeader && authHeader.startsWith('Bearer ')) { token = authHeader.slice(7); } else if (req.cookies) { const { verifyAccessToken, ACCESS_COOKIE } = require('../utils/tokens'); token = req.cookies[ACCESS_COOKIE]; } if (!token) { return res.status(401).json({ error: 'Требуется авторизация' }); } // Проверить токен const { verifyAccessToken } = require('../utils/tokens'); let payload; try { payload = verifyAccessToken(token); } catch (error) { logSecurityEvent('INVALID_JWT_TOKEN', req); return res.status(401).json({ error: 'Неверный токен' }); } // Найти пользователя const user = await User.findById(payload.userId); if (!user) { return res.status(401).json({ error: 'Пользователь не найден' }); } if (user.banned) { return res.status(403).json({ error: 'Аккаунт заблокирован' }); } req.user = user; next(); } catch (error) { console.error('Ошибка JWT авторизации:', error); res.status(401).json({ error: 'Ошибка авторизации' }); } }; // Комбинированный middleware: Telegram или JWT const authenticateModerationFlexible = async (req, res, next) => { // Попробовать Telegram авторизацию const authHeader = req.headers.authorization || ''; const hasTelegramAuth = authHeader.startsWith('tma ') || req.headers['x-telegram-init-data']; if (hasTelegramAuth) { return authenticateModeration(req, res, next); } else { return authenticateJWT(req, res, next); } }; module.exports = { authenticate, authenticateModeration, authenticateJWT, authenticateModerationFlexible, requireModerator, requireAdmin, touchUserActivity, ensureUserSettings, ensureUserData };