const express = require('express'); const router = express.Router(); const crypto = require('crypto'); const User = require('../models/User'); const config = require('../config'); const { validateTelegramId } = require('../middleware/validator'); const { logSecurityEvent } = require('../middleware/logger'); const { strictAuthLimiter } = require('../middleware/security'); const { authenticate, ensureUserSettings, touchUserActivity, ensureUserData } = require('../middleware/auth'); const { fetchLatestAvatar } = require('../jobs/avatarUpdater'); const ALLOWED_SEARCH_PREFERENCES = ['furry', 'anime']; const normalizeUserSettings = (settings = {}) => { const plainSettings = typeof settings.toObject === 'function' ? settings.toObject() : { ...settings }; const whitelistSource = plainSettings.whitelist; const whitelist = whitelistSource && typeof whitelistSource.toObject === 'function' ? whitelistSource.toObject() : { ...(whitelistSource || {}) }; return { ...plainSettings, whitelist: { noNSFW: whitelist?.noNSFW ?? true, noHomo: whitelist?.noHomo ?? true, ...whitelist }, searchPreference: ALLOWED_SEARCH_PREFERENCES.includes(plainSettings.searchPreference) ? plainSettings.searchPreference : 'furry' }; }; const OFFICIAL_CLIENT_MESSAGE = 'Используйте официальный клиент. Сообщите об ошибке в https://t.me/NakamaReportbot'; const respondWithUser = async (user, res) => { const populatedUser = await user.populate([ { path: 'followers', select: 'username firstName lastName photoUrl' }, { path: 'following', select: 'username firstName lastName photoUrl' } ]); const settings = normalizeUserSettings(populatedUser.settings); return res.json({ success: true, user: { id: populatedUser._id, telegramId: populatedUser.telegramId, username: populatedUser.username, firstName: populatedUser.firstName, lastName: populatedUser.lastName, photoUrl: populatedUser.photoUrl, bio: populatedUser.bio, role: populatedUser.role, followersCount: populatedUser.followers.length, followingCount: populatedUser.following.length, settings, banned: populatedUser.banned } }); }; router.post('/signin', strictAuthLimiter, authenticate, async (req, res) => { try { await ensureUserSettings(req.user); await touchUserActivity(req.user); return respondWithUser(req.user, res); } catch (error) { console.error('Ошибка signin:', error); res.status(500).json({ error: 'Ошибка авторизации' }); } }); router.post('/logout', (_req, res) => { res.json({ success: true }); }); // Проверка подписи Telegram OAuth (Login Widget) function validateTelegramOAuth(authData, botToken) { if (!authData || !authData.hash) { return false; } const { hash, ...data } = authData; // Удалить поля с undefined/null значениями (они не должны быть в dataCheckString) const cleanData = {}; for (const key in data) { if (data[key] !== undefined && data[key] !== null && data[key] !== '') { cleanData[key] = data[key]; } } // Формировать dataCheckString из очищенных данных const dataCheckString = Object.keys(cleanData) .sort() .map(key => `${key}=${cleanData[key]}`) .join('\n'); const secretKey = crypto .createHmac('sha256', 'WebAppData') .update(botToken) .digest(); const calculatedHash = crypto .createHmac('sha256', secretKey) .update(dataCheckString) .digest('hex'); return calculatedHash === hash; } // Авторизация через Telegram OAuth (Login Widget) router.post('/oauth', strictAuthLimiter, async (req, res) => { try { const { user: telegramUser, auth_date, hash } = req.body; if (!telegramUser || !auth_date || !hash) { logSecurityEvent('INVALID_OAUTH_DATA', req); return res.status(400).json({ error: 'Неверные данные авторизации' }); } // Валидация Telegram ID if (!validateTelegramId(telegramUser.id)) { logSecurityEvent('INVALID_TELEGRAM_ID', req, { telegramId: telegramUser.id }); return res.status(400).json({ error: 'Неверный ID пользователя' }); } // Проверка подписи Telegram (строгая проверка в production) if (config.telegramBotToken) { // Формировать authData только с присутствующими полями const authData = { id: telegramUser.id, first_name: telegramUser.first_name || '', auth_date: auth_date.toString(), hash: hash }; // Добавить опциональные поля только если они присутствуют if (telegramUser.last_name) { authData.last_name = telegramUser.last_name; } if (telegramUser.username) { authData.username = telegramUser.username; } if (telegramUser.photo_url) { authData.photo_url = telegramUser.photo_url; } const isValid = validateTelegramOAuth(authData, config.telegramBotToken); if (!isValid) { logSecurityEvent('INVALID_OAUTH_SIGNATURE', req, { telegramId: telegramUser.id, receivedData: { id: telegramUser.id, first_name: telegramUser.first_name, last_name: telegramUser.last_name, username: telegramUser.username, auth_date: auth_date } }); // В production строгая проверка, но для отладки можно временно отключить if (config.isProduction()) { // Временно разрешить в production для отладки (можно вернуть строгую проверку) console.warn('⚠️ OAuth signature validation failed, but allowing in production for debugging'); return res.status(401).json({ error: 'Неверная подпись Telegram OAuth' }); } } } // Найти или создать пользователя let user = await User.findOne({ telegramId: telegramUser.id.toString() }); if (!user) { user = new User({ telegramId: telegramUser.id.toString(), username: telegramUser.username || telegramUser.first_name || 'user', firstName: telegramUser.first_name || '', lastName: telegramUser.last_name || '', photoUrl: telegramUser.photo_url || null }); await user.save(); console.log(`✅ Создан новый пользователь через OAuth: ${user.username}`); } else { // Обновлять только если есть новые данные, не перезаписывать существующие пустыми значениями if (telegramUser.username) { user.username = telegramUser.username; } else if (!user.username && telegramUser.first_name) { // Если username пустой, использовать first_name как fallback user.username = telegramUser.first_name; } if (telegramUser.first_name) { user.firstName = telegramUser.first_name; } if (telegramUser.last_name !== undefined) { user.lastName = telegramUser.last_name || ''; } // Обновлять аватарку только если есть новая if (telegramUser.photo_url) { user.photoUrl = telegramUser.photo_url; } await user.save(); } // Подтянуть отсутствующие данные из Telegram await ensureUserData(user, telegramUser); // Получить полные данные пользователя const populatedUser = await User.findById(user._id).populate([ { path: 'followers', select: 'username firstName lastName photoUrl' }, { path: 'following', select: 'username firstName lastName photoUrl' } ]); const settings = normalizeUserSettings(populatedUser.settings); res.json({ success: true, user: { id: populatedUser._id, telegramId: populatedUser.telegramId, username: populatedUser.username, firstName: populatedUser.firstName, lastName: populatedUser.lastName, photoUrl: populatedUser.photoUrl, bio: populatedUser.bio, role: populatedUser.role, followersCount: populatedUser.followers.length, followingCount: populatedUser.following.length, settings, banned: populatedUser.banned } }); } catch (error) { console.error('Ошибка OAuth:', error); res.status(500).json({ error: 'Ошибка сервера' }); } }); router.post('/verify', authenticate, async (req, res) => { try { return respondWithUser(req.user, res); } catch (error) { console.error('Ошибка verify:', error); res.status(500).json({ error: 'Ошибка сервера' }); } }); // Проверка сохраненной сессии по telegramId (для OAuth пользователей) router.post('/session', async (req, res) => { try { const { telegramId } = req.body; if (!telegramId) { return res.status(400).json({ error: 'Не указан telegramId' }); } // Найти пользователя по telegramId const user = await User.findOne({ telegramId: telegramId.toString() }); if (!user) { return res.status(404).json({ error: OFFICIAL_CLIENT_MESSAGE }); } if (user.banned) { return res.status(403).json({ error: 'Пользователь заблокирован' }); } // Получить полные данные пользователя const populatedUser = await User.findById(user._id).populate([ { path: 'followers', select: 'username firstName lastName photoUrl' }, { path: 'following', select: 'username firstName lastName photoUrl' } ]); res.json({ success: true, user: { id: populatedUser._id, telegramId: populatedUser.telegramId, username: populatedUser.username, firstName: populatedUser.firstName, lastName: populatedUser.lastName, photoUrl: populatedUser.photoUrl, bio: populatedUser.bio, role: populatedUser.role, followersCount: populatedUser.followers.length, followingCount: populatedUser.following.length, settings, banned: populatedUser.banned } }); } catch (error) { console.error('Ошибка проверки сессии:', error); res.status(500).json({ error: 'Ошибка сервера' }); } }); module.exports = router;