начата логика создания сервера

This commit is contained in:
Georgiy Syralev
2025-09-18 17:49:06 +03:00
parent cce9e7b996
commit f254597b1a
11 changed files with 440 additions and 234 deletions

View File

@@ -0,0 +1,6 @@
import { checkProxmoxConnection } from './modules/server/proxmoxApi';
(async () => {
const result = await checkProxmoxConnection();
console.log('Проверка соединения с Proxmox:', result);
})();

View File

@@ -29,10 +29,23 @@ app.use((req, res, next) => {
next(); next();
}); });
app.get('/', (req, res) => { import { checkProxmoxConnection } from './modules/server/proxmoxApi';
res.json({
app.get('/', async (req, res) => {
// Проверка соединения с Proxmox
let proxmoxStatus;
try {
proxmoxStatus = await checkProxmoxConnection();
} catch (err) {
proxmoxStatus = { status: 'fail', message: 'Ошибка проверки Proxmox', error: err };
}
res.json({
message: 'Сервер ospab.host запущен!', message: 'Сервер ospab.host запущен!',
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
port: PORT,
database: process.env.DATABASE_URL ? 'подключена' : 'НЕ НАСТРОЕНА',
proxmox: proxmoxStatus
}); });
}); });

View File

@@ -0,0 +1,60 @@
import axios from 'axios';
import dotenv from 'dotenv';
dotenv.config();
const PROXMOX_API_URL = process.env.PROXMOX_API_URL;
const PROXMOX_TOKEN_ID = process.env.PROXMOX_TOKEN_ID;
const PROXMOX_TOKEN_SECRET = process.env.PROXMOX_TOKEN_SECRET;
export async function createProxmoxContainer({ os, tariff, user }: any) {
try {
const node = process.env.PROXMOX_NODE;
const diskTemplate = process.env.PROXMOX_DISK_TEMPLATE;
if (!node || !diskTemplate) {
return { status: 'fail', message: 'Не указаны PROXMOX_NODE или PROXMOX_DISK_TEMPLATE в .env' };
}
const vmId = Math.floor(10000 + Math.random() * 89999);
const res = await axios.post(
`${PROXMOX_API_URL}/nodes/${node}/qemu`,
{
vmid: vmId,
name: `user${user.id}-vm${vmId}`,
ostype: os.code || 'l26',
cores: tariff.cores || 2,
memory: tariff.memory || 2048,
storage: diskTemplate,
},
{
headers: {
'Authorization': `PVEAPIToken=${PROXMOX_TOKEN_ID}=${PROXMOX_TOKEN_SECRET}`,
'Content-Type': 'application/json'
}
}
);
if (res.data && res.data.data) {
return { status: 'ok', proxmoxId: vmId, message: 'Сервер создан на Proxmox', proxmox: res.data.data };
}
return { status: 'fail', message: 'Не удалось создать сервер на Proxmox', details: res.data };
} catch (err) {
return { status: 'fail', message: 'Ошибка создания сервера на Proxmox', error: err };
}
}
export async function checkProxmoxConnection() {
try {
const res = await axios.get(
`${PROXMOX_API_URL}/version`,
{
headers: {
'Authorization': `PVEAPIToken=${PROXMOX_TOKEN_ID}=${PROXMOX_TOKEN_SECRET}`
}
}
);
if (res.data && res.data.data) {
return { status: 'ok', message: 'Соединение с Proxmox установлено', version: res.data.data.version };
}
return { status: 'fail', message: 'Не удалось получить версию Proxmox' };
} catch (err) {
return { status: 'fail', message: 'Ошибка соединения с Proxmox', error: err };
}
}

View File

