From d6ed268c4a199b8e247e3b5578caeb2503f865fa Mon Sep 17 00:00:00 2001 From: glpshchn <464976@niuitmo.ru> Date: Tue, 9 Dec 2025 03:29:13 +0300 Subject: [PATCH] Update files --- backend/utils/tokens.js | 2 +- moderation/DEPLOY.md | 2 +- moderation/backend/server.js | 7 +- moderation/frontend/src/App.jsx | 167 +++++++++++++++++---------- moderation/frontend/src/utils/api.js | 4 +- nginx-moderation-production.conf | 50 +++++--- 6 files changed, 143 insertions(+), 89 deletions(-) diff --git a/backend/utils/tokens.js b/backend/utils/tokens.js index a5ef75c..4435165 100644 --- a/backend/utils/tokens.js +++ b/backend/utils/tokens.js @@ -28,7 +28,7 @@ const signAuthTokens = (user) => ({ const getCookieBaseOptions = () => ({ httpOnly: true, - secure: config.isProduction(), + secure: config.isProduction(), // HTTPS только в production sameSite: config.isProduction() ? 'lax' : 'lax', path: '/' }); diff --git a/moderation/DEPLOY.md b/moderation/DEPLOY.md index b10271c..147bcf2 100644 --- a/moderation/DEPLOY.md +++ b/moderation/DEPLOY.md @@ -58,7 +58,7 @@ EMAIL_FROM=noreply@nakama.guru # Модерация MODERATION_PORT=3001 MODERATION_CORS_ORIGIN=https://moderation.nkm.guru -VITE_MODERATION_API_URL=https://moderation.nkm.guru/api +VITE_MODERATION_API_URL=https://moderation.nkm.guru/api # ВАЖНО: обязательно HTTPS! # Email для кодов подтверждения админа OWNER_EMAIL=aaem9848@gmail.com diff --git a/moderation/backend/server.js b/moderation/backend/server.js index 1b03314..3f7fba3 100644 --- a/moderation/backend/server.js +++ b/moderation/backend/server.js @@ -29,10 +29,9 @@ const { generalLimiter } = require('../../backend/middleware/rateLimiter'); const app = express(); const server = http.createServer(app); -// Trust proxy для правильного IP -if (config.isProduction()) { - app.set('trust proxy', 1); -} +// Trust proxy для правильного IP и HTTPS +// В production доверяем прокси (nginx), чтобы правильно определять HTTPS +app.set('trust proxy', config.isProduction() ? 1 : false); // Security headers app.use(helmetConfig); diff --git a/moderation/frontend/src/App.jsx b/moderation/frontend/src/App.jsx index bf5cf43..1b5a143 100644 --- a/moderation/frontend/src/App.jsx +++ b/moderation/frontend/src/App.jsx @@ -120,6 +120,7 @@ export default function App() { const [chatInput, setChatInput] = useState(''); const chatSocketRef = useRef(null); const chatListRef = useRef(null); + const telegramWidgetRef = useRef(null); // Comments modal const [commentsModal, setCommentsModal] = useState(null); // { postId, comments: [] } @@ -139,65 +140,6 @@ export default function App() { useEffect(() => { let cancelled = false; - // Инициализация Telegram Login Widget для обычного браузера - const initTelegramWidget = () => { - // Глобальная функция для обработки авторизации через виджет - window.onTelegramAuth = async (userData) => { - console.log('Telegram Login Widget данные:', userData); - - try { - setAuthLoading(true); - - // Отправить данные виджета на сервер для создания сессии - const API_URL = getApiUrl(); - const response = await fetch(`${API_URL}/moderation-auth/telegram-widget`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', - body: JSON.stringify(userData) - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.error || 'Ошибка авторизации'); - } - - const result = await response.json(); - - if (result.accessToken) { - localStorage.setItem('moderation_jwt_token', result.accessToken); - } - - if (result?.user) { - setUser(result.user); - setError(null); - } - } catch (err) { - console.error('Ошибка авторизации через виджет:', err); - setError(err.message || 'Ошибка авторизации через Telegram'); - } finally { - setAuthLoading(false); - } - }; - - // Загрузить виджет скрипт если его нет и есть контейнер - if (!document.querySelector('script[src*="telegram-widget"]')) { - setTimeout(() => { - const widgetContainer = telegramWidgetRef.current; - if (!widgetContainer) return; - - const script = document.createElement('script'); - script.async = true; - script.src = 'https://telegram.org/js/telegram-widget.js?22'; - script.setAttribute('data-telegram-login', 'NakamaSpaceBot'); - script.setAttribute('data-size', 'large'); - script.setAttribute('data-request-access', 'write'); - script.setAttribute('data-onauth', 'onTelegramAuth'); - - widgetContainer.appendChild(script); - }, 100); - } - }; const init = async () => { try { @@ -221,10 +163,7 @@ export default function App() { } } - // Инициализировать виджет для обычного браузера - if (!telegramApp?.initData) { - initTelegramWidget(); - } + // Инициализация виджета будет в отдельном useEffect после монтирования компонента // Проверить JWT токен const jwtToken = localStorage.getItem('moderation_jwt_token'); @@ -266,6 +205,93 @@ export default function App() { }; }, []); + // Отдельный useEffect для инициализации виджета после монтирования + useEffect(() => { + const telegramApp = window.Telegram?.WebApp; + const showLoginForm = error === 'login_required' || (!user && !loading); + + // Инициализировать виджет только если нет WebApp initData и показана форма входа + if (!telegramApp?.initData && showLoginForm) { + // Глобальная функция для обработки авторизации через виджет + window.onTelegramAuth = async (userData) => { + console.log('Telegram Login Widget данные:', userData); + + try { + setAuthLoading(true); + + // Отправить данные виджета на сервер для создания сессии + const API_URL = getApiUrl(); + // В production используем относительный путь (HTTPS через nginx) + const fullApiUrl = API_URL.startsWith('http') ? API_URL : `${window.location.origin}${API_URL}`; + const response = await fetch(`${fullApiUrl}/moderation-auth/telegram-widget`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(userData) + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error || 'Ошибка авторизации'); + } + + const result = await response.json(); + + if (result.accessToken) { + localStorage.setItem('moderation_jwt_token', result.accessToken); + } + + if (result?.user) { + setUser(result.user); + setError(null); + } + } catch (err) { + console.error('Ошибка авторизации через виджет:', err); + setError(err.message || 'Ошибка авторизации через Telegram'); + } finally { + setAuthLoading(false); + } + }; + + const initWidget = () => { + // Проверить не загружен ли уже виджет + if (document.querySelector('script[src*="telegram-widget"]')) { + return; + } + + const widgetContainer = telegramWidgetRef.current; + if (!widgetContainer) { + return; + } + + const script = document.createElement('script'); + script.async = true; + script.src = 'https://telegram.org/js/telegram-widget.js?22'; + script.setAttribute('data-telegram-login', 'NakamaSpaceBot'); + script.setAttribute('data-size', 'large'); + script.setAttribute('data-request-access', 'write'); + script.setAttribute('data-onauth', 'onTelegramAuth'); + + widgetContainer.appendChild(script); + }; + + // Подождать немного чтобы контейнер был готов + const timer = setTimeout(initWidget, 500); + return () => { + clearTimeout(timer); + if (window.onTelegramAuth) { + delete window.onTelegramAuth; + } + }; + } + + return () => { + if (window.onTelegramAuth) { + delete window.onTelegramAuth; + } + }; + }, [error, user, loading]); + useEffect(() => { if (tab === 'users') { loadUsers(); @@ -403,16 +429,27 @@ export default function App() { if (import.meta.env.VITE_API_URL) { return import.meta.env.VITE_API_URL; } + // В production используем относительный путь (HTTPS) if (import.meta.env.PROD) { return '/api'; } + // В development используем localhost (только для dev) return 'http://localhost:3001/api'; }; const API_URL = getApiUrl(); // Для WebSocket убираем "/api" из base URL, т.к. socket.io слушает на корне - const socketBase = API_URL.replace(/\/?api\/?$/, ''); + // В production используем текущий origin через wss:// (HTTPS) + let socketBase = API_URL.replace(/\/?api\/?$/, ''); + if (!socketBase || socketBase === '/api') { + // В production используем текущий origin (HTTPS) + socketBase = window.location.origin; + } + // Заменить http:// на https:// для безопасности (если не localhost) + if (socketBase.startsWith('http://') && !socketBase.includes('localhost')) { + socketBase = socketBase.replace('http://', 'https://'); + } console.log('[Chat] Инициализация чата'); console.log('[Chat] WS base URL:', socketBase); @@ -910,9 +947,11 @@ export default function App() { if (import.meta.env.VITE_API_URL) { return import.meta.env.VITE_API_URL.replace('/api', ''); } + // В production используем текущий origin (HTTPS через nginx) if (import.meta.env.PROD) { return window.location.origin; } + // Только для development return 'http://localhost:3000'; }; diff --git a/moderation/frontend/src/utils/api.js b/moderation/frontend/src/utils/api.js index 449f199..726d49c 100644 --- a/moderation/frontend/src/utils/api.js +++ b/moderation/frontend/src/utils/api.js @@ -6,11 +6,11 @@ export const getApiUrl = () => { if (import.meta.env.VITE_API_URL) { return import.meta.env.VITE_API_URL; } - // В production используем относительный путь + // В production используем относительный путь (HTTPS через nginx) if (import.meta.env.PROD) { return '/api'; } - // В development используем порт модерации + // В development используем порт модерации (только для dev) return 'http://localhost:3001/api'; }; diff --git a/nginx-moderation-production.conf b/nginx-moderation-production.conf index 786f32c..875d065 100644 --- a/nginx-moderation-production.conf +++ b/nginx-moderation-production.conf @@ -36,6 +36,7 @@ server { add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "no-referrer-when-downgrade" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # Логи access_log /var/log/nginx/moderation-access.log; @@ -44,6 +45,10 @@ server { # Максимальный размер загружаемых файлов client_max_body_size 20M; + # Корневая директория фронтенда + root /var/www/nakama/moderation/frontend/dist; + index index.html; + # Gzip compression gzip on; gzip_vary on; @@ -56,15 +61,17 @@ server { application/vnd.ms-fontobject image/svg+xml; # Проксирование API запросов к бэкенду модерации + # ИЗМЕНИТЕ localhost:3001 на ваш реальный адрес бэкенда location /api { - proxy_pass http://nakama-moderation-backend:3001; + proxy_pass http://127.0.0.1:3001; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto https; # Всегда HTTPS proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port 443; # WebSocket поддержка proxy_set_header Upgrade $http_upgrade; @@ -74,11 +81,14 @@ server { proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; + + # Отключить буферизацию для реального времени + proxy_buffering off; } # WebSocket для чата модераторов location /mod-chat { - proxy_pass http://nakama-moderation-backend:3001; + proxy_pass http://127.0.0.1:3001; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; @@ -86,27 +96,33 @@ server { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto https; # Всегда HTTPS + proxy_set_header X-Forwarded-Port 443; # Таймауты для WebSocket proxy_connect_timeout 7d; proxy_send_timeout 7d; proxy_read_timeout 7d; + + proxy_buffering off; } - # Статические файлы фронтенда (из Docker контейнера) + # Статические файлы фронтенда location / { - proxy_pass http://nakama-moderation-frontend:80; - proxy_http_version 1.1; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # Кэширование статических файлов - proxy_cache_valid 200 1y; - proxy_cache_valid 404 1h; + try_files $uri $uri/ /index.html; + } + + # Кэширование статических файлов + location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot|map)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # Без кэша для index.html (для обновлений) + location = /index.html { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; } } -