Update files

This commit is contained in:
glpshchn 2025-12-09 03:29:13 +03:00
parent e446691b3d
commit d6ed268c4a
6 changed files with 143 additions and 89 deletions

View File

@ -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: '/'
});

View File

@ -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

View File

@ -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);

View File

@ -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';
};

View File

@ -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';
};

View File

@ -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";
}
}