@@ -1,16 +1,23 @@
import { Router } from 'express'; import { Router } from 'express';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
// import { createProxmoxContainer } from '../../proxmox/proxmoxApi'; // если есть интеграция import { authMiddleware } from '../auth/auth.middleware';
import { checkProxmoxConnection, createProxmoxContainer } from './proxmoxApi';
const router = Router(); const router = Router();
const prisma = new PrismaClient(); const prisma = new PrismaClient();
// POST /api/server/create — создать сервер (контейнер) router.use(authMiddleware);
router.get('/proxmox-status', async (req, res) => {
const status = await checkProxmoxConnection();
res.json(status);
});
router.post('/create', async (req, res) => { router.post('/create', async (req, res) => {
try { try {
const { tariffId, osId } = req.body; const { tariffId, osId } = req.body;
// TODO: получить userId из авторизации (req.user) // Получаем userId из авторизации
const userId = 1; // временно, заменить на реального пользователя const userId = req.user?.id;
if (!userId) return res.status(401).json({ error: 'Нет авторизации' });
// Получаем тариф и ОС // Получаем тариф и ОС
const tariff = await prisma.tariff.findUnique({ where: { id: tariffId } }); const tariff = await prisma.tariff.findUnique({ where: { id: tariffId } });
@@ -19,30 +26,52 @@ router.post('/create', async (req, res) => {
return res.status(400).json({ error: 'Тариф или ОС не найдены' }); return res.status(400).json({ error: 'Тариф или ОС не найдены' });
} }
// TODO: интеграция с Proxmox для создания контейнера // Проверяем баланс пользователя
// Если интеграция с Proxmox есть, то только при успешном создании контейнера создавать запись в БД const user = await prisma.user.findUnique({ where: { id: userId } });
// Например: if (!user) return res.status(404).json({ error: 'Пользователь не найден' });
// let proxmoxResult; if (user.balance < tariff.price) {
// try { return res.status(400).json({ error: 'Недостаточно средств на балансе' });
// proxmoxResult = await createProxmoxContainer({ ... }); }
// } catch (proxmoxErr) {
// console.error('Ошибка Proxmox:', proxmoxErr);
// return res.status(500).json({ error: 'Ошибка создания контейнера на Proxmox' });
// }
// Если всё успешно — создаём запись сервера в БД // 1. Создаём сервер на Proxmox
let proxmoxResult;
try {
proxmoxResult = await createProxmoxContainer({ os, tariff, user });
} catch (proxmoxErr) {
console.error('Ошибка Proxmox:', proxmoxErr);
return res.status(500).json({ error: 'Ошибка создания сервера на Proxmox', details: proxmoxErr });
}
if (!proxmoxResult || proxmoxResult.status !== 'ok') {
return res.status(500).json({ error: 'Сервер не создан на Proxmox', details: proxmoxResult });
}
// 2. Списываем средства
await prisma.user.update({
where: { id: userId },
data: {
balance: {
decrement: tariff.price
}
}
});
// 3. Создаём запись о сервере в БД
const node = process.env.PROXMOX_NODE;
const diskTemplate = process.env.PROXMOX_DISK_TEMPLATE;
const server = await prisma.server.create({ const server = await prisma.server.create({
data: { data: {
userId, userId,
tariffId, tariffId,
osId, osId,
status: 'creating', status: 'active',
node,
diskTemplate,
proxmoxId: proxmoxResult.proxmoxId || null,
}, },
}); });
res.json({ success: true, server }); res.json({ success: true, server });
} catch (err) { } catch (err) {
console.error('Ошибка создания сервера:', err); console.error('Ошибка создания сервера:', err);
// Не создавать сервер, если есть ошибка
return res.status(500).json({ error: 'Ошибка создания сервера' }); return res.status(500).json({ error: 'Ошибка создания сервера' });
} }
}); });
@@ -67,4 +96,22 @@ router.get('/', async (req, res) => {
} }
}); });
// GET /api/server/:id — получить один сервер пользователя по id
router.get('/:id', async (req, res) => {
try {
// TODO: получить userId из авторизации (req.user)
const userId = 1; // временно
const serverId = Number(req.params.id);
const server = await prisma.server.findFirst({
where: { id: serverId, userId },
include: { os: true, tariff: true },
});
if (!server) return res.status(404).json({ error: 'Сервер не найден' });
res.json(server);
} catch (err) {
console.error('Ошибка получения сервера:', err);
res.status(500).json({ error: 'Ошибка получения сервера' });
}
});
export default router; export default router;

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

View File

