Update files

This commit is contained in:
glpshchn 2025-11-11 00:56:36 +03:00
parent 723b6824ea
commit d6fcfc5c17
11 changed files with 369 additions and 295 deletions

View File

@ -14,6 +14,14 @@ module.exports = {
// JWT // JWT
jwtSecret: process.env.JWT_SECRET || 'nakama_secret_key_change_in_production', jwtSecret: process.env.JWT_SECRET || 'nakama_secret_key_change_in_production',
jwt: {
accessSecret: process.env.JWT_ACCESS_SECRET || process.env.JWT_SECRET || 'nakama_access_secret_change_me',
refreshSecret: process.env.JWT_REFRESH_SECRET || process.env.JWT_SECRET || 'nakama_refresh_secret_change_me',
accessExpiresIn: parseInt(process.env.JWT_ACCESS_EXPIRES_IN || '300', 10), // 5 минут
refreshExpiresIn: parseInt(process.env.JWT_REFRESH_EXPIRES_IN || '604800', 10), // 7 дней
accessCookieName: process.env.JWT_ACCESS_COOKIE_NAME || 'nakama_access_token',
refreshCookieName: process.env.JWT_REFRESH_COOKIE_NAME || 'nakama_refresh_token'
},
// Telegram // Telegram
telegramBotToken: process.env.TELEGRAM_BOT_TOKEN, telegramBotToken: process.env.TELEGRAM_BOT_TOKEN,

View File

@ -1,12 +1,19 @@
const crypto = require('crypto');
const User = require('../models/User'); const User = require('../models/User');
const { validateTelegramId } = require('./validator'); const { validateTelegramId } = require('./validator');
const { logSecurityEvent } = require('./logger'); const { logSecurityEvent } = require('./logger');
const config = require('../config'); const config = require('../config');
const {
ACCESS_COOKIE,
REFRESH_COOKIE,
signAuthTokens,
setAuthCookies,
clearAuthCookies,
verifyAccessToken,
verifyRefreshToken
} = require('../utils/tokens');
const OFFICIAL_CLIENT_MESSAGE = 'Используйте официальный клиент. Сообщите об ошибке в https://t.me/NakamaReportbot'; const OFFICIAL_CLIENT_MESSAGE = 'Используйте официальный клиент. Сообщите об ошибке в https://t.me/NakamaReportbot';
const ALLOWED_SEARCH_PREFERENCES = ['furry', 'anime']; const ALLOWED_SEARCH_PREFERENCES = ['furry', 'anime'];
const MAX_AUTH_AGE_SECONDS = 5 * 60; // 5 минут
const touchUserActivity = async (user) => { const touchUserActivity = async (user) => {
if (!user) return; if (!user) return;
@ -49,124 +56,72 @@ const ensureUserSettings = async (user) => {
} }
}; };
function validateTelegramWebAppData(initData, botToken) {
if (!botToken) {
throw new Error('TELEGRAM_BOT_TOKEN не настроен');
}
const params = new URLSearchParams(initData);
const hash = params.get('hash');
const authDate = Number(params.get('auth_date'));
if (!hash) {
throw new Error('Отсутствует hash в initData');
}
if (!authDate) {
throw new Error('Отсутствует auth_date в initData');
}
const dataCheck = [];
for (const [key, value] of params.entries()) {
if (key === 'hash') continue;
dataCheck.push(`${key}=${value}`);
}
dataCheck.sort((a, b) => a.localeCompare(b));
const dataCheckString = dataCheck.join('\n');
const secretKey = crypto.createHmac('sha256', 'WebAppData').update(botToken).digest();
const calculatedHash = crypto.createHmac('sha256', secretKey).update(dataCheckString).digest('hex');
if (calculatedHash !== hash) {
throw new Error('Неверная подпись initData');
}
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - authDate) > MAX_AUTH_AGE_SECONDS) {
throw new Error('Данные авторизации устарели');
}
return params;
}
// Middleware для проверки авторизации // Middleware для проверки авторизации
const authenticate = async (req, res, next) => { const authenticate = async (req, res, next) => {
try { try {
const initData = req.headers['x-telegram-init-data']; const accessToken = req.cookies[ACCESS_COOKIE];
const refreshToken = req.cookies[REFRESH_COOKIE];
if (!initData) { let tokenPayload = null;
logSecurityEvent('MISSING_INITDATA', req);
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
}
if (!config.telegramBotToken) { if (accessToken) {
logSecurityEvent('MISSING_BOT_TOKEN', req); try {
if (config.isProduction()) { tokenPayload = verifyAccessToken(accessToken);
return res.status(500).json({ error: 'Сервер некорректно настроен для авторизации через Telegram' }); } catch (error) {
if (error.name !== 'TokenExpiredError') {
logSecurityEvent('INVALID_ACCESS_TOKEN', req, { error: error.message });
clearAuthCookies(res);
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
}
} }
console.warn('⚠️ TELEGRAM_BOT_TOKEN не установлен, пропускаем проверку подписи (dev режим)');
} }
let params; if (!tokenPayload && refreshToken) {
try { try {
if (config.telegramBotToken) { const refreshPayload = verifyRefreshToken(refreshToken);
params = validateTelegramWebAppData(initData, config.telegramBotToken); const userForRefresh = await User.findById(refreshPayload.userId);
} else {
params = new URLSearchParams(initData); if (!userForRefresh) {
clearAuthCookies(res);
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
}
const tokens = signAuthTokens(userForRefresh);
setAuthCookies(res, tokens);
tokenPayload = verifyAccessToken(tokens.accessToken);
} catch (error) {
logSecurityEvent('INVALID_REFRESH_TOKEN', req, { error: error.message });
clearAuthCookies(res);
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
} }
} catch (validationError) {
logSecurityEvent('INVALID_INITDATA_SIGNATURE', req, { reason: validationError.message });
return res.status(401).json({ error: `${validationError.message}. ${OFFICIAL_CLIENT_MESSAGE}` });
} }
const userParam = params.get('user'); if (!tokenPayload) {
logSecurityEvent('AUTH_TOKEN_MISSING', req);
if (!userParam) {
logSecurityEvent('MISSING_INITDATA_USER', req);
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE }); return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
} }
let telegramUser; if (!validateTelegramId(tokenPayload.telegramId)) {
try { logSecurityEvent('INVALID_TELEGRAM_ID', req, { telegramId: tokenPayload.telegramId });
telegramUser = JSON.parse(userParam); clearAuthCookies(res);
} catch (parseError) {
logSecurityEvent('INVALID_INITDATA_USER_JSON', req, { error: parseError.message });
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
}
req.telegramUser = telegramUser;
if (!validateTelegramId(telegramUser.id)) {
logSecurityEvent('INVALID_TELEGRAM_ID', req, { telegramId: telegramUser.id });
return res.status(401).json({ error: 'Неверный ID пользователя' }); return res.status(401).json({ error: 'Неверный ID пользователя' });
} }
// Найти или создать пользователя let user = await User.findOne({ telegramId: tokenPayload.telegramId.toString() });
let user = await User.findOne({ telegramId: telegramUser.id.toString() });
if (!user) { if (!user) {
user = new User({ clearAuthCookies(res);
telegramId: telegramUser.id.toString(), return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
username: telegramUser.username || telegramUser.first_name, }
firstName: telegramUser.first_name,
lastName: telegramUser.last_name, if (user.banned) {
photoUrl: telegramUser.photo_url clearAuthCookies(res);
}); return res.status(403).json({ error: 'Пользователь заблокирован' });
await user.save();
console.log(`✅ Создан новый пользователь: ${user.username}`);
} else {
user.username = telegramUser.username || telegramUser.first_name;
user.firstName = telegramUser.first_name;
user.lastName = telegramUser.last_name;
if (telegramUser.photo_url) {
user.photoUrl = telegramUser.photo_url;
}
await user.save();
} }
await ensureUserSettings(user); await ensureUserSettings(user);
await touchUserActivity(user); await touchUserActivity(user);
req.user = user; req.user = user;
req.telegramUser = { id: user.telegramId };
next(); next();
} catch (error) { } catch (error) {
console.error('❌ Ошибка авторизации:', error); console.error('❌ Ошибка авторизации:', error);
@ -193,5 +148,7 @@ const requireAdmin = (req, res, next) => {
module.exports = { module.exports = {
authenticate, authenticate,
requireModerator, requireModerator,
requireAdmin requireAdmin,
touchUserActivity,
ensureUserSettings
}; };

View File

@ -6,6 +6,9 @@ const config = require('../config');
const { validateTelegramId } = require('../middleware/validator'); const { validateTelegramId } = require('../middleware/validator');
const { logSecurityEvent } = require('../middleware/logger'); const { logSecurityEvent } = require('../middleware/logger');
const { strictAuthLimiter } = require('../middleware/security'); const { strictAuthLimiter } = require('../middleware/security');
const { validateAndParseInitData } = require('../utils/telegram');
const { signAuthTokens, setAuthCookies, clearAuthCookies } = require('../utils/tokens');
const { touchUserActivity, ensureUserSettings } = require('../middleware/auth');
const ALLOWED_SEARCH_PREFERENCES = ['furry', 'anime']; const ALLOWED_SEARCH_PREFERENCES = ['furry', 'anime'];
@ -31,6 +34,93 @@ const normalizeUserSettings = (settings = {}) => {
const OFFICIAL_CLIENT_MESSAGE = 'Используйте официальный клиент. Сообщите об ошибке в https://t.me/NakamaReportbot'; const OFFICIAL_CLIENT_MESSAGE = 'Используйте официальный клиент. Сообщите об ошибке в https://t.me/NakamaReportbot';
router.post('/signin', strictAuthLimiter, async (req, res) => {
try {
const { initData } = req.body || {};
if (!initData || typeof initData !== 'string') {
return res.status(400).json({ error: 'initData обязателен' });
}
let telegramUser;
try {
({ telegramUser } = validateAndParseInitData(initData, req));
} catch (error) {
return res.status(401).json({ error: error.message });
}
if (!validateTelegramId(telegramUser.id)) {
logSecurityEvent('INVALID_TELEGRAM_ID', req, { telegramId: telegramUser.id });
return res.status(400).json({ error: 'Неверный ID пользователя' });
}
let user = await User.findOne({ telegramId: telegramUser.id.toString() });
if (!user) {
user = new User({
telegramId: telegramUser.id.toString(),
username: telegramUser.username || telegramUser.first_name,
firstName: telegramUser.first_name,
lastName: telegramUser.last_name,
photoUrl: telegramUser.photo_url
});
await user.save();
} else {
user.username = telegramUser.username || telegramUser.first_name;
user.firstName = telegramUser.first_name;
user.lastName = telegramUser.last_name;
if (telegramUser.photo_url) {
user.photoUrl = telegramUser.photo_url;
}
await user.save();
}
if (user.banned) {
return res.status(403).json({ error: 'Пользователь заблокирован' });
}
await ensureUserSettings(user);
await touchUserActivity(user);
const tokens = signAuthTokens(user);
setAuthCookies(res, tokens);
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
}
});
} catch (error) {
console.error('Ошибка signin:', error);
res.status(500).json({ error: 'Ошибка авторизации' });
}
});
router.post('/logout', (req, res) => {
clearAuthCookies(res);
res.json({ success: true });
});
// Проверка подписи Telegram OAuth (Login Widget) // Проверка подписи Telegram OAuth (Login Widget)
function validateTelegramOAuth(authData, botToken) { function validateTelegramOAuth(authData, botToken) {
if (!authData || !authData.hash) { if (!authData || !authData.hash) {

View File

@ -4,6 +4,7 @@ const cors = require('cors');
const dotenv = require('dotenv'); const dotenv = require('dotenv');
const path = require('path'); const path = require('path');
const http = require('http'); const http = require('http');
const cookieParser = require('cookie-parser');
// Загрузить переменные окружения ДО импорта config // Загрузить переменные окружения ДО импорта config
dotenv.config({ path: path.join(__dirname, '.env') }); dotenv.config({ path: path.join(__dirname, '.env') });
@ -55,6 +56,7 @@ app.use(cors(corsOptions));
// Body parsing с ограничениями // Body parsing с ограничениями
app.use(express.json({ limit: '10mb' })); app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(cookieParser());
// Security middleware // Security middleware
app.use(sanitizeMongo); // Защита от NoSQL injection app.use(sanitizeMongo); // Защита от NoSQL injection

67
backend/utils/telegram.js Normal file
View File

@ -0,0 +1,67 @@
const crypto = require('crypto');
const config = require('../config');
const MAX_AUTH_AGE_SECONDS = 5 * 60;
function validateAndParseInitData(initData, req) {
if (!config.telegramBotToken) {
throw new Error('TELEGRAM_BOT_TOKEN не настроен');
}
const params = new URLSearchParams(initData);
const hash = params.get('hash');
const authDate = Number(params.get('auth_date'));
if (!hash) {
throw new Error('Отсутствует hash в initData');
}
if (!authDate) {
throw new Error('Отсутствует auth_date в initData');
}
const dataCheck = [];
for (const [key, value] of params.entries()) {
if (key === 'hash') continue;
dataCheck.push(`${key}=${value}`);
}
dataCheck.sort((a, b) => a.localeCompare(b));
const dataCheckString = dataCheck.join('\n');
const secretKey = crypto.createHmac('sha256', 'WebAppData').update(config.telegramBotToken).digest();
const calculatedHash = crypto.createHmac('sha256', secretKey).update(dataCheckString).digest('hex');
if (calculatedHash !== hash) {
throw new Error('Неверная подпись initData');
}
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - authDate) > MAX_AUTH_AGE_SECONDS) {
throw new Error('Данные авторизации устарели');
}
const userParam = params.get('user');
if (!userParam) {
throw new Error('Отсутствует пользователь в initData');
}
let telegramUser;
try {
telegramUser = JSON.parse(userParam);
} catch (error) {
throw new Error('Некорректный формат user в initData');
}
if (!telegramUser || !telegramUser.id) {
throw new Error('Отсутствует ID пользователя в initData');
}
return { params, telegramUser };
}
module.exports = {
validateAndParseInitData
};

67
backend/utils/tokens.js Normal file
View File

@ -0,0 +1,67 @@
const jwt = require('jsonwebtoken');
const config = require('../config');
const ACCESS_COOKIE = config.jwt.accessCookieName;
const REFRESH_COOKIE = config.jwt.refreshCookieName;
const buildPayload = (user) => ({
userId: user._id.toString(),
telegramId: user.telegramId,
role: user.role
});
const signAccessToken = (user) =>
jwt.sign(buildPayload(user), config.jwt.accessSecret, {
expiresIn: `${config.jwt.accessExpiresIn}s`
});
const signRefreshToken = (user) =>
jwt.sign(buildPayload(user), config.jwt.refreshSecret, {
expiresIn: `${config.jwt.refreshExpiresIn}s`
});
const signAuthTokens = (user) => ({
accessToken: signAccessToken(user),
refreshToken: signRefreshToken(user)
});
const getCookieBaseOptions = () => ({
httpOnly: true,
secure: config.isProduction(),
sameSite: config.isProduction() ? 'lax' : 'lax',
path: '/'
});
const setAuthCookies = (res, tokens) => {
const base = getCookieBaseOptions();
res.cookie(ACCESS_COOKIE, tokens.accessToken, {
...base,
maxAge: config.jwt.accessExpiresIn * 1000
});
res.cookie(REFRESH_COOKIE, tokens.refreshToken, {
...base,
maxAge: config.jwt.refreshExpiresIn * 1000
});
};
const clearAuthCookies = (res) => {
const base = getCookieBaseOptions();
res.clearCookie(ACCESS_COOKIE, base);
res.clearCookie(REFRESH_COOKIE, base);
};
const verifyAccessToken = (token) => jwt.verify(token, config.jwt.accessSecret);
const verifyRefreshToken = (token) => jwt.verify(token, config.jwt.refreshSecret);
module.exports = {
ACCESS_COOKIE,
REFRESH_COOKIE,
signAuthTokens,
setAuthCookies,
clearAuthCookies,
verifyAccessToken,
verifyRefreshToken
};

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 } from './utils/telegram' import { initTelegramApp } from './utils/telegram'
import { verifyAuth, verifySession } from './utils/api' import { signInWithTelegram, verifyAuth } 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'
@ -17,120 +17,68 @@ function AppContent() {
const [user, setUser] = useState(null) const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState(null) const [error, setError] = useState(null)
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 вызывался только один раз const initAppCalled = useRef(false) // Флаг чтобы initApp вызывался только один раз
useEffect(() => { useEffect(() => {
// Инициализировать тему только один раз
initTheme() initTheme()
// Инициализировать приложение только если еще не было вызвано
if (!initAppCalled.current) { if (!initAppCalled.current) {
initAppCalled.current = true initAppCalled.current = true
initApp() initApp()
} }
}, []) }, [])
const waitForInitData = async () => {
const start = Date.now()
const timeout = 5000
while (Date.now() - start < timeout) {
const tg = window.Telegram?.WebApp
if (tg?.initData && tg.initData.length > 0) {
return tg
}
await new Promise(resolve => setTimeout(resolve, 100))
}
throw new Error('Telegram не передал initData. Откройте приложение в официальном клиенте.')
}
const initApp = async () => { const initApp = async () => {
try { try {
// Инициализация Telegram Web App
initTelegramApp() initTelegramApp()
// Проверить наличие Telegram Web App API const tg = await waitForInitData()
const tg = window.Telegram?.WebApp
// Дать время на полную инициализацию Telegram Web App tg.disableVerticalSwipes?.()
await new Promise(resolve => setTimeout(resolve, 300)) tg.ready?.()
tg.expand?.()
// Проверить наличие initData (главный индикатор что мы в Telegram) const userData = await signInWithTelegram(tg.initData)
const initData = tg?.initData || '' setUser(userData)
setError(null)
if (initData) { if (!startParamProcessed.current && tg?.startParam?.startsWith('post_')) {
// Есть initData - пробуем авторизоваться через API startParamProcessed.current = true
try { const postId = tg.startParam.replace('post_', '')
const userData = await verifyAuth() setTimeout(() => {
navigate(`/feed?post=${postId}`, { replace: true })
// Сохранить сессию для будущих загрузок }, 200)
localStorage.setItem('nakama_user', JSON.stringify(userData))
localStorage.setItem('nakama_auth_type', 'telegram')
// КРИТИЧНО: Сначала установить loading в false, потом user
// Это предотвращает бесконечную загрузку
setLoading(false)
setUser(userData)
// Обработать параметр start из Telegram (только один раз)
if (!startParamProcessed.current && tg?.startParam?.startsWith('post_')) {
startParamProcessed.current = true // Пометить как обработанный
const postId = tg.startParam.replace('post_', '')
// Использовать navigate вместо window.location.href (не вызывает перезагрузку)
// Задержка чтобы компонент успел отрендериться с user
setTimeout(() => {
navigate(`/feed?post=${postId}`, { replace: true })
}, 200)
}
return
} catch (authError) {
console.error('Ошибка авторизации через API:', authError)
// Если авторизация не удалась, проверяем сохраненную сессию
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, проверяем сохраненную сессию 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) { } catch (err) {
console.error('Ошибка инициализации:', err) console.error('Ошибка инициализации:', err)
setError(err.message)
try {
// Попытаться восстановить сессию по токенам
const userData = await verifyAuth()
setUser(userData)
setError(null)
} catch (verifyError) {
console.error('Не удалось восстановить сессию:', verifyError)
setError(err?.response?.data?.error || err.message || 'Ошибка авторизации')
}
} finally {
setLoading(false) setLoading(false)
} }
} }
@ -151,46 +99,6 @@ function AppContent() {
) )
} }
// Показать Login Widget если нет авторизации
if (showLogin) {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
padding: '24px',
textAlign: 'center',
flexDirection: 'column',
gap: '16px'
}}>
<h2 style={{ color: 'var(--text-primary)', margin: 0 }}>Используйте официальный клиент Telegram</h2>
<p style={{ color: 'var(--text-secondary)', margin: 0, maxWidth: '360px', lineHeight: 1.5 }}>
Для доступа к NakamaHost откройте бота в официальном приложении Telegram.
Если вы уже используете официальный клиент и видите это сообщение,
пожалуйста сообщите об ошибке в&nbsp;
<a href="https://t.me/NakamaReportbot" style={{ color: 'var(--text-primary)', textDecoration: 'underline' }}>
@NakamaReportbot
</a>.
</p>
<button
onClick={() => window.location.reload()}
style={{
padding: '12px 24px',
borderRadius: '12px',
border: 'none',
background: 'var(--bg-primary)',
color: 'var(--text-primary)',
fontSize: '16px',
cursor: 'pointer'
}}
>
Перезагрузить
</button>
</div>
)
}
if (error) { if (error) {
return ( return (
<div style={{ <div style={{
@ -209,10 +117,7 @@ function AppContent() {
{error} {error}
</p> </p>
<button <button
onClick={() => { onClick={() => window.location.reload()}
setError(null)
setShowLogin(true)
}}
style={{ style={{
padding: '12px 24px', padding: '12px 24px',
borderRadius: '12px', borderRadius: '12px',

View File

@ -1,5 +1,4 @@
import axios from 'axios' import axios from 'axios'
import { getTelegramInitData } from './telegram'
// API URL из переменных окружения // API URL из переменных окружения
const API_URL = import.meta.env.VITE_API_URL || ( const API_URL = import.meta.env.VITE_API_URL || (
@ -11,48 +10,20 @@ const API_URL = import.meta.env.VITE_API_URL || (
// Создать инстанс axios с настройками // Создать инстанс axios с настройками
const api = axios.create({ const api = axios.create({
baseURL: API_URL, baseURL: API_URL,
withCredentials: true,
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
}) })
// Добавить interceptor для добавления Telegram Init Data или сохраненной сессии
api.interceptors.request.use((config) => {
const initData = getTelegramInitData()
// Отправляем 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
})
// Auth API // Auth API
export const verifyAuth = async () => { export const signInWithTelegram = async (initData) => {
const response = await api.post('/auth/verify') const response = await api.post('/auth/signin', { initData })
return response.data.user return response.data.user
} }
// Проверка сохраненной сессии (для OAuth пользователей) export const verifyAuth = async () => {
export const verifySession = async (telegramId) => { const response = await api.post('/auth/verify')
const response = await api.post('/auth/session', { telegramId })
return response.data.user return response.data.user
} }

View File

@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { import {
signInWithTelegram,
verifyAuth, verifyAuth,
fetchUsers, fetchUsers,
banUser, banUser,
@ -129,22 +130,36 @@ export default function App() {
const app = await waitForInitData(); const app = await waitForInitData();
if (cancelled) return; if (cancelled) return;
const initData = app.initData || ''; const initData = app.initData;
if (!initData) { if (!initData) {
throw new Error('Откройте модераторский интерфейс из Telegram (бот @rbachbot).'); throw new Error('Telegram не передал initData');
} }
const response = await verifyAuth(); const userData = await signInWithTelegram(initData);
if (cancelled) return; if (cancelled) return;
setUser(response.data.user); setUser(userData);
setError(null); setError(null);
} catch (err) { } catch (err) {
if (cancelled) return; if (cancelled) return;
console.error('Ошибка инициализации модератора:', err); console.error('Ошибка инициализации модератора:', err);
const serverMessage = err?.response?.data?.error || err?.message; try {
setError(serverMessage || 'Нет доступа. Убедитесь, что вы добавлены как администратор.'); const userData = await verifyAuth();
if (!cancelled) {
setUser(userData);
setError(null);
}
} catch (verifyError) {
if (!cancelled) {
const message =
err?.response?.data?.error ||
verifyError?.response?.data?.error ||
err?.message ||
'Нет доступа. Убедитесь, что вы добавлены как администратор.';
setError(message);
}
}
} finally { } finally {
if (!cancelled) { if (!cancelled) {
setLoading(false); setLoading(false);

View File

@ -6,22 +6,13 @@ const API_URL =
const api = axios.create({ const api = axios.create({
baseURL: API_URL, baseURL: API_URL,
withCredentials: false withCredentials: true
}) })
const getInitData = () => window.Telegram?.WebApp?.initData || null export const signInWithTelegram = (initData) =>
api.post('/auth/signin', { initData }).then((res) => res.data.user)
api.interceptors.request.use((config) => { export const verifyAuth = () => api.post('/mod-app/auth/verify').then((res) => res.data.user)
const initData = getInitData()
if (initData) {
config.headers['x-telegram-init-data'] = initData
}
return config
})
export const verifyAuth = () => api.post('/mod-app/auth/verify')
export const fetchUsers = (params = {}) => export const fetchUsers = (params = {}) =>
api.get('/mod-app/users', { params }).then((res) => res.data) api.get('/mod-app/users', { params }).then((res) => res.data)

View File

@ -35,7 +35,8 @@
"express-mongo-sanitize": "^2.2.0", "express-mongo-sanitize": "^2.2.0",
"xss-clean": "^0.1.4", "xss-clean": "^0.1.4",
"hpp": "^0.2.3", "hpp": "^0.2.3",
"validator": "^13.11.0" "validator": "^13.11.0",
"cookie-parser": "^1.4.6"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.0.1", "nodemon": "^3.0.1",