начата логика создания сервера
This commit is contained in:
@@ -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