@@ -60,16 +60,42 @@ const Checkout: React.FC<CheckoutProps> = ({ onSuccess }) => {
setLoading(true); setLoading(true);
setError(''); setError('');
try { try {
const token = localStorage.getItem('access_token') || localStorage.getItem('token');
console.log('Покупка сервера:', { tariffId: selectedTariff, osId: selectedOs }); console.log('Покупка сервера:', { tariffId: selectedTariff, osId: selectedOs });
const res = await axios.post('http://localhost:5000/api/server/create', { const res = await axios.post('http://localhost:5000/api/server/create', {
tariffId: selectedTariff, tariffId: selectedTariff,
osId: selectedOs, osId: selectedOs,
}, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
withCredentials: true,
}); });
console.log('Ответ сервера:', res.data); if (res.data && res.data.error === 'Недостаточно средств на балансе') {
setError('Недостаточно средств на балансе. Пополните баланс и попробуйте снова.');
setLoading(false);
return;
}
// После успешной покупки обновляем userData
try {
const userRes = await axios.get('http://localhost:5000/api/auth/me', { headers: token ? { Authorization: `Bearer ${token}` } : {} });
window.dispatchEvent(new CustomEvent('userDataUpdate', {
detail: {
user: userRes.data.user,
balance: userRes.data.user.balance ?? 0,
servers: userRes.data.user.servers ?? [],
tickets: userRes.data.user.tickets ?? [],
}
}));
} catch (err) {
console.error('Ошибка обновления userData после покупки:', err);
}
onSuccess(); onSuccess();
} catch (err) { } catch (err) {
if (axios.isAxiosError(err) && err.response?.data?.error === 'Недостаточно средств на балансе') {
setError('Недостаточно средств на балансе. Пополните баланс и попробуйте снова.');
} else {
setError('Ошибка покупки сервера');
}
console.error('Ошибка покупки сервера:', err); console.error('Ошибка покупки сервера:', err);
setError('Ошибка покупки сервера');
} }
setLoading(false); setLoading(false);
}; };

View File

