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

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

@@ -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);
};

View File

@@ -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>&copy; 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>&copy; 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 />} />

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 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>
);
};

View File

@@ -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 || 'Неизвестная ошибка входа.');