feat: initial commit - ServerManager Pro v2.0.0
This commit is contained in:
@@ -0,0 +1,555 @@
|
||||
const { app, BrowserWindow, ipcMain, shell, dialog, clipboard } = require('electron');
|
||||
const path = require('path');
|
||||
const isDev = require('electron-is-dev');
|
||||
const { exec } = require('child_process');
|
||||
const fs = require('fs');
|
||||
|
||||
// КРИТИЧЕСКИ ВАЖНО ДЛЯ LINUX - исправление песочницы
|
||||
app.commandLine.appendSwitch('--no-sandbox');
|
||||
app.commandLine.appendSwitch('--disable-setuid-sandbox');
|
||||
app.commandLine.appendSwitch('--disable-gpu');
|
||||
|
||||
// Дополнительные флаги для Linux
|
||||
if (process.platform === 'linux') {
|
||||
app.commandLine.appendSwitch('--no-zygote');
|
||||
app.commandLine.appendSwitch('--disable-dev-shm-usage');
|
||||
}
|
||||
|
||||
app.disableHardwareAcceleration();
|
||||
|
||||
let mainWindow;
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
webSecurity: false,
|
||||
},
|
||||
title: 'ServerManager Pro',
|
||||
icon: path.join(__dirname, 'icon.png'),
|
||||
show: false,
|
||||
titleBarStyle: 'default'
|
||||
});
|
||||
|
||||
const startUrl = isDev
|
||||
? 'http://localhost:3000'
|
||||
: `file://${path.join(__dirname, '../build/index.html')}`;
|
||||
|
||||
mainWindow.loadURL(startUrl);
|
||||
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
});
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
// Открываем DevTools только в режиме разработки
|
||||
if (isDev) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ====================
|
||||
|
||||
function ensureUserDataDir() {
|
||||
const userDataPath = app.getPath('userData');
|
||||
if (!fs.existsSync(userDataPath)) {
|
||||
fs.mkdirSync(userDataPath, { recursive: true });
|
||||
}
|
||||
return userDataPath;
|
||||
}
|
||||
|
||||
ipcMain.handle('connect-ssh', async (event, server) => {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const { ip, port = '22', username = 'root', password, sshKey } = server;
|
||||
|
||||
// Очищаем IP от лишних символов
|
||||
const cleanIp = ip.trim().replace(/[^0-9.:]/g, '');
|
||||
|
||||
let sshCommand = `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${username}@${cleanIp} -p ${port}`;
|
||||
|
||||
if (sshKey) {
|
||||
const tempKeyPath = `/tmp/ssh_key_${Date.now()}`;
|
||||
fs.writeFileSync(tempKeyPath, sshKey);
|
||||
fs.chmodSync(tempKeyPath, 0o600);
|
||||
sshCommand = `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 -i ${tempKeyPath} ${username}@${cleanIp} -p ${port}`;
|
||||
}
|
||||
|
||||
const terminals = [
|
||||
{ cmd: 'gnome-terminal --', test: 'gnome-terminal' },
|
||||
{ cmd: 'konsole -e', test: 'konsole' },
|
||||
{ cmd: 'xfce4-terminal -x', test: 'xfce4-terminal' },
|
||||
{ cmd: 'mate-terminal -x', test: 'mate-terminal' },
|
||||
{ cmd: 'xterm -e', test: 'xterm' }
|
||||
];
|
||||
|
||||
const tryTerminal = (index) => {
|
||||
if (index >= terminals.length) {
|
||||
// Если терминал не найден, показываем команду для копирования
|
||||
const message = `Терминал не найден\n\nКоманда SSH:\n${sshCommand}\n\nПароль: ${password || 'не установлен'}\n\nСкопируйте команду и выполните вручную в терминале.`;
|
||||
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
type: 'info',
|
||||
title: 'SSH Подключение',
|
||||
message: 'Терминал не найден',
|
||||
detail: message
|
||||
}).then(() => {
|
||||
resolve({ success: true });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const terminal = terminals[index];
|
||||
|
||||
// Проверяем, доступен ли терминал
|
||||
exec(`which ${terminal.test}`, (error) => {
|
||||
if (error) {
|
||||
tryTerminal(index + 1);
|
||||
} else {
|
||||
console.log('Using terminal:', terminal.cmd);
|
||||
const fullCommand = `${terminal.cmd} bash -c "${sshCommand}; echo 'Нажмите Enter для закрытия...'; read"`;
|
||||
|
||||
exec(fullCommand, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error('SSH terminal error:', error);
|
||||
// Показываем ошибку пользователю
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
type: 'error',
|
||||
title: 'Ошибка SSH',
|
||||
message: `Не удалось подключиться: ${error.message}`,
|
||||
detail: stderr || 'Проверьте параметры подключения'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
resolve({ success: true });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
tryTerminal(0);
|
||||
|
||||
} catch (error) {
|
||||
console.error('SSH connection error:', error);
|
||||
resolve({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== ВЕБ-ИНТЕРФЕЙС ====================
|
||||
ipcMain.handle('open-web-interface', async (event, server) => {
|
||||
try {
|
||||
const { ip, webPort = '51821' } = server;
|
||||
|
||||
// Очищаем IP
|
||||
const cleanIp = ip.trim().replace(/[^0-9.:]/g, '');
|
||||
const url = `http://${cleanIp}:${webPort}`;
|
||||
|
||||
console.log('Opening web interface:', url);
|
||||
|
||||
// Проверяем доступность URL перед открытием
|
||||
const { exec } = require('child_process');
|
||||
|
||||
// Проверяем доступность с помощью curl
|
||||
exec(`curl -s --head --connect-timeout 3 ${url}`, (error) => {
|
||||
if (error) {
|
||||
// Если недоступно, показываем предупреждение
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
type: 'warning',
|
||||
title: 'Веб-интерфейс может быть недоступен',
|
||||
message: 'Сервер может не отвечать',
|
||||
detail: `URL: ${url}\n\nПроверьте:\n• Доступность сервера\n• Правильность порта\n• Запущен ли веб-сервис`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await shell.openExternal(url);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error opening web interface:', error);
|
||||
|
||||
const { ip, webPort = '51821' } = server;
|
||||
const cleanIp = ip.trim().replace(/[^0-9.:]/g, '');
|
||||
const url = `http://${cleanIp}:${webPort}`;
|
||||
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
type: 'info',
|
||||
title: 'Веб-интерфейс',
|
||||
message: 'Откройте в браузере',
|
||||
detail: `URL: ${url}\n\nЕсли не открывается автоматически, скопируйте ссылку в браузер.`
|
||||
});
|
||||
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== УПРАВЛЕНИЕ ДАННЫМИ ====================
|
||||
|
||||
// Серверы
|
||||
ipcMain.handle('load-servers', async () => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const serversPath = path.join(userDataPath, 'servers.json');
|
||||
if (fs.existsSync(serversPath)) {
|
||||
const data = fs.readFileSync(serversPath, 'utf8');
|
||||
return { success: true, servers: JSON.parse(data) };
|
||||
}
|
||||
return { success: true, servers: [] };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message, servers: [] };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('save-servers', async (event, servers) => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const serversPath = path.join(userDataPath, 'servers.json');
|
||||
fs.writeFileSync(serversPath, JSON.stringify(servers, null, 2));
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Пароли
|
||||
ipcMain.handle('load-passwords', async () => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const passwordsPath = path.join(userDataPath, 'passwords.json');
|
||||
if (fs.existsSync(passwordsPath)) {
|
||||
const data = fs.readFileSync(passwordsPath, 'utf8');
|
||||
return { success: true, passwords: JSON.parse(data) };
|
||||
}
|
||||
return { success: true, passwords: [] };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message, passwords: [] };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('save-passwords', async (event, passwords) => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const passwordsPath = path.join(userDataPath, 'passwords.json');
|
||||
fs.writeFileSync(passwordsPath, JSON.stringify(passwords, null, 2));
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Шаблоны команд
|
||||
ipcMain.handle('load-command-templates', async () => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const templatesPath = path.join(userDataPath, 'command-templates.json');
|
||||
if (fs.existsSync(templatesPath)) {
|
||||
const data = fs.readFileSync(templatesPath, 'utf8');
|
||||
return { success: true, templates: JSON.parse(data) };
|
||||
}
|
||||
return { success: true, templates: [] };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message, templates: [] };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('save-command-templates', async (event, templates) => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const templatesPath = path.join(userDataPath, 'command-templates.json');
|
||||
fs.writeFileSync(templatesPath, JSON.stringify(templates, null, 2));
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Базовые утилиты
|
||||
ipcMain.handle('copy-to-clipboard', async (event, text) => {
|
||||
try {
|
||||
clipboard.writeText(text);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Экспорт/импорт данных
|
||||
ipcMain.handle('export-data', async (event, data) => {
|
||||
const result = await dialog.showSaveDialog(mainWindow, {
|
||||
title: 'Экспорт данных',
|
||||
defaultPath: `servermanager-backup-${new Date().toISOString().split('T')[0]}.json`,
|
||||
filters: [{ name: 'JSON', extensions: ['json'] }]
|
||||
});
|
||||
|
||||
if (result.canceled) return { success: false, error: 'Отменено' };
|
||||
|
||||
try {
|
||||
fs.writeFileSync(result.filePath, JSON.stringify(data, null, 2));
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('import-data', async (event) => {
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
title: 'Импорт данных',
|
||||
filters: [{ name: 'JSON', extensions: ['json'] }],
|
||||
properties: ['openFile']
|
||||
});
|
||||
|
||||
if (result.canceled) return { success: false, error: 'Отменено' };
|
||||
|
||||
try {
|
||||
const data = fs.readFileSync(result.filePaths[0], 'utf8');
|
||||
return { success: true, data: JSON.parse(data) };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Добавьте после других ipcMain.handle
|
||||
|
||||
// Уведомления
|
||||
ipcMain.handle('show-notification', async (event, title, body) => {
|
||||
try {
|
||||
// Используем встроенные уведомления Electron
|
||||
const notification = {
|
||||
title: title,
|
||||
body: body,
|
||||
silent: true
|
||||
};
|
||||
|
||||
// Показываем уведомление в главном процессе
|
||||
const notif = new Notification(notification);
|
||||
notif.show();
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Notification error:', error);
|
||||
// Если уведомления не работают, просто логируем
|
||||
console.log(`Уведомление: ${title} - ${body}`);
|
||||
return { success: true };
|
||||
}
|
||||
});
|
||||
// Логирование действий
|
||||
ipcMain.handle('log-action', async (event, action) => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const logPath = path.join(userDataPath, 'actions.log');
|
||||
const timestamp = new Date().toISOString();
|
||||
const logEntry = `${timestamp}: ${action}\n`;
|
||||
fs.appendFileSync(logPath, logEntry);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Ping функция для проверки серверов
|
||||
ipcMain.handle('ping-server', async (event, server) => {
|
||||
try {
|
||||
const { ip } = server;
|
||||
const cleanIp = ip.trim().replace(/[^0-9.:]/g, '');
|
||||
|
||||
// Используем системный ping
|
||||
const { exec } = require('child_process');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
exec(`ping -c 2 -W 1 ${cleanIp}`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
resolve({
|
||||
success: true,
|
||||
online: false,
|
||||
responseTime: null,
|
||||
packetLoss: 100
|
||||
});
|
||||
} else {
|
||||
// Парсим вывод ping для получения времени ответа
|
||||
const timeMatch = stdout.match(/time=(\d+\.?\d*) ms/);
|
||||
const responseTime = timeMatch ? parseFloat(timeMatch[1]) : null;
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
online: true,
|
||||
responseTime: responseTime,
|
||||
packetLoss: 0
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
online: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== СЕТЕВЫЕ ДАННЫЕ ====================
|
||||
|
||||
// Получение сетевых адаптеров
|
||||
ipcMain.handle('get-network-adapters', async () => {
|
||||
try {
|
||||
const { networkInterfaces } = require('os');
|
||||
const interfaces = networkInterfaces();
|
||||
const adapters = [];
|
||||
|
||||
for (const [name, details] of Object.entries(interfaces)) {
|
||||
for (const detail of details) {
|
||||
if (detail.family === 'IPv4' && !detail.internal) {
|
||||
adapters.push({
|
||||
name: name,
|
||||
description: `${name} Network Interface`,
|
||||
status: 'Up',
|
||||
ipAddress: detail.address,
|
||||
macAddress: detail.mac,
|
||||
type: getInterfaceType(name)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, adapters };
|
||||
} catch (error) {
|
||||
console.error('Error getting network adapters:', error);
|
||||
return { success: false, error: error.message, adapters: [] };
|
||||
}
|
||||
});
|
||||
|
||||
// Получение VPN подключений
|
||||
ipcMain.handle('get-vpn-connections', async () => {
|
||||
try {
|
||||
const connections = [];
|
||||
|
||||
// Проверяем WireGuard
|
||||
try {
|
||||
const { execSync } = require('child_process');
|
||||
const wireguardResult = execSync('ip link show type wireguard 2>/dev/null || echo ""', { encoding: 'utf8' });
|
||||
if (wireguardResult && wireguardResult.includes('wireguard')) {
|
||||
connections.push({
|
||||
name: 'WireGuard VPN',
|
||||
interface: 'wg0',
|
||||
status: 'Connected',
|
||||
description: 'WireGuard VPN Tunnel'
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// WireGuard не установлен или нет интерфейсов
|
||||
}
|
||||
|
||||
// Проверяем OpenVPN
|
||||
try {
|
||||
const { execSync } = require('child_process');
|
||||
const openvpnResult = execSync('ps aux | grep openvpn | grep -v grep || echo ""', { encoding: 'utf8' });
|
||||
if (openvpnResult && openvpnResult.includes('openvpn')) {
|
||||
connections.push({
|
||||
name: 'OpenVPN',
|
||||
interface: 'tun0',
|
||||
status: 'Connected',
|
||||
description: 'OpenVPN Connection'
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// OpenVPN не запущен
|
||||
}
|
||||
|
||||
return { success: true, connections };
|
||||
} catch (error) {
|
||||
console.error('Error getting VPN connections:', error);
|
||||
return { success: false, error: error.message, connections: [] };
|
||||
}
|
||||
});
|
||||
|
||||
// Вспомогательная функция для определения типа интерфейса
|
||||
function getInterfaceType(name) {
|
||||
if (name.includes('wl') || name.includes('wlan') || name.includes('wifi')) {
|
||||
return 'Wi-Fi';
|
||||
} else if (name.includes('eth') || name.includes('enp') || name.includes('ens')) {
|
||||
return 'Ethernet';
|
||||
} else if (name.includes('tun') || name.includes('tap')) {
|
||||
return 'VPN';
|
||||
} else if (name.includes('lo')) {
|
||||
return 'Loopback';
|
||||
} else {
|
||||
return 'Ethernet';
|
||||
}
|
||||
}
|
||||
|
||||
// Заметки
|
||||
ipcMain.handle('load-notes', async () => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const notesPath = path.join(userDataPath, 'notes.json');
|
||||
if (fs.existsSync(notesPath)) {
|
||||
const data = fs.readFileSync(notesPath, 'utf8');
|
||||
return { success: true, notes: JSON.parse(data) };
|
||||
}
|
||||
return { success: true, notes: [] };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message, notes: [] };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('save-notes', async (event, notes) => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const notesPath = path.join(userDataPath, 'notes.json');
|
||||
fs.writeFileSync(notesPath, JSON.stringify(notes, null, 2));
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('load-note-folders', async () => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const foldersPath = path.join(userDataPath, 'note-folders.json');
|
||||
if (fs.existsSync(foldersPath)) {
|
||||
const data = fs.readFileSync(foldersPath, 'utf8');
|
||||
return { success: true, folders: JSON.parse(data) };
|
||||
}
|
||||
return { success: true, folders: [] };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message, folders: [] };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('save-note-folders', async (event, folders) => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const foldersPath = path.join(userDataPath, 'note-folders.json');
|
||||
fs.writeFileSync(foldersPath, JSON.stringify(folders, null, 2));
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user