@@ -7,7 +7,8 @@ import { useContext } from 'react';
// Импортируем компоненты для вкладок // Импортируем компоненты для вкладок
import Summary from './summary'; import Summary from './summary';
import ServerManagementPage from './servermanagement'; import Servers from './servers';
import ServerPanel from './serverpanel';
import TicketsPage from './tickets'; import TicketsPage from './tickets';
import Billing from './billing'; import Billing from './billing';
import Settings from './settings'; import Settings from './settings';
@@ -109,11 +110,23 @@ const Dashboard = () => {
); );
} }
// Вкладки для сайдбара
const tabs = [
{ key: 'summary', label: 'Сводка', to: '/dashboard' },
{ key: 'servers', label: 'Серверы', to: '/dashboard/servers' },
{ key: 'tickets', label: 'Тикеты', to: '/dashboard/tickets' },
{ key: 'billing', label: 'Баланс', to: '/dashboard/billing' },
{ key: 'settings', label: 'Настройки', to: '/dashboard/settings' },
];
const adminTabs = [
{ key: 'checkverification', label: 'Проверка чеков', to: '/dashboard/checkverification' },
{ key: 'ticketresponse', label: 'Ответы на тикеты', to: '/dashboard/ticketresponse' },
];
return ( return (
<div className="flex min-h-screen bg-gray-50"> <div className="flex min-h-screen bg-gray-50">
{/* Sidebar - фиксированный слева */} {/* Sidebar */}
<div className="w-64 bg-white shadow-xl flex flex-col"> <div className="w-64 bg-white shadow-xl flex flex-col">
{/* Заголовок сайдбара */}
<div className="p-6 border-b border-gray-200"> <div className="p-6 border-b border-gray-200">
<h2 className="text-xl font-bold text-gray-800"> <h2 className="text-xl font-bold text-gray-800">
Привет, {userData?.user?.username || 'Гость'}! Привет, {userData?.user?.username || 'Гость'}!
@@ -127,122 +140,68 @@ const Dashboard = () => {
Баланс: <span className="font-semibold text-ospab-primary">{userData?.balance ?? 0}</span> Баланс: <span className="font-semibold text-ospab-primary">{userData?.balance ?? 0}</span>
</div> </div>
</div> </div>
{/* Навигация */}
<nav className="flex-1 p-6"> <nav className="flex-1 p-6">
<div className="space-y-1"> <div className="space-y-1">
<Link {tabs.map(tab => (
to="/dashboard" <Link
className={`flex items-center py-3 px-4 rounded-xl font-semibold transition-colors duration-200 ${ key={tab.key}
activeTab === 'summary' ? 'bg-ospab-primary text-white shadow-lg' : 'text-gray-600 hover:bg-gray-100' to={tab.to}
}`} className={`flex items-center py-3 px-4 rounded-xl font-semibold transition-colors duration-200 ${
> activeTab === tab.key ? 'bg-ospab-primary text-white shadow-lg' : 'text-gray-600 hover:bg-gray-100'
Сводка }`}
</Link> >
<Link {tab.label}
to="/dashboard/servers" </Link>
className={`flex items-center py-3 px-4 rounded-xl font-semibold transition-colors duration-200 ${ ))}
activeTab === 'servers' ? 'bg-ospab-primary text-white shadow-lg' : 'text-gray-600 hover:bg-gray-100'
}`}
>
Серверы
</Link>
<Link
to="/dashboard/tickets"
className={`flex items-center py-3 px-4 rounded-xl font-semibold transition-colors duration-200 ${
activeTab === 'tickets' ? 'bg-ospab-primary text-white shadow-lg' : 'text-gray-600 hover:bg-gray-100'
}`}
>
Тикеты
</Link>
<Link
to="/dashboard/billing"
className={`flex items-center py-3 px-4 rounded-xl font-semibold transition-colors duration-200 ${
activeTab === 'billing' ? 'bg-ospab-primary text-white shadow-lg' : 'text-gray-600 hover:bg-gray-100'
}`}
>
Пополнить баланс
</Link>
<Link
to="/dashboard/settings"
className={`flex items-center py-3 px-4 rounded-xl font-semibold transition-colors duration-200 ${
activeTab === 'settings' ? 'bg-ospab-primary text-white shadow-lg' : 'text-gray-600 hover:bg-gray-100'
}`}
>
Настройки
</Link>
</div> </div>
{isOperator && ( {isOperator && (
<div className="mt-8 pt-6 border-t border-gray-200"> <div className="mt-8 pt-6 border-t border-gray-200">
<p className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3 px-4"> <p className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3 px-4">
Админ панель Админ панель
</p> </p>
<div className="space-y-1"> <div className="space-y-1">
<Link {adminTabs.map(tab => (
to="/dashboard/checkverification" <Link
className={`flex items-center py-3 px-4 rounded-xl font-semibold transition-colors duration-200 ${ key={tab.key}
activeTab === 'checkverification' ? 'bg-ospab-primary text-white shadow-lg' : 'text-gray-600 hover:bg-gray-100' to={tab.to}
}`} className={`flex items-center py-3 px-4 rounded-xl font-semibold transition-colors duration-200 ${
> activeTab === tab.key ? 'bg-ospab-primary text-white shadow-lg' : 'text-gray-600 hover:bg-gray-100'
Проверка чеков }`}
</Link> >
<Link {tab.label}
to="/dashboard/ticketresponse" </Link>
className={`flex items-center py-3 px-4 rounded-xl font-semibold transition-colors duration-200 ${ ))}
activeTab === 'ticketresponse' ? 'bg-ospab-primary text-white shadow-lg' : 'text-gray-600 hover:bg-gray-100'
}`}
>
Ответы на тикеты
</Link>
</div> </div>
</div> </div>
)} )}
</nav> </nav>
<div className="p-6 border-t border-gray-200 text-xs text-gray-500 text-center">
{/* Футер сайдбара */} <p>&copy; 2025 ospab.host</p>
<div className="p-6 border-t border-gray-200"> <p className="mt-1">Версия 1.0.0</p>
<div className="text-xs text-gray-500 text-center">
<p>&copy; 2024 ospab.host</p>
<p className="mt-1">Версия 1.0.0</p>
</div>
</div> </div>
</div> </div>
{/* Main Content - занимает оставшееся место */} {/* Main Content */}
<div className="flex-1 flex flex-col"> <div className="flex-1 flex flex-col">
{/* Хлебные крошки/заголовок */}
<div className="bg-white border-b border-gray-200 px-8 py-4"> <div className="bg-white border-b border-gray-200 px-8 py-4">
<div className="flex items-center"> <h1 className="text-2xl font-bold text-gray-900 capitalize">
<div> {tabs.concat(adminTabs).find(t => t.key === activeTab)?.label || 'Панель управления'}
<h1 className="text-2xl font-bold text-gray-900 capitalize"> </h1>
{activeTab === 'summary' ? 'Сводка' : <p className="text-sm text-gray-600 mt-1">
activeTab === 'servers' ? 'Серверы' : {new Date().toLocaleDateString('ru-RU', {
activeTab === 'tickets' ? 'Тикеты поддержки' : weekday: 'long',
activeTab === 'billing' ? 'Пополнение баланса' : year: 'numeric',
activeTab === 'settings' ? 'Настройки аккаунта' : month: 'long',
activeTab === 'checkverification' ? 'Проверка чеков' : day: 'numeric',
activeTab === 'ticketresponse' ? 'Ответы на тикеты' : })}
'Панель управления'} </p>
</h1>
<p className="text-sm text-gray-600 mt-1">
{new Date().toLocaleDateString('ru-RU', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</p>
</div>
</div>
</div> </div>
{/* Контент страницы */}
<div className="flex-1 p-8"> <div className="flex-1 p-8">
<Routes> <Routes>
<Route path="/" element={<Summary userData={userData ?? { user: { username: '', operator: 0 }, balance: 0, servers: [], tickets: [] }} />} /> <Route path="/" element={<Summary userData={userData ?? { user: { username: '', operator: 0 }, balance: 0, servers: [], tickets: [] }} />} />
<Route path="servers" element={<ServerManagementPage />} /> <Route path="servers" element={<Servers />} />
<Route path="checkout" element={<Checkout onSuccess={() => window.location.reload()} />} /> <Route path="server/:id" element={<ServerPanel />} />
<Route path="checkout" element={<Checkout onSuccess={() => navigate('/dashboard/servers')} />} />
<Route path="tariffs" element={<TariffsPage />} /> <Route path="tariffs" element={<TariffsPage />} />
{userData && ( {userData && (
<Route path="tickets" element={<TicketsPage setUserData={setUserData} />} /> <Route path="tickets" element={<TicketsPage setUserData={setUserData} />} />
@@ -251,7 +210,6 @@ const Dashboard = () => {
<Route path="billing" element={<Billing />} /> <Route path="billing" element={<Billing />} />
)} )}
<Route path="settings" element={<Settings />} /> <Route path="settings" element={<Settings />} />
{isOperator && ( {isOperator && (
<> <>
<Route path="checkverification" element={<CheckVerification />} /> <Route path="checkverification" element={<CheckVerification />} />

View File

@@ -1,96 +0,0 @@
import { useEffect, useState } from 'react';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
import useAuth from '../../context/useAuth';
interface Server {
id: number;
status: string;
createdAt: string;
updatedAt: string;
tariff: { name: string; price: number };
os: { name: string; type: string };
}
const ServerManagementPage = () => {
const [servers, setServers] = useState<Server[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const { isLoggedIn } = useAuth();
const navigate = useNavigate();
useEffect(() => {
if (!isLoggedIn) {
navigate('/login');
return;
}
const fetchServers = async () => {
try {
const token = localStorage.getItem('access_token');
const res = await axios.get('http://localhost:5000/api/server', {
headers: { Authorization: `Bearer ${token}` },
});
setServers(res.data);
} catch {
setError('Ошибка загрузки серверов');
setServers([]);
}
setLoading(false);
};
fetchServers();
}, [isLoggedIn, navigate]);
// TODO: добавить управление сервером (включить, выключить, перезагрузить, переустановить ОС)
try {
return (
<div className="min-h-screen bg-gray-50 py-20">
<div className="container mx-auto px-4">
<h1 className="text-4xl font-bold text-center mb-10 text-gray-900">Управление серверами</h1>
{loading ? (
<p className="text-lg text-gray-500 text-center">Загрузка...</p>
) : error ? (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-center">
<p className="text-lg text-red-500">{error}</p>
<button className="mt-4 px-4 py-2 rounded bg-ospab-primary text-white font-bold" onClick={() => window.location.reload()}>Перезагрузить</button>
</div>
) : servers.length === 0 ? (
<p className="text-lg text-gray-500 text-center">У вас нет серверов.</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{servers.map(server => (
<div key={server.id} className="bg-white p-8 rounded-2xl shadow-xl flex flex-col gap-4">
<div>
<h2 className="text-2xl font-bold text-gray-800 mb-2">{server.tariff.name}</h2>
<p className="text-lg text-gray-600">ОС: {server.os.name} ({server.os.type})</p>
<p className="text-lg text-gray-600">Статус: <span className="font-bold">{server.status}</span></p>
<p className="text-sm text-gray-400">Создан: {new Date(server.createdAt).toLocaleString()}</p>
</div>
{/* TODO: Кнопки управления сервером */}
<div className="flex gap-3 mt-4">
<button className="px-4 py-2 rounded bg-ospab-primary text-white font-bold hover:bg-ospab-accent">Включить</button>
<button className="px-4 py-2 rounded bg-gray-200 text-gray-800 font-bold hover:bg-gray-300">Выключить</button>
<button className="px-4 py-2 rounded bg-gray-200 text-gray-800 font-bold hover:bg-gray-300">Перезагрузить</button>
<button className="px-4 py-2 rounded bg-gray-200 text-gray-800 font-bold hover:bg-gray-300">Переустановить ОС</button>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
} catch {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="bg-white p-8 rounded-2xl shadow-xl text-center">
<h2 className="text-2xl font-bold text-red-600 mb-4">Ошибка отображения страницы</h2>
<p className="text-gray-700 mb-4">Произошла критическая ошибка. Попробуйте перезагрузить страницу.</p>
<button className="px-4 py-2 rounded bg-ospab-primary text-white font-bold" onClick={() => window.location.reload()}>Перезагрузить</button>
</div>
</div>
);
}
};
export default ServerManagementPage;

View File

@@ -0,0 +1,169 @@
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import axios, { AxiosError } from 'axios';
interface Server {
id: number;
status: string;
createdAt: string;
updatedAt: string;
os: { name: string; type: string };
tariff: { name: string; price: number };
ip?: string;
rootPassword?: string;
}
const TABS = [
{ key: 'overview', label: 'Обзор' },
{ key: 'console', label: 'Консоль' },
{ key: 'stats', label: 'Статистика' },
{ key: 'manage', label: 'Управление' },
{ key: 'security', label: 'Безопасность' },
];
const generatePassword = () => {
return Math.random().toString(36).slice(-10) + Math.random().toString(36).slice(-6);
};
const ServerPanel: React.FC = () => {
const { id } = useParams();
const [server, setServer] = useState<Server | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [activeTab, setActiveTab] = useState('overview');
const [newRoot, setNewRoot] = useState<string | null>(null);
const [showRoot, setShowRoot] = useState(false);
useEffect(() => {
const fetchServer = async () => {
try {
const token = localStorage.getItem('access_token');
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const res = await axios.get(`http://localhost:5000/api/server/${id}`, { headers });
setServer(res.data);
} catch (err) {
const error = err as AxiosError;
if (error?.response?.status === 404) {
setError('Сервер не найден или был удалён.');
} else {
setError('Ошибка загрузки данных сервера');
}
console.error('Ошибка загрузки данных сервера:', err);
} finally {
setLoading(false);
}
};
fetchServer();
}, [id]);
// Генерация root-пароля (только для копирования)
const handleGenerateRoot = () => {
try {
const password = generatePassword();
setNewRoot(password);
setShowRoot(true);
// TODO: отправить новый пароль на backend для смены
} catch (err) {
console.error('Ошибка генерации root-пароля:', err);
}
};
// Базовые действия (заглушки)
const handleAction = async (action: 'start' | 'stop' | 'restart') => {
alert(`Выполнено действие: ${action} (реализовать вызов к backend)`);
// TODO: реализовать вызов к backend
};
if (loading) {
return <div className="flex min-h-screen items-center justify-center"><span className="text-gray-500 text-lg">Загрузка...</span></div>;
}
if (error) {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-50">
<div className="bg-white rounded-3xl shadow-xl p-10 max-w-xl w-full flex flex-col items-center">
<span className="text-red-500 text-xl font-bold mb-4">{error}</span>
<button
className="bg-ospab-primary text-white px-6 py-3 rounded-full font-bold hover:bg-ospab-primary-dark transition"
onClick={() => window.location.href = '/dashboard/servers'}
>
Вернуться к списку серверов
</button>
</div>
</div>
);
}
if (!server) {
return <div className="flex min-h-screen items-center justify-center"><span className="text-red-500 text-lg">Сервер не найден</span></div>;
}
return (
<div className="min-h-screen flex flex-col items-center bg-gray-50">
<div className="bg-white rounded-3xl shadow-xl p-10 max-w-3xl w-full mt-10">
<h1 className="text-3xl font-bold text-gray-800 mb-6">Панель управления сервером #{server.id}</h1>
<div className="flex gap-4 mb-8">
{TABS.map(tab => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`px-5 py-2 rounded-xl font-semibold transition-colors duration-200 ${activeTab === tab.key ? 'bg-ospab-primary text-white shadow-lg' : 'text-gray-600 hover:bg-gray-100'}`}
>
{tab.label}
</button>
))}
</div>
{activeTab === 'overview' && (
<div className="space-y-4">
<div className="text-lg text-gray-800 font-bold">Статус: <span className="font-normal">{server.status}</span></div>
<div className="text-lg text-gray-800 font-bold">Тариф: <span className="font-normal">{server.tariff.name} ({server.tariff.price})</span></div>
<div className="text-lg text-gray-800 font-bold">ОС: <span className="font-normal">{server.os.name} ({server.os.type})</span></div>
<div className="text-lg text-gray-800 font-bold">IP: <span className="font-normal">{server.ip || '—'}</span></div>
<div className="text-lg text-gray-800 font-bold">Создан: <span className="font-normal">{new Date(server.createdAt).toLocaleString()}</span></div>
<div className="text-lg text-gray-800 font-bold">Обновлён: <span className="font-normal">{new Date(server.updatedAt).toLocaleString()}</span></div>
</div>
)}
{activeTab === 'console' && (
<div className="bg-gray-100 rounded-xl p-6 text-gray-700 font-mono text-sm">
<div className="mb-2 font-bold">Консоль сервера (заглушка)</div>
<div className="bg-black text-green-400 p-4 rounded-lg">root@{server.ip || 'server'}:~# _</div>
</div>
)}
{activeTab === 'stats' && (
<div className="bg-gray-100 rounded-xl p-6">
<div className="mb-2 font-bold">Графики нагрузки (заглушка)</div>
<div className="flex gap-6">
<div className="w-1/2 h-32 bg-white rounded-lg shadow-inner flex items-center justify-center text-gray-400">CPU</div>
<div className="w-1/2 h-32 bg-white rounded-lg shadow-inner flex items-center justify-center text-gray-400">RAM</div>
</div>
</div>
)}
{activeTab === 'manage' && (
<div className="flex gap-6">
<button className="bg-green-500 hover:bg-green-600 text-white px-6 py-3 rounded-full font-bold" onClick={() => handleAction('start')}>Запустить</button>
<button className="bg-yellow-500 hover:bg-yellow-600 text-white px-6 py-3 rounded-full font-bold" onClick={() => handleAction('restart')}>Перезагрузить</button>
<button className="bg-red-500 hover:bg-red-600 text-white px-6 py-3 rounded-full font-bold" onClick={() => handleAction('stop')}>Остановить</button>
</div>
)}
{activeTab === 'security' && (
<div className="space-y-4">
<button className="bg-ospab-primary text-white px-6 py-3 rounded-full font-bold" onClick={handleGenerateRoot}>Сгенерировать новый root-пароль</button>
{showRoot && newRoot && (
<div className="bg-gray-100 rounded-xl p-6 flex flex-col items-center">
<div className="mb-2 font-bold text-lg">Ваш новый root-пароль:</div>
<div className="font-mono text-xl bg-white px-6 py-3 rounded-lg shadow-inner select-all">{newRoot}</div>
<div className="text-gray-500 mt-2">Скопируйте пароль он будет показан только один раз!</div>
</div>
)}
</div>
)}
</div>
</div>
);
};
export default ServerPanel;

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import axios from 'axios'; import axios from 'axios';
import { Link } from 'react-router-dom';
interface Server { interface Server {
id: number; id: number;
@@ -18,7 +19,9 @@ const Servers: React.FC = () => {
useEffect(() => { useEffect(() => {
const fetchServers = async () => { const fetchServers = async () => {
try { try {
const res = await axios.get('http://localhost:5000/api/server'); const token = localStorage.getItem('access_token');
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const res = await axios.get('http://localhost:5000/api/server', { headers });
console.log('Ответ API серверов:', res.data); console.log('Ответ API серверов:', res.data);
// Защита от получения HTML вместо JSON // Защита от получения HTML вместо JSON
if (typeof res.data === 'string' && res.data.startsWith('<!doctype html')) { if (typeof res.data === 'string' && res.data.startsWith('<!doctype html')) {
@@ -43,8 +46,7 @@ const Servers: React.FC = () => {
return ( return (
<div className="p-8 bg-white rounded-3xl shadow-xl max-w-4xl mx-auto"> <div className="p-8 bg-white rounded-3xl shadow-xl max-w-4xl mx-auto">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h2 className="text-3xl font-bold text-gray-800">Серверы</h2> <h2 className="text-3xl font-bold text-gray-800">Мои серверы</h2>
{/* Кнопка 'Купить сервер' только если серверов нет */}
{servers.length === 0 && !loading && !error && ( {servers.length === 0 && !loading && !error && (
<a href="/tariffs" className="bg-ospab-primary text-white px-4 py-2 rounded font-bold hover:bg-ospab-primary-dark transition">Купить сервер</a> <a href="/tariffs" className="bg-ospab-primary text-white px-4 py-2 rounded font-bold hover:bg-ospab-primary-dark transition">Купить сервер</a>
)} )}
@@ -62,10 +64,28 @@ const Servers: React.FC = () => {
<a href="/tariffs" className="inline-block bg-ospab-primary text-white px-6 py-3 rounded-full font-bold hover:bg-ospab-primary-dark transition">Посмотреть тарифы</a> <a href="/tariffs" className="inline-block bg-ospab-primary text-white px-6 py-3 rounded-full font-bold hover:bg-ospab-primary-dark transition">Посмотреть тарифы</a>
</div> </div>
) : ( ) : (
<div className="text-center"> <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<a href="/dashboard/servermanagement" className="bg-ospab-primary text-white px-6 py-3 rounded-full font-bold hover:bg-ospab-primary-dark transition">Перейти к управлению серверами</a> {servers.map(server => (
<div key={server.id} className="bg-white p-8 rounded-2xl shadow-xl flex flex-col gap-4">
<div>
<h2 className="text-2xl font-bold text-gray-800 mb-2">{server.tariff.name}</h2>
<p className="text-lg text-gray-600">ОС: {server.os.name} ({server.os.type})</p>
<p className="text-lg text-gray-600">Статус: <span className="font-bold">{server.status}</span></p>
<p className="text-sm text-gray-400">Создан: {new Date(server.createdAt).toLocaleString()}</p>
<p className="text-sm text-gray-400">Обновлён: {new Date(server.updatedAt).toLocaleString()}</p>
</div>
<div>
<Link
to={`/dashboard/server/${server.id}`}
className="bg-ospab-primary text-white px-6 py-3 rounded-full font-bold hover:bg-ospab-primary-dark transition"
>
Перейти в панель управления
</Link>
</div>
</div>
))}
</div> </div>
)} )}
</div> </div>
); );
}; };

View File

@@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom'; import { Link, useNavigate, useLocation } from 'react-router-dom';
import axios from 'axios'; import axios from 'axios';
import useAuth from '../context/useAuth'; import useAuth from '../context/useAuth';
@@ -10,27 +10,30 @@ const LoginPage = () => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { login } = useAuth(); const { login, isLoggedIn } = useAuth();
// Если уже авторизован — редирект на dashboard
useEffect(() => {
if (isLoggedIn) {
navigate('/dashboard', { replace: true });
}
}, [isLoggedIn, navigate]);
const handleLogin = async (e: React.FormEvent) => { const handleLogin = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setError(''); setError('');
setIsLoading(true); setIsLoading(true);
try { try {
const response = await axios.post('http://localhost:5000/api/auth/login', { const response = await axios.post('http://localhost:5000/api/auth/login', {
email: email, email: email,
password: password, password: password,
}); });
login(response.data.token);
localStorage.setItem('token', response.data.token); // Возврат на исходную страницу, если был редирект
login(response.data.token); type LocationState = { from?: { pathname?: string } };
// Возврат на исходную страницу, если был редирект const state = location.state as LocationState | null;
type LocationState = { from?: { pathname?: string } }; const from = state?.from?.pathname || '/dashboard';
const state = location.state as LocationState | null; navigate(from);
const from = state?.from?.pathname || '/dashboard';
navigate(from);
} catch (err) { } catch (err) {
if (axios.isAxiosError(err) && err.response) { if (axios.isAxiosError(err) && err.response) {
setError(err.response.data.message || 'Неизвестная ошибка входа.'); setError(err.response.data.message || 'Неизвестная ошибка входа.');