diff --git a/ospabhost/backend/package-lock.json b/ospabhost/backend/package-lock.json
index a79fc1b..ff582fd 100644
--- a/ospabhost/backend/package-lock.json
+++ b/ospabhost/backend/package-lock.json
@@ -17,7 +17,9 @@
"dotenv": "^16.4.5",
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2",
- "multer": "^2.0.2"
+ "multer": "^2.0.2",
+ "nodemailer": "^6.9.16",
+ "socket.io": "^4.8.1"
},
"devDependencies": {
"@types/bcrypt": "^6.0.0",
@@ -27,6 +29,7 @@
"@types/jsonwebtoken": "^9.0.10",
"@types/multer": "^2.0.0",
"@types/node": "^20.12.12",
+ "@types/nodemailer": "^6.4.17",
"prisma": "^6.16.2",
"ts-node-dev": "^2.0.0",
"typescript": "^5.4.5"
@@ -158,6 +161,12 @@
"@prisma/debug": "6.16.2"
}
},
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+ "license": "MIT"
+ },
"node_modules/@standard-schema/spec": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
@@ -235,7 +244,6 @@
"version": "2.8.19",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
@@ -313,12 +321,21 @@
"version": "20.19.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz",
"integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
+ "node_modules/@types/nodemailer": {
+ "version": "6.4.17",
+ "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz",
+ "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
@@ -466,6 +483,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/base64id": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
+ "license": "MIT",
+ "engines": {
+ "node": "^4.5.0 || >= 5.9"
+ }
+ },
"node_modules/bcrypt": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
@@ -922,6 +948,67 @@
"node": ">= 0.8"
}
},
+ "node_modules/engine.io": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
+ "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/cors": "^2.8.12",
+ "@types/node": ">=10.0.0",
+ "accepts": "~1.3.4",
+ "base64id": "2.0.0",
+ "cookie": "~0.7.2",
+ "cors": "~2.8.5",
+ "debug": "~4.3.1",
+ "engine.io-parser": "~5.2.1",
+ "ws": "~8.17.1"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/engine.io-parser": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/engine.io/node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/engine.io/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/engine.io/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -1716,6 +1803,15 @@
"node-gyp-build-test": "build-test.js"
}
},
+ "node_modules/nodemailer": {
+ "version": "6.9.16",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz",
+ "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==",
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -2212,6 +2308,116 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/socket.io": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
+ "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.4",
+ "base64id": "~2.0.0",
+ "cors": "~2.8.5",
+ "debug": "~4.3.2",
+ "engine.io": "~6.6.0",
+ "socket.io-adapter": "~2.5.2",
+ "socket.io-parser": "~4.2.4"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/socket.io-adapter": {
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
+ "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "~4.3.4",
+ "ws": "~8.17.1"
+ }
+ },
+ "node_modules/socket.io-adapter/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-adapter/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/socket.io-parser": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/socket.io/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -2498,7 +2704,6 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/unpipe": {
@@ -2548,6 +2753,27 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/ws": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
diff --git a/ospabhost/backend/package.json b/ospabhost/backend/package.json
index aea5dd3..4f02ecb 100644
--- a/ospabhost/backend/package.json
+++ b/ospabhost/backend/package.json
@@ -20,7 +20,9 @@
"dotenv": "^16.4.5",
"express": "^4.21.2",
"jsonwebtoken": "^9.0.2",
- "multer": "^2.0.2"
+ "multer": "^2.0.2",
+ "nodemailer": "^6.9.16",
+ "socket.io": "^4.8.1"
},
"devDependencies": {
"@types/bcrypt": "^6.0.0",
@@ -30,6 +32,7 @@
"@types/jsonwebtoken": "^9.0.10",
"@types/multer": "^2.0.0",
"@types/node": "^20.12.12",
+ "@types/nodemailer": "^6.4.17",
"prisma": "^6.16.2",
"ts-node-dev": "^2.0.0",
"typescript": "^5.4.5"
diff --git a/ospabhost/backend/src/index.ts b/ospabhost/backend/src/index.ts
index 06262c8..3b8ea4e 100644
--- a/ospabhost/backend/src/index.ts
+++ b/ospabhost/backend/src/index.ts
@@ -1,6 +1,8 @@
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
+import http from 'http';
+import { Server as SocketIOServer } from 'socket.io';
import authRoutes from './modules/auth/auth.routes';
import ticketRoutes from './modules/ticket/ticket.routes';
import checkRoutes from './modules/check/check.routes';
@@ -8,10 +10,21 @@ import proxmoxRoutes from '../proxmox/proxmox.routes';
import tariffRoutes from './modules/tariff';
import osRoutes from './modules/os';
import serverRoutes from './modules/server';
+import { MonitoringService } from './modules/server/monitoring.service';
dotenv.config();
const app = express();
+const server = http.createServer(app);
+
+// Настройка Socket.IO с CORS
+const io = new SocketIOServer(server, {
+ cors: {
+ origin: ['http://localhost:3000', 'http://localhost:5173'],
+ methods: ['GET', 'POST'],
+ credentials: true
+ }
+});
// ИСПРАВЛЕНО: более точная настройка CORS
app.use(cors({
@@ -65,7 +78,13 @@ app.use('/api/server', serverRoutes);
const PORT = process.env.PORT || 5000;
-app.listen(PORT, () => {
+// Инициализация сервиса мониторинга
+const monitoringService = new MonitoringService(io);
+monitoringService.startMonitoring();
+
+server.listen(PORT, () => {
console.log(`🚀 Сервер запущен на порту ${PORT}`);
console.log(`📊 База данных: ${process.env.DATABASE_URL ? 'подключена' : 'НЕ НАСТРОЕНА'}`);
+ console.log(`🔌 WebSocket сервер запущен`);
+ console.log(`📡 Мониторинг серверов активен`);
});
\ No newline at end of file
diff --git a/ospabhost/backend/src/modules/notification/email.service.ts b/ospabhost/backend/src/modules/notification/email.service.ts
new file mode 100644
index 0000000..6839ac9
--- /dev/null
+++ b/ospabhost/backend/src/modules/notification/email.service.ts
@@ -0,0 +1,133 @@
+import nodemailer from 'nodemailer';
+import { PrismaClient } from '@prisma/client';
+
+const prisma = new PrismaClient();
+
+// Конфигурация email транспорта
+const transporter = nodemailer.createTransport({
+ host: process.env.SMTP_HOST || 'smtp.gmail.com',
+ port: Number(process.env.SMTP_PORT) || 587,
+ secure: false, // true для 465, false для других портов
+ auth: {
+ user: process.env.SMTP_USER,
+ pass: process.env.SMTP_PASS
+ }
+});
+
+export interface EmailNotification {
+ to: string;
+ subject: string;
+ text?: string;
+ html?: string;
+}
+
+// Отправка email уведомления
+export async function sendEmail(notification: EmailNotification) {
+ try {
+ // Проверяем наличие конфигурации SMTP
+ if (!process.env.SMTP_USER || !process.env.SMTP_PASS) {
+ console.log('SMTP not configured, skipping email notification');
+ return { status: 'skipped', message: 'SMTP not configured' };
+ }
+
+ const info = await transporter.sendMail({
+ from: `"Ospab Host" <${process.env.SMTP_USER}>`,
+ ...notification
+ });
+
+ console.log('Email sent: %s', info.messageId);
+ return { status: 'success', messageId: info.messageId };
+ } catch (error: any) {
+ console.error('Error sending email:', error);
+ return { status: 'error', message: error.message };
+ }
+}
+
+// Отправка уведомления о высокой нагрузке
+export async function sendResourceAlertEmail(userId: number, serverId: number, alertType: string, value: string) {
+ try {
+ const user = await prisma.user.findUnique({ where: { id: userId } });
+ if (!user) return { status: 'error', message: 'User not found' };
+
+ const subject = `Предупреждение: Высокая нагрузка на сервер #${serverId}`;
+ const html = `
+
Предупреждение о ресурсах сервера
+ Здравствуйте, ${user.username}!
+ Обнаружено превышение лимитов ресурсов на вашем сервере #${serverId}:
+
+ - Тип: ${alertType}
+ - Значение: ${value}
+
+ Рекомендуем проверить сервер и при необходимости увеличить его ресурсы.
+ С уважением,
Команда Ospab Host
+ `;
+
+ return await sendEmail({
+ to: user.email,
+ subject,
+ html
+ });
+ } catch (error: any) {
+ console.error('Error sending resource alert email:', error);
+ return { status: 'error', message: error.message };
+ }
+}
+
+// Отправка уведомления о создании сервера
+export async function sendServerCreatedEmail(userId: number, serverId: number, serverDetails: any) {
+ try {
+ const user = await prisma.user.findUnique({ where: { id: userId } });
+ if (!user) return { status: 'error', message: 'User not found' };
+
+ const subject = `Ваш сервер #${serverId} успешно создан`;
+ const html = `
+ Сервер успешно создан!
+ Здравствуйте, ${user.username}!
+ Ваш новый сервер был успешно создан:
+
+ - ID сервера: ${serverId}
+ - Тариф: ${serverDetails.tariff}
+ - ОС: ${serverDetails.os}
+ - IP адрес: ${serverDetails.ip || 'Получение...'}
+
+ Вы можете управлять сервером через панель управления.
+ С уважением,
Команда Ospab Host
+ `;
+
+ return await sendEmail({
+ to: user.email,
+ subject,
+ html
+ });
+ } catch (error: any) {
+ console.error('Error sending server created email:', error);
+ return { status: 'error', message: error.message };
+ }
+}
+
+// Отправка уведомления о приближении срока оплаты
+export async function sendPaymentReminderEmail(userId: number, serverId: number, daysLeft: number) {
+ try {
+ const user = await prisma.user.findUnique({ where: { id: userId } });
+ if (!user) return { status: 'error', message: 'User not found' };
+
+ const subject = `Напоминание: Оплата за сервер #${serverId}`;
+ const html = `
+ Напоминание об оплате
+ Здравствуйте, ${user.username}!
+ До окончания срока действия вашего тарифа для сервера #${serverId} осталось ${daysLeft} дней.
+ Пожалуйста, пополните баланс, чтобы избежать прерывания обслуживания.
+ Ваш текущий баланс: ${user.balance}₽
+ С уважением,
Команда Ospab Host
+ `;
+
+ return await sendEmail({
+ to: user.email,
+ subject,
+ html
+ });
+ } catch (error: any) {
+ console.error('Error sending payment reminder email:', error);
+ return { status: 'error', message: error.message };
+ }
+}
diff --git a/ospabhost/backend/src/modules/server/monitoring.service.ts b/ospabhost/backend/src/modules/server/monitoring.service.ts
new file mode 100644
index 0000000..7570bf4
--- /dev/null
+++ b/ospabhost/backend/src/modules/server/monitoring.service.ts
@@ -0,0 +1,191 @@
+import { Server as SocketIOServer } from 'socket.io';
+import { PrismaClient } from '@prisma/client';
+import { getContainerStats } from './proxmoxApi';
+import { sendResourceAlertEmail } from '../notification/email.service';
+
+const prisma = new PrismaClient();
+
+export class MonitoringService {
+ private io: SocketIOServer;
+ private monitoringInterval: NodeJS.Timeout | null = null;
+ private readonly MONITORING_INTERVAL = 30000; // 30 секунд
+
+ constructor(io: SocketIOServer) {
+ this.io = io;
+ this.setupSocketHandlers();
+ }
+
+ private setupSocketHandlers() {
+ this.io.on('connection', (socket) => {
+ console.log(`Client connected: ${socket.id}`);
+
+ // Подписка на обновления конкретного сервера
+ socket.on('subscribe-server', async (serverId: number) => {
+ console.log(`Client ${socket.id} subscribed to server ${serverId}`);
+ socket.join(`server-${serverId}`);
+
+ // Отправляем начальную статистику
+ try {
+ const server = await prisma.server.findUnique({ where: { id: serverId } });
+ if (server && server.proxmoxId) {
+ const stats = await getContainerStats(server.proxmoxId);
+ socket.emit('server-stats', { serverId, stats });
+ }
+ } catch (error) {
+ console.error(`Error fetching initial stats for server ${serverId}:`, error);
+ }
+ });
+
+ // Отписка от обновлений сервера
+ socket.on('unsubscribe-server', (serverId: number) => {
+ console.log(`Client ${socket.id} unsubscribed from server ${serverId}`);
+ socket.leave(`server-${serverId}`);
+ });
+
+ socket.on('disconnect', () => {
+ console.log(`Client disconnected: ${socket.id}`);
+ });
+ });
+ }
+
+ // Запуск периодического мониторинга
+ public startMonitoring() {
+ if (this.monitoringInterval) {
+ console.log('Monitoring already running');
+ return;
+ }
+
+ console.log('Starting server monitoring service...');
+ this.monitoringInterval = setInterval(async () => {
+ await this.checkAllServers();
+ }, this.MONITORING_INTERVAL);
+
+ // Первая проверка сразу
+ this.checkAllServers();
+ }
+
+ // Остановка мониторинга
+ public stopMonitoring() {
+ if (this.monitoringInterval) {
+ clearInterval(this.monitoringInterval);
+ this.monitoringInterval = null;
+ console.log('Monitoring service stopped');
+ }
+ }
+
+ // Проверка всех активных серверов
+ private async checkAllServers() {
+ try {
+ const servers = await prisma.server.findMany({
+ where: {
+ status: {
+ in: ['running', 'stopped', 'creating']
+ }
+ }
+ });
+
+ for (const server of servers) {
+ if (server.proxmoxId) {
+ try {
+ const stats = await getContainerStats(server.proxmoxId);
+
+ if (stats.status === 'success' && stats.data) {
+ // Обновляем статус и метрики в БД
+ await prisma.server.update({
+ where: { id: server.id },
+ data: {
+ status: stats.data.status,
+ cpuUsage: stats.data.cpu || 0,
+ memoryUsage: stats.data.memory?.usage || 0,
+ diskUsage: stats.data.disk?.usage || 0,
+ networkIn: stats.data.network?.in || 0,
+ networkOut: stats.data.network?.out || 0,
+ lastPing: new Date()
+ }
+ });
+
+ // Отправляем обновления подписанным клиентам
+ this.io.to(`server-${server.id}`).emit('server-stats', {
+ serverId: server.id,
+ stats
+ });
+
+ // Проверяем превышение лимитов и отправляем алерты
+ await this.checkResourceLimits(server, stats.data);
+ }
+ } catch (error) {
+ console.error(`Error monitoring server ${server.id}:`, error);
+ }
+ }
+ }
+ } catch (error) {
+ console.error('Error in checkAllServers:', error);
+ }
+ }
+
+ // Проверка превышения лимитов ресурсов
+ private async checkResourceLimits(server: any, stats: any) {
+ const alerts = [];
+
+ // CPU превышает 90%
+ if (stats.cpu && stats.cpu > 0.9) {
+ alerts.push({
+ type: 'cpu',
+ message: `CPU usage is at ${(stats.cpu * 100).toFixed(1)}%`,
+ level: 'warning'
+ });
+
+ // Отправляем email уведомление
+ await sendResourceAlertEmail(
+ server.userId,
+ server.id,
+ 'CPU',
+ `${(stats.cpu * 100).toFixed(1)}%`
+ );
+ }
+
+ // Memory превышает 90%
+ if (stats.memory?.usage && stats.memory.usage > 90) {
+ alerts.push({
+ type: 'memory',
+ message: `Memory usage is at ${stats.memory.usage.toFixed(1)}%`,
+ level: 'warning'
+ });
+
+ // Отправляем email уведомление
+ await sendResourceAlertEmail(
+ server.userId,
+ server.id,
+ 'Memory',
+ `${stats.memory.usage.toFixed(1)}%`
+ );
+ }
+
+ // Disk превышает 90%
+ if (stats.disk?.usage && stats.disk.usage > 90) {
+ alerts.push({
+ type: 'disk',
+ message: `Disk usage is at ${stats.disk.usage.toFixed(1)}%`,
+ level: 'warning'
+ });
+
+ // Отправляем email уведомление
+ await sendResourceAlertEmail(
+ server.userId,
+ server.id,
+ 'Disk',
+ `${stats.disk.usage.toFixed(1)}%`
+ );
+ }
+
+ // Отправляем алерты, если есть
+ if (alerts.length > 0) {
+ this.io.to(`server-${server.id}`).emit('server-alerts', {
+ serverId: server.id,
+ alerts
+ });
+
+ console.log(`Alerts for server ${server.id}:`, alerts);
+ }
+ }
+}
diff --git a/ospabhost/backend/src/modules/server/proxmoxApi.ts b/ospabhost/backend/src/modules/server/proxmoxApi.ts
index aa3aacf..fed4bc6 100644
--- a/ospabhost/backend/src/modules/server/proxmoxApi.ts
+++ b/ospabhost/backend/src/modules/server/proxmoxApi.ts
@@ -342,6 +342,133 @@ export async function getConsoleURL(vmid: number): Promise<{ status: string; url
}
}
+// Изменение конфигурации контейнера (CPU, RAM, Disk)
+export async function resizeContainer(vmid: number, config: { cores?: number; memory?: number; rootfs?: string }) {
+ try {
+ const response = await axios.put(
+ `${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/config`,
+ config,
+ { headers: getProxmoxHeaders() }
+ );
+ return {
+ status: 'success',
+ data: response.data?.data
+ };
+ } catch (error: any) {
+ console.error('Ошибка изменения конфигурации:', error);
+ return {
+ status: 'error',
+ message: error.response?.data?.errors || error.message
+ };
+ }
+}
+
+// Создание снэпшота
+export async function createSnapshot(vmid: number, snapname: string, description?: string) {
+ try {
+ const response = await axios.post(
+ `${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/snapshot`,
+ {
+ snapname,
+ description: description || `Snapshot ${snapname}`
+ },
+ { headers: getProxmoxHeaders() }
+ );
+ return {
+ status: 'success',
+ taskId: response.data?.data,
+ snapname
+ };
+ } catch (error: any) {
+ console.error('Ошибка создания снэпшота:', error);
+ return {
+ status: 'error',
+ message: error.response?.data?.errors || error.message
+ };
+ }
+}
+
+// Получение списка снэпшотов
+export async function listSnapshots(vmid: number) {
+ try {
+ const response = await axios.get(
+ `${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/snapshot`,
+ { headers: getProxmoxHeaders() }
+ );
+ return {
+ status: 'success',
+ data: response.data?.data || []
+ };
+ } catch (error: any) {
+ console.error('Ошибка получения списка снэпшотов:', error);
+ return {
+ status: 'error',
+ message: error.response?.data?.errors || error.message
+ };
+ }
+}
+
+// Восстановление из снэпшота
+export async function rollbackSnapshot(vmid: number, snapname: string) {
+ try {
+ const response = await axios.post(
+ `${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/snapshot/${snapname}/rollback`,
+ {},
+ { headers: getProxmoxHeaders() }
+ );
+ return {
+ status: 'success',
+ taskId: response.data?.data
+ };
+ } catch (error: any) {
+ console.error('Ошибка восстановления снэпшота:', error);
+ return {
+ status: 'error',
+ message: error.response?.data?.errors || error.message
+ };
+ }
+}
+
+// Удаление снэпшота
+export async function deleteSnapshot(vmid: number, snapname: string) {
+ try {
+ const response = await axios.delete(
+ `${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/snapshot/${snapname}`,
+ { headers: getProxmoxHeaders() }
+ );
+ return {
+ status: 'success',
+ taskId: response.data?.data
+ };
+ } catch (error: any) {
+ console.error('Ошибка удаления снэпшота:', error);
+ return {
+ status: 'error',
+ message: error.response?.data?.errors || error.message
+ };
+ }
+}
+
+// Получение списка всех контейнеров
+export async function listContainers() {
+ try {
+ const response = await axios.get(
+ `${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc`,
+ { headers: getProxmoxHeaders() }
+ );
+ return {
+ status: 'success',
+ data: response.data?.data || []
+ };
+ } catch (error: any) {
+ console.error('Ошибка получения списка контейнеров:', error);
+ return {
+ status: 'error',
+ message: error.response?.data?.errors || error.message
+ };
+ }
+}
+
// Проверка соединения с Proxmox
export async function checkProxmoxConnection() {
try {
diff --git a/ospabhost/backend/src/modules/server/server.controller.ts b/ospabhost/backend/src/modules/server/server.controller.ts
index e1a2832..bf2e1a1 100644
--- a/ospabhost/backend/src/modules/server/server.controller.ts
+++ b/ospabhost/backend/src/modules/server/server.controller.ts
@@ -5,7 +5,12 @@ import {
controlContainer,
getContainerStats,
changeRootPassword as proxmoxChangeRootPassword,
- deleteContainer
+ deleteContainer,
+ resizeContainer,
+ createSnapshot,
+ listSnapshots,
+ rollbackSnapshot,
+ deleteSnapshot
} from './proxmoxApi';
const prisma = new PrismaClient();
@@ -212,3 +217,88 @@ export async function changeRootPassword(req: Request, res: Response) {
res.status(500).json({ error: error?.message || 'Ошибка смены пароля' });
}
}
+
+// Изменить конфигурацию сервера
+export async function resizeServer(req: Request, res: Response) {
+ try {
+ const id = Number(req.params.id);
+ const { cores, memory, disk } = req.body;
+ const server = await prisma.server.findUnique({ where: { id } });
+ if (!server || !server.proxmoxId) return res.status(404).json({ error: 'Сервер не найден или нет VMID' });
+
+ const config: any = {};
+ if (cores) config.cores = Number(cores);
+ if (memory) config.memory = Number(memory);
+ if (disk) config.rootfs = `local:${Number(disk)}`;
+
+ const result = await resizeContainer(server.proxmoxId, config);
+ res.json(result);
+ } catch (error: any) {
+ res.status(500).json({ error: error?.message || 'Ошибка изменения конфигурации' });
+ }
+}
+
+// Создать снэпшот
+export async function createServerSnapshot(req: Request, res: Response) {
+ try {
+ const id = Number(req.params.id);
+ const { snapname, description } = req.body;
+ if (!snapname) return res.status(400).json({ error: 'Не указано имя снэпшота' });
+
+ const server = await prisma.server.findUnique({ where: { id } });
+ if (!server || !server.proxmoxId) return res.status(404).json({ error: 'Сервер не найден или нет VMID' });
+
+ const result = await createSnapshot(server.proxmoxId, snapname, description);
+ res.json(result);
+ } catch (error: any) {
+ res.status(500).json({ error: error?.message || 'Ошибка создания снэпшота' });
+ }
+}
+
+// Получить список снэпшотов
+export async function getServerSnapshots(req: Request, res: Response) {
+ try {
+ const id = Number(req.params.id);
+ const server = await prisma.server.findUnique({ where: { id } });
+ if (!server || !server.proxmoxId) return res.status(404).json({ error: 'Сервер не найден или нет VMID' });
+
+ const result = await listSnapshots(server.proxmoxId);
+ res.json(result);
+ } catch (error: any) {
+ res.status(500).json({ error: error?.message || 'Ошибка получения снэпшотов' });
+ }
+}
+
+// Восстановить из снэпшота
+export async function rollbackServerSnapshot(req: Request, res: Response) {
+ try {
+ const id = Number(req.params.id);
+ const { snapname } = req.body;
+ if (!snapname) return res.status(400).json({ error: 'Не указано имя снэпшота' });
+
+ const server = await prisma.server.findUnique({ where: { id } });
+ if (!server || !server.proxmoxId) return res.status(404).json({ error: 'Сервер не найден или нет VMID' });
+
+ const result = await rollbackSnapshot(server.proxmoxId, snapname);
+ res.json(result);
+ } catch (error: any) {
+ res.status(500).json({ error: error?.message || 'Ошибка восстановления снэпшота' });
+ }
+}
+
+// Удалить снэпшот
+export async function deleteServerSnapshot(req: Request, res: Response) {
+ try {
+ const id = Number(req.params.id);
+ const { snapname } = req.body;
+ if (!snapname) return res.status(400).json({ error: 'Не указано имя снэпшота' });
+
+ const server = await prisma.server.findUnique({ where: { id } });
+ if (!server || !server.proxmoxId) return res.status(404).json({ error: 'Сервер не найден или нет VMID' });
+
+ const result = await deleteSnapshot(server.proxmoxId, snapname);
+ res.json(result);
+ } catch (error: any) {
+ res.status(500).json({ error: error?.message || 'Ошибка удаления снэпшота' });
+ }
+}
diff --git a/ospabhost/backend/src/modules/server/server.routes.ts b/ospabhost/backend/src/modules/server/server.routes.ts
index 2c17b28..2e182ec 100644
--- a/ospabhost/backend/src/modules/server/server.routes.ts
+++ b/ospabhost/backend/src/modules/server/server.routes.ts
@@ -7,7 +7,12 @@ import {
restartServer,
getServerStatus,
deleteServer,
- changeRootPassword
+ changeRootPassword,
+ resizeServer,
+ createServerSnapshot,
+ getServerSnapshots,
+ rollbackServerSnapshot,
+ deleteServerSnapshot
} from './server.controller';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
@@ -72,4 +77,11 @@ router.post('/:id/restart', restartServer);
router.delete('/:id', deleteServer);
router.post('/:id/password', changeRootPassword);
+// Новые маршруты для управления конфигурацией и снэпшотами
+router.put('/:id/resize', resizeServer);
+router.post('/:id/snapshots', createServerSnapshot);
+router.get('/:id/snapshots', getServerSnapshots);
+router.post('/:id/snapshots/rollback', rollbackServerSnapshot);
+router.delete('/:id/snapshots', deleteServerSnapshot);
+
export default router;
\ No newline at end of file