Files
vue-pult/server/index.js
sasha 3e90269b0b Initial commit: Vue.js тир управления система
- Клиентская часть Vue 3 + Vite
- Серверная часть Node.js + WebSocket
- Система авторизации и смен
- Управление игровыми портами
- Поддержка тем (светлая/темная)
- Адаптивный дизайн

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-19 12:24:22 +03:00

541 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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();
});