Update files
This commit is contained in:
parent
73c91b4ec5
commit
ec23fdbf94
|
|
@ -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}` });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue