- Клиентская часть Vue 3 + Vite - Серверная часть Node.js + WebSocket - Система авторизации и смен - Управление игровыми портами - Поддержка тем (светлая/темная) - Адаптивный дизайн 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
541 lines
26 KiB
JavaScript
541 lines
26 KiB
JavaScript
"use strict";
|
||
|
||
global.MY_date=require('./www/js/date.js'); // ЗАГРУЗКА
|
||
|
||
const api = require('./api.js');
|
||
const game = require('./game.js');
|
||
const ws = require('./ws.js');
|
||
const fs = require('node:fs/promises');
|
||
const path = require('path');
|
||
|
||
const http = require('http');
|
||
|
||
// Запуск команд
|
||
const util = require('util');
|
||
const runCommand = util.promisify(require('child_process').exec);
|
||
|
||
let ws_cfg={
|
||
zlibDeflateOptions: {chunkSize: 1024, memLevel: 9,level: 7},
|
||
zlibInflateOptions: {chunkSize: 10 * 1024 },
|
||
clientNoContextTakeover: true, // Defaults to negotiated value.
|
||
serverNoContextTakeover: true, // Defaults to negotiated value.
|
||
serverMaxWindowBits: 12, // Defaults to negotiated value.
|
||
concurrencyLimit: 16, // Limits zlib concurrency for perf.
|
||
threshold: 128 // Size (in bytes) below which messages
|
||
}
|
||
const WebSocket = require('ws');
|
||
|
||
// Добавляем обработку ошибок для WebSocket сервера
|
||
try {
|
||
global.wsServer = new WebSocket.Server({
|
||
port: process.env.PORT || 9000,
|
||
perMessageDeflate: ws_cfg,
|
||
host: '0.0.0.0' // Слушаем на всех интерфейсах
|
||
});
|
||
|
||
// Обработка ошибок сервера
|
||
wsServer.on('error', (error) => {
|
||
console.error('[ERROR] WebSocket сервер на порту 9000:', error);
|
||
if (error.code === 'EADDRINUSE') {
|
||
console.error('[ERROR] ❌ Порт 9000 уже занят! Возможные причины:');
|
||
console.error(' 1. Другой экземпляр сервера уже запущен');
|
||
console.error(' 2. Новый сервер (index_old.ts) также запущен');
|
||
console.error(' 3. Процесс не был корректно остановлен');
|
||
console.error('[РЕШЕНИЕ] 💡 Выполните команды:');
|
||
console.error(' - pkill -f "node.*index" (остановить все серверы)');
|
||
console.error(' - lsof -i :9000 (проверить кто использует порт)');
|
||
console.error(' - kill -9 <PID> (принудительно остановить процесс)');
|
||
|
||
// Не завершаем процесс сразу, даем время на чтение сообщения
|
||
setTimeout(() => process.exit(1), 3000);
|
||
}
|
||
});
|
||
|
||
console.log('[OK] WebSocket сервер запущен на порту 9000');
|
||
} catch (error) {
|
||
console.error('[FATAL] Не удалось создать WebSocket сервер:', error);
|
||
process.exit(1);
|
||
}
|
||
|
||
// Убираем локальный сервер авторизации - используем только внешний
|
||
|
||
async function main() {
|
||
// Эмуляция serialcpu для Windows
|
||
try {
|
||
global.serialcpu = (await runCommand("cat /proc/cpuinfo | grep Serial")).stdout.split(": ")[1];
|
||
if (serialcpu.length>5) serialcpu=serialcpu.substring(0, serialcpu.length - 2);
|
||
} catch (error) {
|
||
// На Windows используем mock значение
|
||
global.serialcpu = "mock-serial-cpu-12345";
|
||
console.log("[MOCK] Используем эмуляцию serialcpu для Windows");
|
||
}
|
||
console.log("serialcpu========", global.serialcpu)
|
||
|
||
// ОПРЕДЕЛЕНИЕ ORANGE PI
|
||
global.isOrangePi = false;
|
||
global.staticDirectory = '../dist/client'; // По умолчанию обычная сборка
|
||
|
||
try {
|
||
// Проверяем архитектуру процессора
|
||
const cpuInfo = await runCommand("uname -m");
|
||
const isARM = cpuInfo.stdout.includes('arm');
|
||
|
||
// Проверяем модель устройства
|
||
const deviceInfo = await runCommand("cat /proc/device-tree/model 2>/dev/null || echo 'unknown'");
|
||
const isOrange = deviceInfo.stdout.toLowerCase().includes('orange');
|
||
|
||
// Проверяем переменную окружения
|
||
const envTarget = process.env.TARGET === 'orange-pi';
|
||
|
||
global.isOrangePi = isARM || isOrange || envTarget;
|
||
|
||
if (global.isOrangePi) {
|
||
global.staticDirectory = '../dist/client-orange-pi';
|
||
console.log("🍊 [ORANGE PI] Определена Orange PI плата - использую оптимизированную сборку");
|
||
console.log("📁 [ORANGE PI] Статические файлы: dist/client-orange-pi");
|
||
} else {
|
||
console.log("💻 [DESKTOP] Обычное устройство - использую стандартную сборку");
|
||
console.log("📁 [DESKTOP] Статические файлы: dist/client");
|
||
}
|
||
} catch (error) {
|
||
console.log("⚠️ [WARNING] Не удалось определить тип устройства, использую стандартную сборку");
|
||
}
|
||
|
||
// Инициализация подключения к серверу
|
||
global.conn_to_server = false;
|
||
|
||
// ВЕРСИЯ
|
||
global.VER={ prg:0, git:null, commit:null, name:null };
|
||
//
|
||
|
||
try {
|
||
let dan= JSON.parse(await fs.readFile('package.json', "utf8"));
|
||
VER.name=dan.name; VER.prg=dan.version;
|
||
} catch (error) {}
|
||
runCommand("git symbolic-ref --short HEAD").then((d)=>{
|
||
VER.git=d.stdout.slice(0, -1);
|
||
runCommand("git log --oneline | wc -l").then((d)=>{VER.commit=d.stdout.slice(0, -1)}).catch(()=>{});
|
||
}).catch(()=>{});
|
||
|
||
|
||
const ws_toserver = require('./ws-toserver.js');
|
||
|
||
// Добавляем обработку ошибок для HTTP сервера
|
||
const httpServer = http.createServer( (req, res)=> {
|
||
let jsonString = '', origin=req.headers.origin;
|
||
|
||
// Улучшенная поддержка CORS для development, production и локальной сети
|
||
if (origin) {
|
||
// Разрешаем доступ с любых IP в локальной сети
|
||
const allowedOrigins = [
|
||
"https://code.tir.moygig.ru",
|
||
"http://localhost:5173",
|
||
"http://localhost:3000",
|
||
"http://localhost:5000",
|
||
"http://127.0.0.1:5173",
|
||
"http://127.0.0.1:5000"
|
||
];
|
||
|
||
// Проверяем, что origin содержит localhost или IP адрес
|
||
const isLocalNetwork = origin.includes('localhost') ||
|
||
origin.includes('127.0.0.1') ||
|
||
origin.includes('192.168.') ||
|
||
origin.includes('10.0.') ||
|
||
origin.includes('172.16.') ||
|
||
origin.includes('172.17.') ||
|
||
origin.includes('172.18.') ||
|
||
origin.includes('172.19.') ||
|
||
origin.includes('172.2') ||
|
||
origin.includes('172.3');
|
||
|
||
if (allowedOrigins.includes(origin) || isLocalNetwork) {
|
||
res.setHeader('Access-Control-Allow-Origin', origin);
|
||
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
||
console.log(`[CORS] ✅ Разрешен доступ с: ${origin}`);
|
||
} else {
|
||
console.log(`[CORS] ⚠️ Заблокирован доступ с: ${origin}`);
|
||
}
|
||
}
|
||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||
|
||
// Обработка OPTIONS запросов
|
||
if (req.method === 'OPTIONS') {
|
||
res.writeHead(200);
|
||
res.end();
|
||
return;
|
||
}
|
||
|
||
req.on('data', (data) => jsonString += data ); // Пришла информация - записали.
|
||
req.on('end', () => {
|
||
// Обработка /api/task-file - проксирование файлов с внешнего сервера
|
||
if (req.url.startsWith('/api/task-file/') && req.method === 'GET') {
|
||
const fileId = req.url.substring('/api/task-file/'.length);
|
||
console.log('[API] Запрос файла с ID:', fileId);
|
||
|
||
// Проксируем запрос к внешнему серверу через HTTP
|
||
const http = require('http');
|
||
const externalUrl = `http://ws.tirpriz.ru/api/task-file/${fileId}`;
|
||
|
||
console.log('[API] Проксируем запрос к:', externalUrl);
|
||
|
||
http.get(externalUrl, (externalRes) => {
|
||
console.log('[API] Ответ от внешнего сервера:', externalRes.statusCode);
|
||
|
||
if (externalRes.statusCode === 200) {
|
||
// Передаем заголовки от внешнего сервера
|
||
res.writeHead(200, {
|
||
'Content-Type': externalRes.headers['content-type'] || 'application/octet-stream',
|
||
'Content-Length': externalRes.headers['content-length'],
|
||
'Cache-Control': 'public, max-age=3600'
|
||
});
|
||
// Проксируем данные
|
||
externalRes.pipe(res);
|
||
console.log('[API] ✅ Файл передан с внешнего сервера, ID:', fileId);
|
||
} else if (externalRes.statusCode === 302 || externalRes.statusCode === 301) {
|
||
// Обрабатываем редирект
|
||
const redirectUrl = externalRes.headers.location;
|
||
console.log('[API] Перенаправление на:', redirectUrl);
|
||
|
||
// Отправляем редирект клиенту
|
||
res.writeHead(302, { 'Location': redirectUrl });
|
||
res.end();
|
||
} else {
|
||
// Файл не найден
|
||
console.log('[API] Файл не найден, статус:', externalRes.statusCode);
|
||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
error: 'File not found',
|
||
fileId: fileId,
|
||
status: externalRes.statusCode
|
||
}));
|
||
}
|
||
}).on('error', (err) => {
|
||
console.error('[API] Ошибка при запросе к внешнему серверу:', err);
|
||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
error: 'External server error',
|
||
message: String(err)
|
||
}));
|
||
});
|
||
return;
|
||
}
|
||
|
||
// Обработка /api/files - эндпоинт для локальных файлов задач
|
||
if (req.url.startsWith('/api/files/') && req.method === 'GET') {
|
||
(async () => {
|
||
try {
|
||
const fileName = decodeURIComponent(req.url.substring('/api/files/'.length));
|
||
console.log('[API] Запрос файла:', fileName);
|
||
|
||
// Здесь должна быть реальная логика получения файла с внешнего сервера
|
||
// Пока используем заглушку или локальные файлы
|
||
const filePath = path.join(__dirname, 'uploads', fileName);
|
||
|
||
// Проверяем существование файла
|
||
try {
|
||
await fs.access(filePath);
|
||
const fileContent = await fs.readFile(filePath);
|
||
|
||
// Определяем MIME тип по расширению
|
||
const ext = path.extname(fileName).toLowerCase();
|
||
const mimeTypes = {
|
||
'.jpg': 'image/jpeg',
|
||
'.jpeg': 'image/jpeg',
|
||
'.png': 'image/png',
|
||
'.gif': 'image/gif',
|
||
'.webp': 'image/webp',
|
||
'.svg': 'image/svg+xml',
|
||
'.pdf': 'application/pdf',
|
||
'.doc': 'application/msword',
|
||
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||
};
|
||
|
||
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
||
|
||
res.writeHead(200, {
|
||
'Content-Type': contentType,
|
||
'Content-Disposition': `inline; filename="${fileName}"`
|
||
});
|
||
res.end(fileContent);
|
||
console.log('[API] ✅ Файл отправлен:', fileName);
|
||
} catch (fileError) {
|
||
// Если файл не найден локально, пробуем запросить с внешнего сервера
|
||
console.log('[API] Файл не найден локально, проксируем запрос к внешнему серверу...');
|
||
|
||
// Проксирование запроса к внешнему серверу
|
||
const https = require('https');
|
||
const externalUrl = `https://ws.tirpriz.ru/api/files/${fileName}`;
|
||
|
||
https.get(externalUrl, (externalRes) => {
|
||
if (externalRes.statusCode === 200) {
|
||
// Передаем заголовки от внешнего сервера
|
||
res.writeHead(200, {
|
||
'Content-Type': externalRes.headers['content-type'] || 'application/octet-stream',
|
||
'Content-Length': externalRes.headers['content-length'],
|
||
'Cache-Control': 'public, max-age=3600'
|
||
});
|
||
// Проксируем данные
|
||
externalRes.pipe(res);
|
||
console.log('[API] ✅ Файл получен с внешнего сервера:', fileName);
|
||
} else {
|
||
// Файл не найден и на внешнем сервере
|
||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
error: 'File not found',
|
||
fileName: fileName,
|
||
message: 'Файл не найден ни локально, ни на внешнем сервере'
|
||
}));
|
||
}
|
||
}).on('error', (err) => {
|
||
console.error('[API] Ошибка при запросе к внешнему серверу:', err);
|
||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
error: 'External server error',
|
||
message: String(err)
|
||
}));
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('[API] ❌ Ошибка при получении файла:', error);
|
||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
error: 'Failed to load file',
|
||
message: String(error)
|
||
}));
|
||
}
|
||
})();
|
||
return;
|
||
}
|
||
|
||
// Обработка /api/game-info
|
||
if (req.url === '/api/game-info' && req.method === 'GET') {
|
||
try {
|
||
const gameInfo = game.getInfo();
|
||
console.log('[API] Запрос информации об играх');
|
||
console.log('[API] Количество игр:', Object.keys(gameInfo.games || {}).length);
|
||
console.log('[API] Количество групп:', Object.keys(gameInfo.groups || {}).length);
|
||
|
||
if (!gameInfo.games || Object.keys(gameInfo.games).length === 0) {
|
||
console.warn('[API] ⚠️ Нет доступных игр в конфигурации!');
|
||
}
|
||
|
||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify(gameInfo));
|
||
console.log('[API] ✅ Отправлена информация об играх');
|
||
} catch (error) {
|
||
console.error('[API] ❌ Ошибка получения информации об играх:', error);
|
||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
error: 'Failed to load game info',
|
||
message: String(error),
|
||
stack: error.stack
|
||
}));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Если это API запрос или запрос к старому серверу
|
||
if (req.url.startsWith('/api') || req.url.startsWith('/www')) {
|
||
api.go(req, res, jsonString);
|
||
} else if (req.url === '/test.html') {
|
||
// Отдаем тестовую страницу
|
||
serveTestPage(req, res);
|
||
} else {
|
||
// Для всех остальных запросов пытаемся раздать React билд
|
||
serveReactApp(req, res);
|
||
}
|
||
});
|
||
req.on('error', (err) => console.log("ERROR HTTP ",err) );
|
||
|
||
});
|
||
|
||
// Обработка ошибок HTTP сервера
|
||
httpServer.on('error', (error) => {
|
||
console.error(`[ERROR] HTTP сервер на порту ${HTTP_PORT || 'неопределен'}:`, error);
|
||
if (error.code === 'EADDRINUSE') {
|
||
console.error('[ERROR] Порт 80 уже занят! Возможно, другой веб-сервер запущен.');
|
||
} else if (error.code === 'EACCES') {
|
||
console.error('[ERROR] Нет прав для прослушивания порта 80. Запустите с sudo или используйте другой порт.');
|
||
}
|
||
});
|
||
|
||
// Используем порт 80 для production, 5000 для разработки
|
||
const HTTP_PORT = process.env.HTTP_PORT || (process.env.NODE_ENV === 'production' ? 80 : 5000);
|
||
httpServer.listen(HTTP_PORT, '0.0.0.0', () => {
|
||
console.log(`[OK] HTTP сервер запущен на порту ${HTTP_PORT}`);
|
||
console.log(`[INFO] Доступ с других устройств: http://[IP-адрес-ПК]:${HTTP_PORT}`);
|
||
});
|
||
|
||
const onConnect= async (wsClient)=> {
|
||
let verSoft = JSON.parse( await fs.readFile(path.join(__dirname, 'data/verSoft.ini') ) );
|
||
|
||
const clientIP = wsClient._socket?.remoteAddress || 'неизвестен';
|
||
const clientId = Math.random().toString(36).substr(2, 9);
|
||
wsClient._clientId = clientId;
|
||
|
||
wsClient.cfg={avt:false};
|
||
|
||
console.log(`[${new Date().toLocaleTimeString()}] [WS CONNECTION] ✅ Подключен клиент:`, {
|
||
clientId: clientId,
|
||
clientIP: clientIP,
|
||
totalClients: wsServer.clients.size,
|
||
userAgent: wsClient.upgradeReq?.headers['user-agent'] || 'неизвестен'
|
||
});
|
||
|
||
wsClient.send(JSON.stringify({do:"link-toserver", link:global.conn_to_server, verSof: verSoft.ver }))
|
||
|
||
wsClient.on('message', function(message) {
|
||
ws.go(wsClient, message);
|
||
})
|
||
|
||
wsClient.on('close', function() {
|
||
console.log(`[${new Date().toLocaleTimeString()}] [WS CONNECTION] ❌ Отключился клиент:`, {
|
||
clientId: clientId,
|
||
clientIP: clientIP,
|
||
remainingClients: wsServer.clients.size,
|
||
wasAuthorized: !!wsClient.cfg.avt,
|
||
userId: wsClient.cfg.avt ? wsClient.cfg.avt._id : null
|
||
});
|
||
})
|
||
|
||
// Добавляем обработку ошибок клиента
|
||
wsClient.on('error', function(error) {
|
||
console.log(`[${new Date().toLocaleTimeString()}] [WS CONNECTION] 💥 Ошибка клиента:`, {
|
||
clientId: clientId,
|
||
clientIP: clientIP,
|
||
error: error.message
|
||
});
|
||
});
|
||
}
|
||
|
||
wsServer.on('connection', onConnect);
|
||
|
||
console.log("pult start" );
|
||
}
|
||
|
||
// Функция для раздачи тестовой страницы
|
||
async function serveTestPage(req, res) {
|
||
try {
|
||
const testPagePath = path.join(__dirname, 'test.html');
|
||
const content = await fs.readFile(testPagePath, 'utf8');
|
||
res.setHeader('Content-Type', 'text/html');
|
||
res.writeHead(200);
|
||
res.end(content);
|
||
} catch (error) {
|
||
console.error('Ошибка при раздаче тестовой страницы:', error);
|
||
res.writeHead(500);
|
||
res.end('Internal Server Error');
|
||
}
|
||
}
|
||
|
||
// Функция для раздачи React приложения
|
||
async function serveReactApp(req, res) {
|
||
try {
|
||
let filePath = req.url;
|
||
|
||
// Если запрос к корню, отдаем index.html
|
||
if (filePath === '/' || filePath === '/index.html') {
|
||
filePath = '/index.html';
|
||
}
|
||
|
||
// 🍊 ИСПОЛЬЗУЕМ ДИНАМИЧЕСКИЙ ПУТЬ В ЗАВИСИМОСТИ ОТ ТИПА УСТРОЙСТВА
|
||
let reactBuildPath;
|
||
|
||
// Проверяем несколько возможных путей
|
||
// ВАЖНО: сначала проверяем путь из global.staticDirectory
|
||
const possiblePaths = [
|
||
path.join(__dirname, global.staticDirectory || '../dist/client', filePath),
|
||
path.join(__dirname, '../dist/client-orange-pi', filePath),
|
||
path.join(__dirname, '../dist/client', filePath)
|
||
];
|
||
|
||
// Ищем первый существующий файл
|
||
for (const testPath of possiblePaths) {
|
||
try {
|
||
await fs.access(testPath);
|
||
reactBuildPath = testPath;
|
||
break;
|
||
} catch {
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// Если ни один путь не найден, используем fallback
|
||
if (!reactBuildPath) {
|
||
reactBuildPath = path.join(__dirname, '../dist/client', filePath);
|
||
}
|
||
|
||
// Проверяем существование файла
|
||
try {
|
||
await fs.access(reactBuildPath);
|
||
} catch {
|
||
// Если файл не найден, отдаем index.html (для SPA роутинга)
|
||
const indexPath = path.join(__dirname, global.staticDirectory, '/index.html');
|
||
try {
|
||
await fs.access(indexPath);
|
||
const indexContent = await fs.readFile(indexPath);
|
||
res.setHeader('Content-Type', 'text/html');
|
||
res.writeHead(200);
|
||
res.end(indexContent);
|
||
return;
|
||
} catch {
|
||
// Fallback: пробуем альтернативную директорию
|
||
const fallbackDir = global.staticDirectory.includes('orange-pi') ? '../dist/client' : '../dist/client-orange-pi';
|
||
const fallbackIndexPath = path.join(__dirname, fallbackDir, '/index.html');
|
||
try {
|
||
await fs.access(fallbackIndexPath);
|
||
const fallbackContent = await fs.readFile(fallbackIndexPath);
|
||
res.setHeader('Content-Type', 'text/html');
|
||
res.writeHead(200);
|
||
res.end(fallbackContent);
|
||
console.log(`⚠️ [FALLBACK] Использую резервную директорию: ${fallbackDir}`);
|
||
return;
|
||
} catch {
|
||
// Fallback к старому серверу если React билд недоступен
|
||
return api.go(req, res, '');
|
||
}
|
||
}
|
||
}
|
||
|
||
// Определяем Content-Type
|
||
const ext = path.extname(filePath).toLowerCase();
|
||
const contentTypes = {
|
||
'.html': 'text/html',
|
||
'.js': 'application/javascript',
|
||
'.css': 'text/css',
|
||
'.json': 'application/json',
|
||
'.png': 'image/png',
|
||
'.jpg': 'image/jpeg',
|
||
'.jpeg': 'image/jpeg',
|
||
'.gif': 'image/gif',
|
||
'.svg': 'image/svg+xml',
|
||
'.ico': 'image/x-icon'
|
||
};
|
||
|
||
const contentType = contentTypes[ext] || 'application/octet-stream';
|
||
res.setHeader('Content-Type', contentType);
|
||
|
||
// Читаем и отдаем файл
|
||
const fileContent = await fs.readFile(reactBuildPath);
|
||
res.writeHead(200);
|
||
res.end(fileContent);
|
||
|
||
} catch (error) {
|
||
console.error('Ошибка при раздаче React файлов:', error);
|
||
res.writeHead(500);
|
||
res.end('Internal Server Error');
|
||
}
|
||
}
|
||
|
||
main();
|
||
|
||
process.on("SIGINT", () => { // прослушиваем прерывание работы программы (ctrl-c)
|
||
process.exit();
|
||
});
|
||
|
||
|
||
|
||
|
||
|