ssh! и документы

This commit is contained in:
Georgiy Syralev
2025-10-12 15:20:39 +03:00
parent 141955dd46
commit 4211692312
50 changed files with 920 additions and 1333 deletions

View File

@@ -1,10 +1,14 @@
import { Router } from 'express';
import { PrismaClient } from '@prisma/client';
import { authMiddleware } from '../auth/auth.middleware';
const router = Router();
const prisma = new PrismaClient();
// GET /api/os — получить все ОС
router.use(authMiddleware);
// GET /api/os — получить все ОС (только для авторизованных)
router.get('/', async (req, res) => {
try {
const oses = await prisma.operatingSystem.findMany();

View File

@@ -1,3 +1,19 @@
// Смена root-пароля через SSH (для LXC)
import { exec } from 'child_process';
export async function changeRootPasswordSSH(vmid: number): Promise<{ status: string; password?: string; message?: string }> {
const newPassword = generateSecurePassword();
return new Promise((resolve) => {
exec(`ssh -o StrictHostKeyChecking=no root@${process.env.PROXMOX_NODE} pct set ${vmid} --password ${newPassword}`, (err, stdout, stderr) => {
if (err) {
console.error('Ошибка смены пароля через SSH:', stderr);
resolve({ status: 'error', message: stderr });
} else {
resolve({ status: 'success', password: newPassword });
}
});
});
}
import axios from 'axios';
import crypto from 'crypto';
import dotenv from 'dotenv';

View File

@@ -0,0 +1,70 @@
import { Server as WebSocketServer, WebSocket } from 'ws';
import { Client as SSHClient } from 'ssh2';
import dotenv from 'dotenv';
import { IncomingMessage } from 'http';
import { Server as HttpServer } from 'http';
dotenv.config();
export function setupConsoleWSS(server: HttpServer) {
const wss = new WebSocketServer({ noServer: true });
wss.on('connection', (ws: WebSocket, req: IncomingMessage) => {
const url = req.url || '';
const match = url.match(/\/api\/server\/(\d+)\/console/);
const vmid = match ? match[1] : null;
if (!vmid) {
ws.close();
return;
}
// Получаем IP и root-пароль из БД (упрощённо)
// Здесь можно добавить реальный запрос к Prisma
const host = process.env.PROXMOX_IP || process.env.PROXMOX_NODE;
const username = 'root';
const password = process.env.PROXMOX_ROOT_PASSWORD;
const ssh = new SSHClient();
const port = process.env.PROXMOX_SSH_PORT ? Number(process.env.PROXMOX_SSH_PORT) : 22;
ssh.on('ready', () => {
ssh.shell((err: Error | undefined, stream: any) => {
if (err) {
ws.send('Ошибка запуска shell: ' + err.message);
ws.close();
ssh.end();
return;
}
ws.on('message', (msg: string | Buffer) => {
stream.write(msg.toString());
});
stream.on('data', (data: Buffer) => {
ws.send(data.toString());
});
stream.on('close', () => {
ws.close();
ssh.end();
});
});
}).connect({
host,
port,
username,
password,
hostVerifier: (hash: string) => {
console.log('SSH fingerprint:', hash);
return true; // всегда принимаем fingerprint
}
});
ws.on('close', () => {
ssh.end();
});
});
server.on('upgrade', (request: IncomingMessage, socket: any, head: Buffer) => {
if (request.url?.startsWith('/api/server/') && request.url?.endsWith('/console')) {
wss.handleUpgrade(request, socket, head, (ws: WebSocket) => {
wss.emit('connection', ws, request);
});
}
});
}

View File

@@ -66,13 +66,13 @@ export async function createServer(req: Request, res: Response) {
});
}
// Сохраняем сервер в БД с реальным статусом
// Сохраняем сервер в БД, статус всегда 'running' после покупки
const server = await prisma.server.create({
data: {
userId,
tariffId,
osId,
status: result.containerStatus || 'creating',
status: 'running',
proxmoxId: Number(result.vmid),
ipAddress: result.ipAddress,
rootPassword: result.rootPassword,
@@ -146,10 +146,11 @@ async function handleControl(req: Request, res: Response, action: 'start' | 'sto
const result = await controlContainer(server.proxmoxId, action);
// Polling статуса VM после управления
let newStatus = server.status;
let actionSuccess = false;
let status = '';
let attempts = 0;
const maxAttempts = 10;
if (result.status === 'success') {
let status = '';
let attempts = 0;
const maxAttempts = 10;
while (attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 3000));
const stats = await getContainerStats(server.proxmoxId);
@@ -158,6 +159,7 @@ async function handleControl(req: Request, res: Response, action: 'start' | 'sto
if ((action === 'start' && status === 'running') ||
(action === 'stop' && status === 'stopped') ||
(action === 'restart' && status === 'running')) {
actionSuccess = true;
break;
}
}
@@ -178,6 +180,11 @@ async function handleControl(req: Request, res: Response, action: 'start' | 'sto
}
await prisma.server.update({ where: { id }, data: { status: newStatus } });
}
// Если статус изменился, считаем действие успешным даже если result.status !== 'success'
if (newStatus !== server.status) {
return res.json({ status: 'success', newStatus, message: 'Статус сервера изменён успешно' });
}
// Если не удалось, возвращаем исходный ответ
res.json({ ...result, status: newStatus });
} catch (error: any) {
res.status(500).json({ error: error?.message || 'Ошибка управления сервером' });
@@ -208,7 +215,9 @@ export async function changeRootPassword(req: Request, res: Response) {
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 proxmoxChangeRootPassword(server.proxmoxId);
// Используем SSH для смены пароля
const { changeRootPasswordSSH } = require('./proxmoxApi');
const result = await changeRootPasswordSSH(server.proxmoxId);
if (result?.status === 'success' && result.password) {
await prisma.server.update({ where: { id }, data: { rootPassword: result.password } });
}

View File

@@ -1,10 +1,10 @@
import { Router } from 'express';
import { PrismaClient } from '@prisma/client';
const router = Router();
const prisma = new PrismaClient();
// GET /api/tariff — получить все тарифы
router.get('/', async (req, res) => {
try {
const tariffs = await prisma.tariff.findMany();