Update files

This commit is contained in:
glpshchn 2025-11-05 01:41:35 +03:00
parent 0e5f67f9e0
commit 138eba28e8
6 changed files with 183 additions and 22 deletions

View File

@ -32,6 +32,30 @@ function validateTelegramWebAppData(initData, botToken) {
const authenticate = async (req, res, next) => { const authenticate = async (req, res, next) => {
try { try {
const initData = req.headers['x-telegram-init-data']; 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) { if (!initData) {
console.warn('⚠️ Нет x-telegram-init-data заголовка'); console.warn('⚠️ Нет x-telegram-init-data заголовка');

View File

@ -52,9 +52,19 @@ const requestLogger = (req, res, next) => {
userId: req.user?.id || 'anonymous' 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) { if (res.statusCode >= 400) {
log('error', 'Request failed', logData); log('error', 'Request failed', logData);
} else if (res.statusCode >= 300) { } else if (res.statusCode >= 300 && res.statusCode !== 304) {
// 304 - это нормально (кеш), не логируем
log('warn', 'Request redirect', logData); log('warn', 'Request redirect', logData);
} else { } else {
log('info', 'Request completed', logData); log('info', 'Request completed', logData);

View File

@ -97,7 +97,7 @@ router.post('/oauth', strictAuthLimiter, async (req, res) => {
if (config.isProduction()) { if (config.isProduction()) {
// Временно разрешить в production для отладки (можно вернуть строгую проверку) // Временно разрешить в production для отладки (можно вернуть строгую проверку)
console.warn('⚠️ OAuth signature validation failed, but allowing in production for debugging'); 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; module.exports = router;

View File

@ -1,7 +1,7 @@
import { BrowserRouter, Routes, Route, Navigate, useNavigate } from 'react-router-dom' import { BrowserRouter, Routes, Route, Navigate, useNavigate } from 'react-router-dom'
import { useState, useEffect, useRef } from 'react' import { useState, useEffect, useRef } from 'react'
import { initTelegramApp, getTelegramUser, isThirdPartyClient } from './utils/telegram' 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 { initTheme } from './utils/theme'
import Layout from './components/Layout' import Layout from './components/Layout'
import Feed from './pages/Feed' import Feed from './pages/Feed'
@ -21,11 +21,17 @@ function AppContent() {
const [showLogin, setShowLogin] = useState(false) const [showLogin, setShowLogin] = useState(false)
const navigate = useNavigate() const navigate = useNavigate()
const startParamProcessed = useRef(false) // Флаг для обработки startParam только один раз const startParamProcessed = useRef(false) // Флаг для обработки startParam только один раз
const initAppCalled = useRef(false) // Флаг чтобы initApp вызывался только один раз
useEffect(() => { useEffect(() => {
// Инициализировать тему // Инициализировать тему только один раз
initTheme() initTheme()
initApp()
// Инициализировать приложение только если еще не было вызвано
if (!initAppCalled.current) {
initAppCalled.current = true
initApp()
}
}, []) }, [])
const initApp = async () => { const initApp = async () => {
@ -36,26 +42,24 @@ function AppContent() {
// Проверить наличие Telegram Web App API // Проверить наличие Telegram Web App API
const tg = window.Telegram?.WebApp 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 // Дать время на полную инициализацию Telegram Web App
await new Promise(resolve => setTimeout(resolve, 300)) await new Promise(resolve => setTimeout(resolve, 300))
// Проверить наличие initData (главный индикатор что мы в Telegram) // Проверить наличие initData (главный индикатор что мы в Telegram)
const initData = tg.initData || '' const initData = tg?.initData || ''
if (initData) { if (initData) {
// Есть initData - пробуем авторизоваться через API // Есть initData - пробуем авторизоваться через API
try { try {
const userData = await verifyAuth() const userData = await verifyAuth()
// Сохранить сессию для будущих загрузок
localStorage.setItem('nakama_user', JSON.stringify(userData))
localStorage.setItem('nakama_auth_type', 'telegram')
// КРИТИЧНО: Сначала установить loading в false, потом user
// Это предотвращает бесконечную загрузку
setLoading(false)
setUser(userData) setUser(userData)
// Обработать параметр start из Telegram (только один раз) // Обработать параметр start из Telegram (только один раз)
@ -63,20 +67,66 @@ function AppContent() {
startParamProcessed.current = true // Пометить как обработанный startParamProcessed.current = true // Пометить как обработанный
const postId = tg.startParam.replace('post_', '') const postId = tg.startParam.replace('post_', '')
// Использовать navigate вместо window.location.href (не вызывает перезагрузку) // Использовать navigate вместо window.location.href (не вызывает перезагрузку)
navigate(`/feed?post=${postId}`, { replace: true }) // Задержка чтобы компонент успел отрендериться с user
setTimeout(() => {
navigate(`/feed?post=${postId}`, { replace: true })
}, 200)
} }
return return
} catch (authError) { } catch (authError) {
console.error('Ошибка авторизации через API:', 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) setShowLogin(true)
setLoading(false) setLoading(false)
return return
} }
} }
// Если нет initData, но есть WebApp API - это странно // Если нет initData, проверяем сохраненную сессию OAuth
// Показываем Login Widget 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) setShowLogin(true)
setLoading(false) setLoading(false)
} catch (err) { } catch (err) {
@ -91,6 +141,11 @@ function AppContent() {
setLoading(true) setLoading(true)
// Отправить данные OAuth на backend // Отправить данные OAuth на backend
const userData = await authWithTelegramOAuth(telegramUser) const userData = await authWithTelegramOAuth(telegramUser)
// Сохранить сессию для будущих загрузок
localStorage.setItem('nakama_user', JSON.stringify(userData))
localStorage.setItem('nakama_auth_type', 'oauth')
setUser(userData) setUser(userData)
setShowLogin(false) setShowLogin(false)
} catch (err) { } catch (err) {

View File

@ -27,6 +27,7 @@ export default function Feed({ user }) {
} else { } else {
loadPosts() loadPosts()
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filter, searchParams]) }, [filter, searchParams])
const loadSpecificPost = async (postId) => { const loadSpecificPost = async (postId) => {

View File

@ -16,13 +16,29 @@ const api = axios.create({
} }
}) })
// Добавить interceptor для добавления Telegram Init Data // Добавить interceptor для добавления Telegram Init Data или сохраненной сессии
api.interceptors.request.use((config) => { api.interceptors.request.use((config) => {
const initData = getTelegramInitData() const initData = getTelegramInitData()
// Отправляем initData только если есть // Отправляем initData если есть (для Telegram Mini App)
if (initData) { if (initData) {
config.headers['x-telegram-init-data'] = 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 return config
@ -34,6 +50,12 @@ export const verifyAuth = async () => {
return response.data.user 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) // Авторизация через Telegram OAuth (Login Widget)
export const authWithTelegramOAuth = async (userData) => { export const authWithTelegramOAuth = async (userData) => {
// userData от Telegram Login Widget содержит: id, first_name, last_name, username, photo_url, auth_date, hash // userData от Telegram Login Widget содержит: id, first_name, last_name, username, photo_url, auth_date, hash