Сделана логика создания вм на сервере, управления есть. Начаты уведомления
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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 />} />
|
||||
|
||||
41
ospabhost/frontend/src/pages/dashboard/notificatons.tsx
Normal file
41
ospabhost/frontend/src/pages/dashboard/notificatons.tsx
Normal 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;
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}` } : {}
|
||||
|
||||
Reference in New Issue
Block a user