diff --git a/ospabhost/backend/src/checkProxmoxConnection.ts b/ospabhost/backend/src/checkProxmoxConnection.ts new file mode 100644 index 0000000..da730e2 --- /dev/null +++ b/ospabhost/backend/src/checkProxmoxConnection.ts @@ -0,0 +1,6 @@ +import { checkProxmoxConnection } from './modules/server/proxmoxApi'; + +(async () => { + const result = await checkProxmoxConnection(); + console.log('Проверка соединения с Proxmox:', result); +})(); diff --git a/ospabhost/backend/src/index.ts b/ospabhost/backend/src/index.ts index db3cc97..06262c8 100644 --- a/ospabhost/backend/src/index.ts +++ b/ospabhost/backend/src/index.ts @@ -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 }); }); diff --git a/ospabhost/backend/src/modules/server/proxmoxApi.ts b/ospabhost/backend/src/modules/server/proxmoxApi.ts new file mode 100644 index 0000000..5c0539d --- /dev/null +++ b/ospabhost/backend/src/modules/server/proxmoxApi.ts @@ -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 }; + } +} diff --git a/ospabhost/backend/src/modules/server/server.routes.ts b/ospabhost/backend/src/modules/server/server.routes.ts index e132e5b..6420a2b 100644 --- a/ospabhost/backend/src/modules/server/server.routes.ts +++ b/ospabhost/backend/src/modules/server/server.routes.ts @@ -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; diff --git a/ospabhost/backend/uploads/checks/1758206338893-666161055-logo.jpg b/ospabhost/backend/uploads/checks/1758206338893-666161055-logo.jpg new file mode 100644 index 0000000..30fd29c Binary files /dev/null and b/ospabhost/backend/uploads/checks/1758206338893-666161055-logo.jpg differ diff --git a/ospabhost/frontend/src/pages/dashboard/checkout.tsx b/ospabhost/frontend/src/pages/dashboard/checkout.tsx index 71de89d..68b6419 100644 --- a/ospabhost/frontend/src/pages/dashboard/checkout.tsx +++ b/ospabhost/frontend/src/pages/dashboard/checkout.tsx @@ -60,16 +60,42 @@ const Checkout: React.FC = ({ 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); }; diff --git a/ospabhost/frontend/src/pages/dashboard/mainpage.tsx b/ospabhost/frontend/src/pages/dashboard/mainpage.tsx index e16b758..fd22051 100644 --- a/ospabhost/frontend/src/pages/dashboard/mainpage.tsx +++ b/ospabhost/frontend/src/pages/dashboard/mainpage.tsx @@ -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 (
- {/* Sidebar - фиксированный слева */} + {/* Sidebar */}
- {/* Заголовок сайдбара */}

Привет, {userData?.user?.username || 'Гость'}! @@ -127,122 +140,68 @@ const Dashboard = () => { Баланс: ₽{userData?.balance ?? 0}

- - {/* Навигация */} - - {/* Футер сайдбара */} -
-
-

© 2024 ospab.host

-

Версия 1.0.0

-
+
+

© 2025 ospab.host

+

Версия 1.0.0

- {/* Main Content - занимает оставшееся место */} + {/* Main Content */}
- {/* Хлебные крошки/заголовок */}
-
-
-

- {activeTab === 'summary' ? 'Сводка' : - activeTab === 'servers' ? 'Серверы' : - activeTab === 'tickets' ? 'Тикеты поддержки' : - activeTab === 'billing' ? 'Пополнение баланса' : - activeTab === 'settings' ? 'Настройки аккаунта' : - activeTab === 'checkverification' ? 'Проверка чеков' : - activeTab === 'ticketresponse' ? 'Ответы на тикеты' : - 'Панель управления'} -

-

- {new Date().toLocaleDateString('ru-RU', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric' - })} -

-
-
+

+ {tabs.concat(adminTabs).find(t => t.key === activeTab)?.label || 'Панель управления'} +

+

+ {new Date().toLocaleDateString('ru-RU', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + })} +

- - {/* Контент страницы */}
} /> - } /> - window.location.reload()} />} /> + } /> + } /> + navigate('/dashboard/servers')} />} /> } /> {userData && ( } /> @@ -251,7 +210,6 @@ const Dashboard = () => { } /> )} } /> - {isOperator && ( <> } /> diff --git a/ospabhost/frontend/src/pages/dashboard/servermanagement.tsx b/ospabhost/frontend/src/pages/dashboard/servermanagement.tsx deleted file mode 100644 index c6b3fbb..0000000 --- a/ospabhost/frontend/src/pages/dashboard/servermanagement.tsx +++ /dev/null @@ -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([]); - 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 ( -
-
-

Управление серверами

- {loading ? ( -

Загрузка...

- ) : error ? ( -
-

{error}

- -
- ) : servers.length === 0 ? ( -

У вас нет серверов.

- ) : ( -
- {servers.map(server => ( -
-
-

{server.tariff.name}

-

ОС: {server.os.name} ({server.os.type})

-

Статус: {server.status}

-

Создан: {new Date(server.createdAt).toLocaleString()}

-
- {/* TODO: Кнопки управления сервером */} -
- - - - -
-
- ))} -
- )} -
-
- ); - } catch { - return ( -
-
-

Ошибка отображения страницы

-

Произошла критическая ошибка. Попробуйте перезагрузить страницу.

- -
-
- ); - } -}; - -export default ServerManagementPage; diff --git a/ospabhost/frontend/src/pages/dashboard/serverpanel.tsx b/ospabhost/frontend/src/pages/dashboard/serverpanel.tsx new file mode 100644 index 0000000..c029d97 --- /dev/null +++ b/ospabhost/frontend/src/pages/dashboard/serverpanel.tsx @@ -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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [activeTab, setActiveTab] = useState('overview'); + const [newRoot, setNewRoot] = useState(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
Загрузка...
; + } + if (error) { + return ( +
+
+ {error} + +
+
+ ); + } + if (!server) { + return
Сервер не найден
; + } + + return ( +
+
+

Панель управления сервером #{server.id}

+
+ {TABS.map(tab => ( + + ))} +
+ + {activeTab === 'overview' && ( +
+
Статус: {server.status}
+
Тариф: {server.tariff.name} ({server.tariff.price}₽)
+
ОС: {server.os.name} ({server.os.type})
+
IP: {server.ip || '—'}
+
Создан: {new Date(server.createdAt).toLocaleString()}
+
Обновлён: {new Date(server.updatedAt).toLocaleString()}
+
+ )} + + {activeTab === 'console' && ( +
+
Консоль сервера (заглушка)
+
root@{server.ip || 'server'}:~# _
+
+ )} + + {activeTab === 'stats' && ( +
+
Графики нагрузки (заглушка)
+
+
CPU
+
RAM
+
+
+ )} + + {activeTab === 'manage' && ( +
+ + + +
+ )} + + {activeTab === 'security' && ( +
+ + {showRoot && newRoot && ( +
+
Ваш новый root-пароль:
+
{newRoot}
+
Скопируйте пароль — он будет показан только один раз!
+
+ )} +
+ )} +
+
+ ); +}; + +export default ServerPanel; diff --git a/ospabhost/frontend/src/pages/dashboard/servers.tsx b/ospabhost/frontend/src/pages/dashboard/servers.tsx index 97a3d95..fa1084c 100644 --- a/ospabhost/frontend/src/pages/dashboard/servers.tsx +++ b/ospabhost/frontend/src/pages/dashboard/servers.tsx @@ -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(' { return (
-

Серверы

- {/* Кнопка 'Купить сервер' только если серверов нет */} +

Мои серверы

{servers.length === 0 && !loading && !error && ( Купить сервер )} @@ -62,10 +64,28 @@ const Servers: React.FC = () => { Посмотреть тарифы
) : ( -
- Перейти к управлению серверами +
+ {servers.map(server => ( +
+
+

{server.tariff.name}

+

ОС: {server.os.name} ({server.os.type})

+

Статус: {server.status}

+

Создан: {new Date(server.createdAt).toLocaleString()}

+

Обновлён: {new Date(server.updatedAt).toLocaleString()}

+
+
+ + Перейти в панель управления + +
+
+ ))}
- )} + )}
); }; diff --git a/ospabhost/frontend/src/pages/login.tsx b/ospabhost/frontend/src/pages/login.tsx index bcdea45..3039f23 100644 --- a/ospabhost/frontend/src/pages/login.tsx +++ b/ospabhost/frontend/src/pages/login.tsx @@ -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 || 'Неизвестная ошибка входа.');