Update files
This commit is contained in:
parent
d2361b0e10
commit
05b808ad8d
|
|
@ -1,16 +1,7 @@
|
||||||
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 { validateAndParseInitData } = require('../utils/telegram');
|
||||||
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'];
|
||||||
|
|
@ -56,72 +47,68 @@ const ensureUserSettings = async (user) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Middleware для проверки авторизации
|
|
||||||
const authenticate = async (req, res, next) => {
|
const authenticate = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const accessToken = req.cookies[ACCESS_COOKIE];
|
const authHeader = req.headers.authorization || '';
|
||||||
const refreshToken = req.cookies[REFRESH_COOKIE];
|
|
||||||
|
|
||||||
let tokenPayload = null;
|
if (!authHeader.startsWith('tma ')) {
|
||||||
|
|
||||||
if (accessToken) {
|
|
||||||
try {
|
|
||||||
tokenPayload = verifyAccessToken(accessToken);
|
|
||||||
} 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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tokenPayload && refreshToken) {
|
|
||||||
try {
|
|
||||||
const refreshPayload = verifyRefreshToken(refreshToken);
|
|
||||||
const userForRefresh = await User.findById(refreshPayload.userId);
|
|
||||||
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tokenPayload) {
|
|
||||||
logSecurityEvent('AUTH_TOKEN_MISSING', req);
|
logSecurityEvent('AUTH_TOKEN_MISSING', req);
|
||||||
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
|
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateTelegramId(tokenPayload.telegramId)) {
|
const initDataRaw = authHeader.slice(4).trim();
|
||||||
logSecurityEvent('INVALID_TELEGRAM_ID', req, { telegramId: tokenPayload.telegramId });
|
|
||||||
clearAuthCookies(res);
|
|
||||||
return res.status(401).json({ error: 'Неверный ID пользователя' });
|
|
||||||
}
|
|
||||||
|
|
||||||
let user = await User.findOne({ telegramId: tokenPayload.telegramId.toString() });
|
if (!initDataRaw) {
|
||||||
if (!user) {
|
logSecurityEvent('EMPTY_INITDATA', req);
|
||||||
clearAuthCookies(res);
|
|
||||||
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
|
return res.status(401).json({ error: OFFICIAL_CLIENT_MESSAGE });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let payload;
|
||||||
|
|
||||||
|
try {
|
||||||
|
payload = validateAndParseInitData(initDataRaw);
|
||||||
|
} catch (error) {
|
||||||
|
logSecurityEvent('INVALID_INITDATA', req, { reason: error.message });
|
||||||
|
return res.status(401).json({ error: `${error.message}. ${OFFICIAL_CLIENT_MESSAGE}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const telegramUser = payload.user;
|
||||||
|
|
||||||
|
if (!validateTelegramId(telegramUser.id)) {
|
||||||
|
logSecurityEvent('INVALID_TELEGRAM_ID', req, { telegramId: telegramUser.id });
|
||||||
|
return res.status(401).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) {
|
if (user.banned) {
|
||||||
clearAuthCookies(res);
|
|
||||||
return res.status(403).json({ error: 'Пользователь заблокирован' });
|
return res.status(403).json({ error: 'Пользователь заблокирован' });
|
||||||
}
|
}
|
||||||
|
|
||||||
await ensureUserSettings(user);
|
await ensureUserSettings(user);
|
||||||
await touchUserActivity(user);
|
await touchUserActivity(user);
|
||||||
|
|
||||||
req.user = user;
|
req.user = user;
|
||||||
req.telegramUser = { id: user.telegramId };
|
req.telegramUser = telegramUser;
|
||||||
next();
|
next();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Ошибка авторизации:', error);
|
console.error('❌ Ошибка авторизации:', error);
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,7 @@ 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 { authenticate, ensureUserSettings, touchUserActivity } = require('../middleware/auth');
|
||||||
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'];
|
||||||
|
|
||||||
|
|
@ -34,65 +32,7 @@ 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) => {
|
const respondWithUser = async (user, res) => {
|
||||||
try {
|
|
||||||
const authHeader = req.headers.authorization || '';
|
|
||||||
const headerInitData = authHeader.startsWith('tma ') ? authHeader.slice(4).trim() : null;
|
|
||||||
const bodyInitData = typeof req.body?.initData === 'string' ? req.body.initData : null;
|
|
||||||
|
|
||||||
const initDataRaw = headerInitData || bodyInitData;
|
|
||||||
|
|
||||||
if (!initDataRaw) {
|
|
||||||
return res.status(400).json({ error: 'initData обязателен' });
|
|
||||||
}
|
|
||||||
|
|
||||||
let payload;
|
|
||||||
|
|
||||||
try {
|
|
||||||
payload = validateAndParseInitData(initDataRaw);
|
|
||||||
} catch (error) {
|
|
||||||
logSecurityEvent('INVALID_INITDATA', req, { reason: error.message });
|
|
||||||
return res.status(401).json({ error: error.message });
|
|
||||||
}
|
|
||||||
|
|
||||||
const telegramUser = payload.user;
|
|
||||||
|
|
||||||
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([
|
const populatedUser = await user.populate([
|
||||||
{ path: 'followers', select: 'username firstName lastName photoUrl' },
|
{ path: 'followers', select: 'username firstName lastName photoUrl' },
|
||||||
{ path: 'following', select: 'username firstName lastName photoUrl' }
|
{ path: 'following', select: 'username firstName lastName photoUrl' }
|
||||||
|
|
@ -117,14 +57,20 @@ router.post('/signin', strictAuthLimiter, async (req, res) => {
|
||||||
banned: populatedUser.banned
|
banned: populatedUser.banned
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
router.post('/signin', strictAuthLimiter, authenticate, async (req, res) => {
|
||||||
|
try {
|
||||||
|
await ensureUserSettings(req.user);
|
||||||
|
await touchUserActivity(req.user);
|
||||||
|
return respondWithUser(req.user, res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка signin:', error);
|
console.error('Ошибка signin:', error);
|
||||||
res.status(500).json({ error: 'Ошибка авторизации' });
|
res.status(500).json({ error: 'Ошибка авторизации' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/logout', (req, res) => {
|
router.post('/logout', (_req, res) => {
|
||||||
clearAuthCookies(res);
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ 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') });
|
||||||
|
|
@ -56,7 +55,6 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -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 { signInWithTelegram, verifyAuth } from './utils/api'
|
import { 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'
|
||||||
|
|
@ -18,8 +18,8 @@ function AppContent() {
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const startParamProcessed = useRef(false) // Флаг для обработки startParam только один раз
|
const startParamProcessed = useRef(false)
|
||||||
const initAppCalled = useRef(false) // Флаг чтобы initApp вызывался только один раз
|
const initAppCalled = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initTheme()
|
initTheme()
|
||||||
|
|
@ -55,7 +55,7 @@ function AppContent() {
|
||||||
tg.ready?.()
|
tg.ready?.()
|
||||||
tg.expand?.()
|
tg.expand?.()
|
||||||
|
|
||||||
const userData = await signInWithTelegram(tg.initData)
|
const userData = await verifyAuth()
|
||||||
setUser(userData)
|
setUser(userData)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
|
|
@ -68,16 +68,7 @@ function AppContent() {
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Ошибка инициализации:', err)
|
console.error('Ошибка инициализации:', err)
|
||||||
|
|
||||||
try {
|
|
||||||
// Попытаться восстановить сессию по токенам
|
|
||||||
const userData = await verifyAuth()
|
|
||||||
setUser(userData)
|
|
||||||
setError(null)
|
|
||||||
} catch (verifyError) {
|
|
||||||
console.error('Не удалось восстановить сессию:', verifyError)
|
|
||||||
setError(err?.response?.data?.error || err.message || 'Ошибка авторизации')
|
setError(err?.response?.data?.error || err.message || 'Ошибка авторизации')
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
signInWithTelegram,
|
|
||||||
verifyAuth,
|
verifyAuth,
|
||||||
fetchUsers,
|
fetchUsers,
|
||||||
banUser,
|
banUser,
|
||||||
|
|
@ -130,13 +129,7 @@ export default function App() {
|
||||||
const app = await waitForInitData();
|
const app = await waitForInitData();
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
const initData = app.initData;
|
const userData = await verifyAuth();
|
||||||
|
|
||||||
if (!initData) {
|
|
||||||
throw new Error('Telegram не передал initData');
|
|
||||||
}
|
|
||||||
|
|
||||||
const userData = await signInWithTelegram(initData);
|
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
setUser(userData);
|
setUser(userData);
|
||||||
|
|
@ -144,22 +137,11 @@ export default function App() {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
console.error('Ошибка инициализации модератора:', err);
|
console.error('Ошибка инициализации модератора:', err);
|
||||||
try {
|
|
||||||
const userData = await verifyAuth();
|
|
||||||
if (!cancelled) {
|
|
||||||
setUser(userData);
|
|
||||||
setError(null);
|
|
||||||
}
|
|
||||||
} catch (verifyError) {
|
|
||||||
if (!cancelled) {
|
|
||||||
const message =
|
const message =
|
||||||
err?.response?.data?.error ||
|
err?.response?.data?.error ||
|
||||||
verifyError?.response?.data?.error ||
|
|
||||||
err?.message ||
|
err?.message ||
|
||||||
'Нет доступа. Убедитесь, что вы добавлены как администратор.';
|
'Нет доступа. Убедитесь, что вы добавлены как администратор.';
|
||||||
setError(message);
|
setError(message);
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,6 @@ api.interceptors.request.use((config) => {
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const signInWithTelegram = (initData) =>
|
|
||||||
api.post('/auth/signin', { initData }).then((res) => res.data.user)
|
|
||||||
|
|
||||||
export const verifyAuth = () => api.post('/mod-app/auth/verify').then((res) => res.data.user)
|
export const verifyAuth = () => api.post('/mod-app/auth/verify').then((res) => res.data.user)
|
||||||
|
|
||||||
export const fetchUsers = (params = {}) =>
|
export const fetchUsers = (params = {}) =>
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@
|
||||||
"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",
|
|
||||||
"@telegram-apps/init-data-node": "^1.0.0"
|
"@telegram-apps/init-data-node": "^1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue