diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index c4bad73..54f1e70 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -32,6 +32,30 @@ function validateTelegramWebAppData(initData, botToken) { const authenticate = async (req, res, next) => { try { const initData = req.headers['x-telegram-init-data']; + const telegramUserId = req.headers['x-telegram-user-id']; + + // Если нет initData, но есть telegramUserId (сохраненная OAuth сессия) + if (!initData && telegramUserId) { + try { + // Найти пользователя по telegramId + const user = await User.findOne({ telegramId: telegramUserId.toString() }); + + if (!user) { + return res.status(401).json({ error: 'Пользователь не найден' }); + } + + if (user.banned) { + return res.status(403).json({ error: 'Пользователь заблокирован' }); + } + + req.user = user; + req.telegramUser = { id: user.telegramId }; + return next(); + } catch (error) { + console.error('❌ Ошибка авторизации по сохраненной сессии:', error); + return res.status(401).json({ error: 'Ошибка авторизации' }); + } + } if (!initData) { console.warn('⚠️ Нет x-telegram-init-data заголовка'); diff --git a/backend/middleware/logger.js b/backend/middleware/logger.js index 1d723e1..3c48e42 100644 --- a/backend/middleware/logger.js +++ b/backend/middleware/logger.js @@ -52,9 +52,19 @@ const requestLogger = (req, res, next) => { userId: req.user?.id || 'anonymous' }; + // Пропустить логирование для публичных роутов (health, корневой роут) + if (req.path === '/health' || req.path === '/') { + // Логировать только ошибки для публичных роутов + if (res.statusCode >= 400) { + log('error', 'Request failed', logData); + } + return; // Не логировать успешные запросы к публичным роутам + } + if (res.statusCode >= 400) { log('error', 'Request failed', logData); - } else if (res.statusCode >= 300) { + } else if (res.statusCode >= 300 && res.statusCode !== 304) { + // 304 - это нормально (кеш), не логируем log('warn', 'Request redirect', logData); } else { log('info', 'Request completed', logData); diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 0b53132..3e4e97c 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -97,7 +97,7 @@ router.post('/oauth', strictAuthLimiter, async (req, res) => { if (config.isProduction()) { // Временно разрешить в production для отладки (можно вернуть строгую проверку) console.warn('⚠️ OAuth signature validation failed, but allowing in production for debugging'); - // return res.status(401).json({ error: 'Неверная подпись Telegram OAuth' }); + return res.status(401).json({ error: 'Неверная подпись Telegram OAuth' }); } } } @@ -185,5 +185,54 @@ router.post('/verify', authenticate, async (req, res) => { } }); +// Проверка сохраненной сессии по 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: 'Пользователь не найден' }); + } + + 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: populatedUser.settings, + banned: populatedUser.banned + } + }); + } catch (error) { + console.error('Ошибка проверки сессии:', error); + res.status(500).json({ error: 'Ошибка сервера' }); + } +}); + module.exports = router; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d85d933..91c493a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,7 +1,7 @@ import { BrowserRouter, Routes, Route, Navigate, useNavigate } from 'react-router-dom' import { useState, useEffect, useRef } from 'react' import { initTelegramApp, getTelegramUser, isThirdPartyClient } from './utils/telegram' -import { verifyAuth, authWithTelegramOAuth } from './utils/api' +import { verifyAuth, authWithTelegramOAuth, verifySession } from './utils/api' import { initTheme } from './utils/theme' import Layout from './components/Layout' import Feed from './pages/Feed' @@ -21,11 +21,17 @@ function AppContent() { const [showLogin, setShowLogin] = useState(false) const navigate = useNavigate() const startParamProcessed = useRef(false) // Флаг для обработки startParam только один раз + const initAppCalled = useRef(false) // Флаг чтобы initApp вызывался только один раз useEffect(() => { - // Инициализировать тему + // Инициализировать тему только один раз initTheme() - initApp() + + // Инициализировать приложение только если еще не было вызвано + if (!initAppCalled.current) { + initAppCalled.current = true + initApp() + } }, []) const initApp = async () => { @@ -36,26 +42,24 @@ function AppContent() { // Проверить наличие Telegram Web App API const tg = window.Telegram?.WebApp - // Если нет Telegram Web App API вообще, показываем Login Widget - if (!tg) { - setShowLogin(true) - setLoading(false) - return - } - - // В официальном клиенте Telegram есть initData (даже если user еще не распарсен) - // Пробуем авторизоваться через API - backend распарсит initData - // Дать время на полную инициализацию Telegram Web App await new Promise(resolve => setTimeout(resolve, 300)) // Проверить наличие initData (главный индикатор что мы в Telegram) - const initData = tg.initData || '' + const initData = tg?.initData || '' if (initData) { // Есть initData - пробуем авторизоваться через API try { const userData = await verifyAuth() + + // Сохранить сессию для будущих загрузок + localStorage.setItem('nakama_user', JSON.stringify(userData)) + localStorage.setItem('nakama_auth_type', 'telegram') + + // КРИТИЧНО: Сначала установить loading в false, потом user + // Это предотвращает бесконечную загрузку + setLoading(false) setUser(userData) // Обработать параметр start из Telegram (только один раз) @@ -63,20 +67,66 @@ function AppContent() { startParamProcessed.current = true // Пометить как обработанный const postId = tg.startParam.replace('post_', '') // Использовать navigate вместо window.location.href (не вызывает перезагрузку) - navigate(`/feed?post=${postId}`, { replace: true }) + // Задержка чтобы компонент успел отрендериться с user + setTimeout(() => { + navigate(`/feed?post=${postId}`, { replace: true }) + }, 200) } return } catch (authError) { console.error('Ошибка авторизации через API:', authError) - // Если авторизация не удалась, показываем Login Widget + // Если авторизация не удалась, проверяем сохраненную сессию + const savedUser = localStorage.getItem('nakama_user') + if (savedUser) { + try { + const userData = JSON.parse(savedUser) + // Проверить что сессия еще актуальна (можно добавить проверку времени) + setUser(userData) + setLoading(false) + return + } catch (e) { + console.error('Ошибка восстановления сессии:', e) + localStorage.removeItem('nakama_user') + localStorage.removeItem('nakama_auth_type') + } + } + // Если нет сохраненной сессии, показываем Login Widget setShowLogin(true) setLoading(false) return } } - // Если нет initData, но есть WebApp API - это странно - // Показываем Login Widget + // Если нет initData, проверяем сохраненную сессию OAuth + const savedUser = localStorage.getItem('nakama_user') + const authType = localStorage.getItem('nakama_auth_type') + + if (savedUser && authType === 'oauth') { + try { + const userData = JSON.parse(savedUser) + + // Проверить сессию на сервере (обновить данные пользователя) + try { + const freshUserData = await verifySession(userData.telegramId) + // Обновить сохраненную сессию + localStorage.setItem('nakama_user', JSON.stringify(freshUserData)) + setUser(freshUserData) + } catch (sessionError) { + console.error('Ошибка проверки сессии:', sessionError) + // Если проверка не удалась, использовать сохраненные данные + setUser(userData) + } + + setLoading(false) + return + } catch (e) { + console.error('Ошибка восстановления сессии:', e) + localStorage.removeItem('nakama_user') + localStorage.removeItem('nakama_auth_type') + } + } + + // Если нет сохраненной сессии и нет initData, показываем Login Widget setShowLogin(true) setLoading(false) } catch (err) { @@ -91,6 +141,11 @@ function AppContent() { setLoading(true) // Отправить данные OAuth на backend const userData = await authWithTelegramOAuth(telegramUser) + + // Сохранить сессию для будущих загрузок + localStorage.setItem('nakama_user', JSON.stringify(userData)) + localStorage.setItem('nakama_auth_type', 'oauth') + setUser(userData) setShowLogin(false) } catch (err) { diff --git a/frontend/src/pages/Feed.jsx b/frontend/src/pages/Feed.jsx index 9dc0263..f08e989 100644 --- a/frontend/src/pages/Feed.jsx +++ b/frontend/src/pages/Feed.jsx @@ -27,6 +27,7 @@ export default function Feed({ user }) { } else { loadPosts() } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [filter, searchParams]) const loadSpecificPost = async (postId) => { diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index 2835229..8a3fd9b 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -16,13 +16,29 @@ const api = axios.create({ } }) -// Добавить interceptor для добавления Telegram Init Data +// Добавить interceptor для добавления Telegram Init Data или сохраненной сессии api.interceptors.request.use((config) => { const initData = getTelegramInitData() - // Отправляем initData только если есть + // Отправляем initData если есть (для Telegram Mini App) if (initData) { config.headers['x-telegram-init-data'] = initData + } else { + // Если нет initData, но есть сохраненная сессия OAuth - отправляем telegramId + const savedUser = localStorage.getItem('nakama_user') + const authType = localStorage.getItem('nakama_auth_type') + + if (savedUser && authType === 'oauth') { + try { + const userData = JSON.parse(savedUser) + if (userData.telegramId) { + // Отправляем telegramId для авторизации по сохраненной сессии + config.headers['x-telegram-user-id'] = userData.telegramId + } + } catch (e) { + // Игнорируем ошибки парсинга + } + } } return config @@ -34,6 +50,12 @@ export const verifyAuth = async () => { return response.data.user } +// Проверка сохраненной сессии (для OAuth пользователей) +export const verifySession = async (telegramId) => { + const response = await api.post('/auth/session', { telegramId }) + return response.data.user +} + // Авторизация через Telegram OAuth (Login Widget) export const authWithTelegramOAuth = async (userData) => { // userData от Telegram Login Widget содержит: id, first_name, last_name, username, photo_url, auth_date, hash