начата логика создания сервера
This commit is contained in:
6
ospabhost/backend/src/checkProxmoxConnection.ts
Normal file
6
ospabhost/backend/src/checkProxmoxConnection.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { checkProxmoxConnection } from './modules/server/proxmoxApi';
|
||||
|
||||
(async () => {
|
||||
const result = await checkProxmoxConnection();
|
||||
console.log('Проверка соединения с Proxmox:', result);
|
||||
})();
|
||||
@@ -29,10 +29,23 @@ app.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.json({
|
||||
import { checkProxmoxConnection } from './modules/server/proxmoxApi';
|
||||
|
||||
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 запущен!',
|
||||
timestamp: new Date().toISOString()
|
||||
timestamp: new Date().toISOString(),
|
||||
port: PORT,
|
||||
database: process.env.DATABASE_URL ? 'подключена' : 'НЕ НАСТРОЕНА',
|
||||
proxmox: proxmoxStatus
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
60
ospabhost/backend/src/modules/server/proxmoxApi.ts
Normal file
60
ospabhost/backend/src/modules/server/proxmoxApi.ts
Normal 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 };
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,23 @@
|
||||
import { Router } from 'express';
|
||||
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 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) => {
|
||||
try {
|
||||
const { tariffId, osId } = req.body;
|
||||
// TODO: получить userId из авторизации (req.user)
|
||||
const userId = 1; // временно, заменить на реального пользователя
|
||||
// Получаем userId из авторизации
|
||||
const userId = req.user?.id;
|
||||
if (!userId) return res.status(401).json({ error: 'Нет авторизации' });
|
||||
|
||||
// Получаем тариф и ОС
|
||||
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: 'Тариф или ОС не найдены' });
|
||||
}
|
||||
|
||||
// TODO: интеграция с Proxmox для создания контейнера
|
||||
// Если интеграция с Proxmox есть, то только при успешном создании контейнера создавать запись в БД
|
||||
// Например:
|
||||
// let proxmoxResult;
|
||||
// try {
|
||||
// proxmoxResult = await createProxmoxContainer({ ... });
|
||||
// } catch (proxmoxErr) {
|
||||
// console.error('Ошибка Proxmox:', proxmoxErr);
|
||||
// return res.status(500).json({ error: 'Ошибка создания контейнера на Proxmox' });
|
||||
// }
|
||||
// Проверяем баланс пользователя
|
||||
const user = await prisma.user.findUnique({ where: { id: userId } });
|
||||
if (!user) return res.status(404).json({ error: 'Пользователь не найден' });
|
||||
if (user.balance < tariff.price) {
|
||||
return res.status(400).json({ error: 'Недостаточно средств на балансе' });
|
||||
}
|
||||
|
||||
// Если всё успешно — создаём запись сервера в БД
|
||||
// 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({
|
||||
data: {
|
||||
userId,
|
||||
tariffId,
|
||||
osId,
|
||||
status: 'creating',
|
||||
status: 'active',
|
||||
node,
|
||||
diskTemplate,
|
||||
proxmoxId: proxmoxResult.proxmoxId || null,
|
||||
},
|
||||
});
|
||||
res.json({ success: true, server });
|
||||
} catch (err) {
|
||||
console.error('Ошибка создания сервера:', err);
|
||||
// Не создавать сервер, если есть ошибка
|
||||
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;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 209 KiB |
@@ -60,16 +60,42 @@ const Checkout: React.FC<CheckoutProps> = ({ onSuccess }) => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const token = localStorage.getItem('access_token') || localStorage.getItem('token');
|
||||
console.log('Покупка сервера:', { tariffId: selectedTariff, osId: selectedOs });
|
||||
const res = await axios.post('http://localhost:5000/api/server/create', {
|
||||
tariffId: selectedTariff,
|
||||
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();
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err) && err.response?.data?.error === 'Недостаточно средств на балансе') {
|
||||
setError('Недостаточно средств на балансе. Пополните баланс и попробуйте снова.');
|
||||
} else {
|
||||
setError('Ошибка покупки сервера');
|
||||
}
|
||||
console.error('Ошибка покупки сервера:', err);
|
||||
setError('Ошибка покупки сервера');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,8 @@ import { useContext } from 'react';
|
||||
|
||||
// Импортируем компоненты для вкладок
|
||||
import Summary from './summary';
|
||||
import ServerManagementPage from './servermanagement';
|
||||
import Servers from './servers';
|
||||
import ServerPanel from './serverpanel';
|
||||
import TicketsPage from './tickets';
|
||||
import Billing from './billing';
|
||||
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 (
|
||||
<div className="flex min-h-screen bg-gray-50">
|
||||
{/* Sidebar - фиксированный слева */}
|
||||
{/* Sidebar */}
|
||||
<div className="w-64 bg-white shadow-xl flex flex-col">
|
||||
{/* Заголовок сайдбара */}
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
<h2 className="text-xl font-bold text-gray-800">
|
||||
Привет, {userData?.user?.username || 'Гость'}!
|
||||
@@ -127,122 +140,68 @@ const Dashboard = () => {
|
||||
Баланс: <span className="font-semibold text-ospab-primary">₽{userData?.balance ?? 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Навигация */}
|
||||
<nav className="flex-1 p-6">
|
||||
<div className="space-y-1">
|
||||
<Link
|
||||
to="/dashboard"
|
||||
className={`flex items-center py-3 px-4 rounded-xl font-semibold transition-colors duration-200 ${
|
||||
activeTab === 'summary' ? 'bg-ospab-primary text-white shadow-lg' : 'text-gray-600 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
Сводка
|
||||
</Link>
|
||||
<Link
|
||||
to="/dashboard/servers"
|
||||
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>
|
||||
{tabs.map(tab => (
|
||||
<Link
|
||||
key={tab.key}
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isOperator && (
|
||||
<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>
|
||||
<div className="space-y-1">
|
||||
<Link
|
||||
to="/dashboard/checkverification"
|
||||
className={`flex items-center py-3 px-4 rounded-xl font-semibold transition-colors duration-200 ${
|
||||
activeTab === 'checkverification' ? 'bg-ospab-primary text-white shadow-lg' : 'text-gray-600 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
Проверка чеков
|
||||
</Link>
|
||||
<Link
|
||||
to="/dashboard/ticketresponse"
|
||||
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>
|
||||
{adminTabs.map(tab => (
|
||||
<Link
|
||||
key={tab.key}
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* Футер сайдбара */}
|
||||
<div className="p-6 border-t border-gray-200">
|
||||
<div className="text-xs text-gray-500 text-center">
|
||||
<p>© 2024 ospab.host</p>
|
||||
<p className="mt-1">Версия 1.0.0</p>
|
||||
</div>
|
||||
<div className="p-6 border-t border-gray-200 text-xs text-gray-500 text-center">
|
||||
<p>© 2025 ospab.host</p>
|
||||
<p className="mt-1">Версия 1.0.0</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content - занимает оставшееся место */}
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* Хлебные крошки/заголовок */}
|
||||
<div className="bg-white border-b border-gray-200 px-8 py-4">
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 capitalize">
|
||||
{activeTab === 'summary' ? 'Сводка' :
|
||||
activeTab === 'servers' ? 'Серверы' :
|
||||
activeTab === 'tickets' ? 'Тикеты поддержки' :
|
||||
activeTab === 'billing' ? 'Пополнение баланса' :
|
||||
activeTab === 'settings' ? 'Настройки аккаунта' :
|
||||
activeTab === 'checkverification' ? 'Проверка чеков' :
|
||||
activeTab === 'ticketresponse' ? 'Ответы на тикеты' :
|
||||
'Панель управления'}
|
||||
</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>
|
||||
<h1 className="text-2xl font-bold text-gray-900 capitalize">
|
||||
{tabs.concat(adminTabs).find(t => t.key === activeTab)?.label || 'Панель управления'}
|
||||
</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 className="flex-1 p-8">
|
||||
<Routes>
|
||||
<Route path="/" element={<Summary userData={userData ?? { user: { username: '', operator: 0 }, balance: 0, servers: [], tickets: [] }} />} />
|
||||
<Route path="servers" element={<ServerManagementPage />} />
|
||||
<Route path="checkout" element={<Checkout onSuccess={() => window.location.reload()} />} />
|
||||
<Route path="servers" element={<Servers />} />
|
||||
<Route path="server/:id" element={<ServerPanel />} />
|
||||
<Route path="checkout" element={<Checkout onSuccess={() => navigate('/dashboard/servers')} />} />
|
||||
<Route path="tariffs" element={<TariffsPage />} />
|
||||
{userData && (
|
||||
<Route path="tickets" element={<TicketsPage setUserData={setUserData} />} />
|
||||
@@ -251,7 +210,6 @@ const Dashboard = () => {
|
||||
<Route path="billing" element={<Billing />} />
|
||||
)}
|
||||
<Route path="settings" element={<Settings />} />
|
||||
|
||||
{isOperator && (
|
||||
<>
|
||||
<Route path="checkverification" element={<CheckVerification />} />
|
||||
|
||||
@@ -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;
|
||||
169
ospabhost/frontend/src/pages/dashboard/serverpanel.tsx
Normal file
169
ospabhost/frontend/src/pages/dashboard/serverpanel.tsx
Normal 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;
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
interface Server {
|
||||
id: number;
|
||||
@@ -18,7 +19,9 @@ const Servers: React.FC = () => {
|
||||
useEffect(() => {
|
||||
const fetchServers = async () => {
|
||||
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);
|
||||
// Защита от получения HTML вместо JSON
|
||||
if (typeof res.data === 'string' && res.data.startsWith('<!doctype html')) {
|
||||
@@ -43,8 +46,7 @@ const Servers: React.FC = () => {
|
||||
return (
|
||||
<div className="p-8 bg-white rounded-3xl shadow-xl max-w-4xl mx-auto">
|
||||
<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 && (
|
||||
<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>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import useAuth from '../context/useAuth';
|
||||
@@ -10,27 +10,30 @@ const LoginPage = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await axios.post('http://localhost:5000/api/auth/login', {
|
||||
email: email,
|
||||
password: password,
|
||||
});
|
||||
|
||||
localStorage.setItem('token', response.data.token);
|
||||
login(response.data.token);
|
||||
// Возврат на исходную страницу, если был редирект
|
||||
type LocationState = { from?: { pathname?: string } };
|
||||
const state = location.state as LocationState | null;
|
||||
const from = state?.from?.pathname || '/dashboard';
|
||||
navigate(from);
|
||||
|
||||
login(response.data.token);
|
||||
// Возврат на исходную страницу, если был редирект
|
||||
type LocationState = { from?: { pathname?: string } };
|
||||
const state = location.state as LocationState | null;
|
||||
const from = state?.from?.pathname || '/dashboard';
|
||||
navigate(from);
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err) && err.response) {
|
||||
setError(err.response.data.message || 'Неизвестная ошибка входа.');
|
||||
|
||||
Reference in New Issue
Block a user