Update files
This commit is contained in:
parent
0e5f67f9e0
commit
138eba28e8
|
|
@ -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 заголовка');
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
if (res.statusCode >= 400) {
|
||||||
log('error', 'Request failed', logData);
|
log('error', 'Request failed', logData);
|
||||||
} else if (res.statusCode >= 300) {
|
}
|
||||||
|
return; // Не логировать успешные запросы к публичным роутам
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.statusCode >= 400) {
|
||||||
|
log('error', 'Request failed', logData);
|
||||||
|
} 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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
// Инициализировать приложение только если еще не было вызвано
|
||||||
|
if (!initAppCalled.current) {
|
||||||
|
initAppCalled.current = true
|
||||||
initApp()
|
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 (не вызывает перезагрузку)
|
||||||
|
// Задержка чтобы компонент успел отрендериться с user
|
||||||
|
setTimeout(() => {
|
||||||
navigate(`/feed?post=${postId}`, { replace: true })
|
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) {
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue