Сделана логика создания вм на сервере, управления есть. Начаты уведомления

This commit is contained in:
Georgiy Syralev
2025-09-20 20:53:13 +03:00
parent 66f1c6fd62
commit 07f3eab020
20 changed files with 1001 additions and 192 deletions

View File

@@ -32,7 +32,7 @@ const Billing = () => {
if (!checkFile || amount <= 0) return;
setUploadLoading(true);
try {
const token = localStorage.getItem('token');
const token = localStorage.getItem('access_token');
const formData = new FormData();
formData.append('file', checkFile);
formData.append('amount', String(amount));

View File

@@ -31,7 +31,7 @@ const CheckVerification: React.FC = () => {
setLoading(true);
setError('');
try {
const token = localStorage.getItem('token');
const token = localStorage.getItem('access_token');
const res = await axios.get<ICheck[]>(API_URL, {
headers: { Authorization: `Bearer ${token}` },
withCredentials: true,
@@ -50,17 +50,17 @@ const CheckVerification: React.FC = () => {
setActionLoading(checkId);
setError('');
try {
const token = localStorage.getItem('token');
const token = localStorage.getItem('access_token');
await axios.post(`${API_URL}/${action}`, { checkId }, {
headers: { Authorization: `Bearer ${token}` },
headers: { Authorization: `Bearer ${token}` },
withCredentials: true,
});
setChecks((prevChecks: ICheck[]) => prevChecks.map((c: ICheck) => c.id === checkId ? { ...c, status: action === 'approve' ? 'approved' : 'rejected' } : c));
// Если подтверждение — обновить баланс пользователя
if (action === 'approve') {
try {
const userToken = localStorage.getItem('access_token') || token;
const headers = { Authorization: `Bearer ${userToken}` };
const token = localStorage.getItem('access_token');
const headers = { Authorization: `Bearer ${token}` };
const userRes = await axios.get('http://localhost:5000/api/auth/me', { headers });
// Глобально обновить userData через типизированное событие (для Dashboard)
window.dispatchEvent(new CustomEvent<import('./types').UserData>('userDataUpdate', {

View File

@@ -12,6 +12,7 @@ import ServerPanel from './serverpanel';
import TicketsPage from './tickets';
import Billing from './billing';
import Settings from './settings';
import Notifications from './notificatons';
import CheckVerification from './checkverification';
import TicketResponse from './ticketresponse';
import Checkout from './checkout';
@@ -117,6 +118,7 @@ const Dashboard = () => {
{ key: 'tickets', label: 'Тикеты', to: '/dashboard/tickets' },
{ key: 'billing', label: 'Баланс', to: '/dashboard/billing' },
{ key: 'settings', label: 'Настройки', to: '/dashboard/settings' },
{ key: 'notifications', label: 'Уведомления', to: '/dashboard/notifications' },
];
const adminTabs = [
{ key: 'checkverification', label: 'Проверка чеков', to: '/dashboard/checkverification' },
@@ -210,6 +212,7 @@ const Dashboard = () => {
<Route path="billing" element={<Billing />} />
)}
<Route path="settings" element={<Settings />} />
<Route path="notifications" element={<Notifications />} />
{isOperator && (
<>
<Route path="checkverification" element={<CheckVerification />} />

View File

@@ -0,0 +1,41 @@
const notificationsList = [
{
title: "Создание сервера",
description: "Вы получите уведомление при успешном создании нового сервера или контейнера.",
},
{
title: "Списание оплаты за месяц",
description: "Напоминание о предстоящем списании средств за продление тарифа.",
},
{
title: "Истечение срока действия тарифа",
description: "Уведомление о необходимости продлить тариф, чтобы избежать отключения.",
},
{
title: "Ответ на тикет",
description: "Вы получите уведомление, когда оператор ответит на ваш тикет поддержки.",
},
{
title: "Поступление оплаты",
description: "Уведомление о зачислении средств на ваш баланс.",
},
];
const Notifications = () => {
return (
<div className="p-8 bg-white rounded-3xl shadow-xl max-w-xl mx-auto mt-6">
<h2 className="text-2xl font-bold mb-6">Типы уведомлений</h2>
<ul className="space-y-6">
{notificationsList.map((n, idx) => (
<li key={idx} className="bg-gray-50 border border-gray-200 rounded-xl p-4">
<div className="font-semibold text-lg text-ospab-primary mb-1">{n.title}</div>
<div className="text-gray-700 text-sm">{n.description}</div>
</li>
))}
</ul>
<div className="text-gray-400 text-sm mt-8">Настройка каналов уведомлений (email, Telegram) появится позже.</div>
</div>
);
};
export default Notifications;

View File

@@ -1,5 +1,52 @@
import React, { useEffect, useState } from 'react';
// Встроенная секция консоли
function ConsoleSection({ serverId }: { serverId: number }) {
const [consoleUrl, setConsoleUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleOpenConsole = async () => {
setLoading(true);
setError('');
try {
const token = localStorage.getItem('access_token');
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const res = await axios.post(`http://localhost:5000/api/proxmox/console`, { vmid: serverId }, { headers });
if (res.data?.status === 'success' && res.data.url) {
setConsoleUrl(res.data.url);
} else {
setError('Ошибка открытия консоли');
}
} catch {
setError('Ошибка открытия консоли');
} finally {
setLoading(false);
}
};
return (
<div className="bg-gray-100 rounded-xl p-6 text-gray-700 font-mono text-sm flex flex-col items-center">
<div className="mb-2 font-bold">Консоль сервера</div>
{!consoleUrl ? (
<button
className="bg-ospab-primary text-white px-6 py-3 rounded-full font-bold mb-4"
onClick={handleOpenConsole}
disabled={loading}
>{loading ? 'Открытие...' : 'Открыть noVNC консоль'}</button>
) : (
<iframe
src={consoleUrl}
title="noVNC Console"
className="w-full h-[600px] rounded-lg border"
allowFullScreen
/>
)}
{error && <div className="text-red-500 text-base font-semibold text-center mt-2">{error}</div>}
</div>
);
}
import { useParams } from 'react-router-dom';
import axios, { AxiosError } from 'axios';
@@ -14,6 +61,13 @@ interface Server {
rootPassword?: string;
}
interface ServerStats {
data?: {
cpu?: number;
memory?: { usage?: number };
};
}
const TABS = [
{ key: 'overview', label: 'Обзор' },
{ key: 'console', label: 'Консоль' },
@@ -22,9 +76,6 @@ const TABS = [
{ 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();
@@ -34,6 +85,7 @@ const ServerPanel: React.FC = () => {
const [activeTab, setActiveTab] = useState('overview');
const [newRoot, setNewRoot] = useState<string | null>(null);
const [showRoot, setShowRoot] = useState(false);
const [stats, setStats] = useState<ServerStats | null>(null);
useEffect(() => {
const fetchServer = async () => {
@@ -42,6 +94,9 @@ const ServerPanel: React.FC = () => {
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const res = await axios.get(`http://localhost:5000/api/server/${id}`, { headers });
setServer(res.data);
// Получаем статистику
const statsRes = await axios.get(`http://localhost:5000/api/server/${id}/status`, { headers });
setStats(statsRes.data.stats);
} catch (err) {
const error = err as AxiosError;
if (error?.response?.status === 404) {
@@ -57,22 +112,47 @@ const ServerPanel: React.FC = () => {
fetchServer();
}, [id]);
// Генерация root-пароля (только для копирования)
const handleGenerateRoot = () => {
// Смена root-пароля через backend
const handleGenerateRoot = async () => {
try {
const password = generatePassword();
setNewRoot(password);
setShowRoot(true);
// TODO: отправить новый пароль на backend для смены
const token = localStorage.getItem('access_token');
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const res = await axios.post(`http://localhost:5000/api/server/${id}/password`, {}, { headers });
if (res.data?.status === 'success' && res.data.password) {
setNewRoot(res.data.password);
setShowRoot(true);
} else {
setError('Ошибка смены root-пароля');
}
} catch (err) {
console.error('Ошибка генерации root-пароля:', err);
setError('Ошибка смены root-пароля');
console.error('Ошибка смены root-пароля:', err);
}
};
// Базовые действия (заглушки)
// Реальные действия управления сервером
const handleAction = async (action: 'start' | 'stop' | 'restart') => {
alert(`Выполнено действие: ${action} (реализовать вызов к backend)`);
// TODO: реализовать вызов к backend
try {
setLoading(true);
setError('');
const token = localStorage.getItem('access_token');
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const res = await axios.post(`http://localhost:5000/api/server/${id}/${action}`, {}, { headers });
if (res.data?.status === 'success') {
// Обновить статус сервера и статистику после действия
const updated = await axios.get(`http://localhost:5000/api/server/${id}`, { headers });
setServer(updated.data);
const statsRes = await axios.get(`http://localhost:5000/api/server/${id}/status`, { headers });
setStats(statsRes.data.stats);
} else {
setError(`Ошибка: ${res.data?.message || 'Не удалось выполнить действие'}`);
}
} catch (err) {
setError('Ошибка управления сервером');
console.error('Ошибка управления сервером:', err);
} finally {
setLoading(false);
}
};
if (loading) {
@@ -124,19 +204,23 @@ const ServerPanel: React.FC = () => {
</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>
<ConsoleSection serverId={server.id} />
)}
{activeTab === 'stats' && (
<div className="bg-gray-100 rounded-xl p-6">
<div className="mb-2 font-bold">Графики нагрузки (заглушка)</div>
<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 className="w-1/2 h-32 bg-white rounded-lg shadow-inner flex flex-col items-center justify-center">
<div className="font-bold text-gray-700">CPU</div>
<div className="text-2xl text-ospab-primary">{stats?.data?.cpu ? (stats.data.cpu * 100).toFixed(1) : '—'}%</div>
</div>
<div className="w-1/2 h-32 bg-white rounded-lg shadow-inner flex flex-col items-center justify-center">
<div className="font-bold text-gray-700">RAM</div>
<div className="text-2xl text-ospab-primary">{stats?.data?.memory?.usage ? stats.data.memory.usage.toFixed(1) : '—'}%</div>
</div>
</div>
</div>
)}

View File

@@ -1,10 +1,53 @@
import React, { useState } from "react";
const Settings = () => {
const [tab, setTab] = useState<'email' | 'password'>('email');
const [email, setEmail] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
// TODO: получить email и username из API
return (
<div className="p-8 bg-white rounded-3xl shadow-xl">
<h2 className="text-3xl font-bold text-gray-800 mb-6">Настройки аккаунта</h2>
<p className="text-lg text-gray-500">
Здесь вы сможете изменить свои личные данные, email и пароль.
</p>
<div className="p-8 bg-white rounded-3xl shadow-xl max-w-xl mx-auto mt-6">
<h2 className="text-2xl font-bold mb-4">Настройки аккаунта</h2>
<div className="flex space-x-4 mb-6">
<button
type="button"
className={`px-4 py-2 rounded-lg font-semibold ${tab === 'email' ? 'bg-ospab-primary text-white' : 'bg-gray-100 text-gray-700'}`}
onClick={() => setTab('email')}
>
Смена email
</button>
<button
type="button"
className={`px-4 py-2 rounded-lg font-semibold ${tab === 'password' ? 'bg-ospab-primary text-white' : 'bg-gray-100 text-gray-700'}`}
onClick={() => setTab('password')}
>
Смена пароля
</button>
</div>
{tab === 'email' ? (
<form className="space-y-6">
<div>
<label className="block text-gray-700 mb-2">Email</label>
<input type="email" value={email} onChange={e => setEmail(e.target.value)} className="w-full px-4 py-2 border rounded-lg bg-gray-100" />
</div>
<div>
<label className="block text-gray-700 mb-2">Имя пользователя</label>
<input type="text" value={username} onChange={e => setUsername(e.target.value)} className="w-full px-4 py-2 border rounded-lg bg-gray-100" />
</div>
<button type="button" className="bg-ospab-primary text-white px-6 py-2 rounded-lg font-bold">Сохранить email</button>
</form>
) : (
<form className="space-y-6">
<div>
<label className="block text-gray-700 mb-2">Новый пароль</label>
<input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="Новый пароль" className="w-full px-4 py-2 border rounded-lg" />
</div>
<button type="button" className="bg-ospab-primary text-white px-6 py-2 rounded-lg font-bold">Сохранить пароль</button>
</form>
)}
</div>
);
};

View File

@@ -31,10 +31,10 @@ const TicketResponse: React.FC = () => {
const fetchTickets = async () => {
setError('');
try {
const token = localStorage.getItem('token');
const token = localStorage.getItem('access_token');
const res = await axios.get('http://localhost:5000/api/ticket', {
withCredentials: true,
headers: token ? { Authorization: `Bearer ${token}` } : {}
headers: token ? { Authorization: `Bearer ${token}` } : {}
});
if (Array.isArray(res.data)) {
setTickets(res.data);
@@ -51,13 +51,13 @@ const TicketResponse: React.FC = () => {
setLoading(true);
setError('');
try {
const token = localStorage.getItem('token');
const token = localStorage.getItem('access_token');
await axios.post('http://localhost:5000/api/ticket/respond', {
ticketId,
message: responseMsg[ticketId]
}, {
withCredentials: true,
headers: token ? { Authorization: `Bearer ${token}` } : {}
headers: token ? { Authorization: `Bearer ${token}` } : {}
});
setResponseMsg(prev => ({ ...prev, [ticketId]: '' }));
fetchTickets();
@@ -73,10 +73,10 @@ const TicketResponse: React.FC = () => {
setLoading(true);
setError('');
try {
const token = localStorage.getItem('token');
const token = localStorage.getItem('access_token');
await axios.post('http://localhost:5000/api/ticket/close', { ticketId }, {
withCredentials: true,
headers: token ? { Authorization: `Bearer ${token}` } : {}
headers: token ? { Authorization: `Bearer ${token}` } : {}
});
fetchTickets();
} catch {

View File

@@ -38,7 +38,7 @@ const TicketsPage: React.FC<TicketsPageProps> = ({ setUserData }) => {
const fetchTickets = async () => {
try {
const token = localStorage.getItem('token');
const token = localStorage.getItem('access_token');
const res = await axios.get('http://localhost:5000/api/ticket', {
withCredentials: true,
headers: token ? { Authorization: `Bearer ${token}` } : {}
@@ -55,7 +55,7 @@ const TicketsPage: React.FC<TicketsPageProps> = ({ setUserData }) => {
const updateUserData = async () => {
try {
const token = localStorage.getItem('access_token') || localStorage.getItem('token');
const token = localStorage.getItem('access_token');
if (!token) return;
const headers = { Authorization: `Bearer ${token}` };
const userRes = await axios.get('http://localhost:5000/api/auth/me', { headers });
@@ -80,7 +80,7 @@ const TicketsPage: React.FC<TicketsPageProps> = ({ setUserData }) => {
}
setLoading(true);
try {
const token = localStorage.getItem('token');
const token = localStorage.getItem('access_token');
await axios.post('http://localhost:5000/api/ticket/create', { title, message }, {
withCredentials: true,
headers: token ? { Authorization: `Bearer ${token}` } : {}
@@ -98,7 +98,7 @@ const TicketsPage: React.FC<TicketsPageProps> = ({ setUserData }) => {
};
const respondTicket = async (ticketId: number) => {
const token = localStorage.getItem('token');
const token = localStorage.getItem('access_token');
await axios.post('http://localhost:5000/api/ticket/respond', { ticketId, message: responseMsg }, {
withCredentials: true,
headers: token ? { Authorization: `Bearer ${token}` } : {}
@@ -109,7 +109,7 @@ const TicketsPage: React.FC<TicketsPageProps> = ({ setUserData }) => {
};
const closeTicket = async (ticketId: number) => {
const token = localStorage.getItem('token');
const token = localStorage.getItem('access_token');
await axios.post('http://localhost:5000/api/ticket/close', { ticketId }, {
withCredentials: true,
headers: token ? { Authorization: `Bearer ${token}` } : {}