From ec23fdbf945600cc6fa50380368c344e77e501d0 Mon Sep 17 00:00:00 2001 From: glpshchn <464976@niuitmo.ru> Date: Tue, 11 Nov 2025 00:22:58 +0300 Subject: [PATCH] Update files --- backend/middleware/auth.js | 188 ++++++++++----------------- moderation/frontend/src/App.jsx | 53 +++++--- moderation/frontend/src/utils/api.js | 51 ++++---- 3 files changed, 125 insertions(+), 167 deletions(-) diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index b69342e..d31cb46 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -6,6 +6,7 @@ const config = require('../config'); const OFFICIAL_CLIENT_MESSAGE = 'Используйте официальный клиент. Сообщите об ошибке в https://t.me/NakamaReportbot'; const ALLOWED_SEARCH_PREFERENCES = ['furry', 'anime']; +const MAX_AUTH_AGE_SECONDS = 5 * 60; // 5 минут const touchUserActivity = async (user) => { if (!user) return; @@ -48,150 +49,99 @@ const ensureUserSettings = async (user) => { } }; -// Проверка Telegram Init Data function validateTelegramWebAppData(initData, botToken) { - const urlParams = new URLSearchParams(initData); - const hash = urlParams.get('hash'); - urlParams.delete('hash'); - - const dataCheckString = Array.from(urlParams.entries()) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([key, value]) => `${key}=${value}`) - .join('\n'); - - const secretKey = crypto - .createHmac('sha256', 'WebAppData') - .update(botToken) - .digest(); - - const calculatedHash = crypto - .createHmac('sha256', secretKey) - .update(dataCheckString) - .digest('hex'); - - return calculatedHash === hash; + 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 для проверки авторизации 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: OFFICIAL_CLIENT_MESSAGE }); - } - - 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 заголовка'); + logSecurityEvent('MISSING_INITDATA', req); return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE }); } - - // Получаем user из initData - let urlParams; - try { - urlParams = new URLSearchParams(initData); - } catch (e) { - // Если initData не URLSearchParams, попробуем как JSON - try { - const parsed = JSON.parse(initData); - if (parsed.user) { - req.telegramUser = parsed.user; - // Найти или создать пользователя - let user = await User.findOne({ telegramId: parsed.user.id.toString() }); - if (!user) { - user = new User({ - telegramId: parsed.user.id.toString(), - username: parsed.user.username || parsed.user.first_name, - firstName: parsed.user.first_name, - lastName: parsed.user.last_name, - photoUrl: parsed.user.photo_url - }); - await user.save(); - console.log(`✅ Создан новый пользователь: ${user.username}`); - } else { - user.username = parsed.user.username || parsed.user.first_name; - user.firstName = parsed.user.first_name; - user.lastName = parsed.user.last_name; - if (parsed.user.photo_url) { - user.photoUrl = parsed.user.photo_url; - } - await user.save(); - } - await ensureUserSettings(user); - await touchUserActivity(user); - req.user = user; - return next(); - } - return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE }); - } catch (e2) { - console.error('❌ Ошибка парсинга initData:', e2.message); - return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE }); + + if (!config.telegramBotToken) { + logSecurityEvent('MISSING_BOT_TOKEN', req); + if (config.isProduction()) { + return res.status(500).json({ error: 'Сервер некорректно настроен для авторизации через Telegram' }); } + console.warn('⚠️ TELEGRAM_BOT_TOKEN не установлен, пропускаем проверку подписи (dev режим)'); } - - const userParam = urlParams.get('user'); - + + let params; + try { + if (config.telegramBotToken) { + params = validateTelegramWebAppData(initData, config.telegramBotToken); + } else { + params = new URLSearchParams(initData); + } + } 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 (!userParam) { - console.warn('⚠️ Нет user параметра в initData'); + logSecurityEvent('MISSING_INITDATA_USER', req); return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE }); } - + let telegramUser; try { telegramUser = JSON.parse(userParam); - } catch (e) { - console.error('❌ Ошибка парсинга user:', e.message); + } catch (parseError) { + logSecurityEvent('INVALID_INITDATA_USER_JSON', req, { error: parseError.message }); return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE }); } - + req.telegramUser = telegramUser; - - // Валидация Telegram ID + if (!validateTelegramId(telegramUser.id)) { logSecurityEvent('INVALID_TELEGRAM_ID', req, { telegramId: telegramUser.id }); return res.status(401).json({ error: 'Неверный ID пользователя' }); } - // Проверка подписи Telegram (строгая проверка в production) - if (config.telegramBotToken) { - const isValid = validateTelegramWebAppData(initData, config.telegramBotToken); - - if (!isValid) { - logSecurityEvent('INVALID_TELEGRAM_SIGNATURE', req, { - telegramId: telegramUser.id, - hasToken: !!config.telegramBotToken - }); - - // В production строгая проверка - if (config.isProduction()) { - return res.status(401).json({ error: 'Неверные данные авторизации' }); - } - } - } else if (config.isProduction()) { - logSecurityEvent('MISSING_BOT_TOKEN', req); - console.warn('⚠️ TELEGRAM_BOT_TOKEN не установлен, проверка подписи пропущена'); - } - // Найти или создать пользователя let user = await User.findOne({ telegramId: telegramUser.id.toString() }); if (!user) { @@ -220,7 +170,7 @@ const authenticate = async (req, res, next) => { next(); } catch (error) { console.error('❌ Ошибка авторизации:', error); - res.status(401).json({ error: 'Ошибка авторизации' }); + res.status(401).json({ error: `Ошибка авторизации. ${OFFICIAL_CLIENT_MESSAGE}` }); } }; diff --git a/moderation/frontend/src/App.jsx b/moderation/frontend/src/App.jsx index ac2127f..58b8b90 100644 --- a/moderation/frontend/src/App.jsx +++ b/moderation/frontend/src/App.jsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { verifyAuth, fetchUsers, @@ -97,40 +97,51 @@ export default function App() { const chatSocketRef = useRef(null); const chatListRef = useRef(null); - const isTelegram = typeof window !== 'undefined' && window.Telegram?.WebApp; + const telegramApp = typeof window !== 'undefined' ? window.Telegram?.WebApp : null; useEffect(() => { - if (isTelegram) { - window.Telegram.WebApp.disableVerticalSwipes(); - window.Telegram.WebApp.ready(); - window.Telegram.WebApp.expand(); - } - const init = async () => { try { - const response = await verifyAuth(); - setUser(response.data.user); - if (isTelegram) { - window.Telegram.WebApp.MainButton.setText('Закрыть'); - window.Telegram.WebApp.MainButton.show(); - window.Telegram.WebApp.MainButton.onClick(() => window.Telegram.WebApp.close()); + if (telegramApp) { + telegramApp.disableVerticalSwipes(); + telegramApp.ready(); + telegramApp.expand(); } - setLoading(false); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + const initData = telegramApp?.initData || ''; + + if (initData) { + const response = await verifyAuth(); + setUser(response.data.user); + setError(null); + setLoading(false); + return; + } + + setError('Откройте модераторский интерфейс из Telegram (бот @rbachbot).'); } catch (err) { - console.error(err); - setError('Нет доступа. Убедитесь, что вы добавлены как администратор.'); + console.error('Ошибка инициализации модератора:', err); + const serverMessage = err?.response?.data?.error; + setError(serverMessage || 'Нет доступа. Убедитесь, что вы добавлены как администратор.'); + } finally { setLoading(false); } + + if (telegramApp) { + telegramApp.MainButton.setText('Закрыть'); + telegramApp.MainButton.show(); + telegramApp.MainButton.onClick(() => telegramApp.close()); + } }; init(); return () => { - if (isTelegram) { - window.Telegram.WebApp.MainButton.hide(); - } + telegramApp?.MainButton?.hide(); }; - }, [isTelegram]); + }, [telegramApp]); useEffect(() => { if (tab === 'users') { diff --git a/moderation/frontend/src/utils/api.js b/moderation/frontend/src/utils/api.js index 899b896..402e2fd 100644 --- a/moderation/frontend/src/utils/api.js +++ b/moderation/frontend/src/utils/api.js @@ -1,64 +1,61 @@ -import axios from 'axios'; +import axios from 'axios' const API_URL = import.meta.env.VITE_API_URL || - (import.meta.env.PROD ? '/api' : 'http://localhost:3000/api'); + (import.meta.env.PROD ? '/api' : 'http://localhost:3000/api') const api = axios.create({ baseURL: API_URL, withCredentials: false -}); +}) -const getInitData = () => { - if (typeof window === 'undefined') { - return null; - } - return window.Telegram?.WebApp?.initData || null; -}; +const getInitData = () => window.Telegram?.WebApp?.initData || null api.interceptors.request.use((config) => { - const initData = getInitData(); - if (initData) { - config.headers['x-telegram-init-data'] = initData; - } - return config; -}); + const initData = getInitData() -export const verifyAuth = () => api.post('/mod-app/auth/verify'); + if (initData) { + config.headers['x-telegram-init-data'] = initData + } + + return config +}) + +export const verifyAuth = () => api.post('/mod-app/auth/verify') export const fetchUsers = (params = {}) => - api.get('/mod-app/users', { params }).then((res) => res.data); + api.get('/mod-app/users', { params }).then((res) => res.data) export const banUser = (userId, data) => - api.put(`/mod-app/users/${userId}/ban`, data).then((res) => res.data); + api.put(`/mod-app/users/${userId}/ban`, data).then((res) => res.data) export const fetchPosts = (params = {}) => - api.get('/mod-app/posts', { params }).then((res) => res.data); + api.get('/mod-app/posts', { params }).then((res) => res.data) export const updatePost = (postId, data) => - api.put(`/mod-app/posts/${postId}`, data).then((res) => res.data); + api.put(`/mod-app/posts/${postId}`, data).then((res) => res.data) export const deletePost = (postId) => - api.delete(`/mod-app/posts/${postId}`).then((res) => res.data); + api.delete(`/mod-app/posts/${postId}`).then((res) => res.data) export const removePostImage = (postId, index) => - api.delete(`/mod-app/posts/${postId}/images/${index}`).then((res) => res.data); + api.delete(`/mod-app/posts/${postId}/images/${index}`).then((res) => res.data) export const banPostAuthor = (postId, data) => - api.post(`/mod-app/posts/${postId}/ban`, data).then((res) => res.data); + api.post(`/mod-app/posts/${postId}/ban`, data).then((res) => res.data) export const fetchReports = (params = {}) => - api.get('/mod-app/reports', { params }).then((res) => res.data); + api.get('/mod-app/reports', { params }).then((res) => res.data) export const updateReportStatus = (reportId, data) => - api.put(`/mod-app/reports/${reportId}`, data).then((res) => res.data); + api.put(`/mod-app/reports/${reportId}`, data).then((res) => res.data) export const publishToChannel = (formData) => api.post('/mod-app/channel/publish', formData, { headers: { 'Content-Type': 'multipart/form-data' } - }); + }) -export default api; +export default api