Сделана логика создания вм на сервере, управления есть. Начаты уведомления
This commit is contained in:
59
.github/copilot-instructions.md
vendored
Normal file
59
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Copilot Instructions for Ospabhost 8.1
|
||||||
|
|
||||||
|
## Архитектура проекта
|
||||||
|
- **Монорепозиторий**: две основные части — `backend` (Express, TypeScript, Prisma) и `frontend` (React, Vite, TypeScript).
|
||||||
|
- **Backend**:
|
||||||
|
- Основной сервер: `backend/src/index.ts` — точка входа, маршрутизация, CORS, логирование.
|
||||||
|
- Модули: `backend/src/modules/*` — бизнес-логика по доменам (auth, ticket, check, os, server, tariff).
|
||||||
|
- Интеграция с Proxmox: через API, см. `backend/src/modules/server/proxmoxApi.ts`.
|
||||||
|
- ORM: Prisma, схема — `backend/prisma/schema.prisma`.
|
||||||
|
- Статические файлы чеков: `backend/uploads/checks`.
|
||||||
|
- **Frontend**:
|
||||||
|
- SPA на React + Vite, точка входа: `frontend/src/main.tsx`.
|
||||||
|
- Страницы: `frontend/src/pages/*`, компоненты: `frontend/src/components/*`.
|
||||||
|
- Контекст авторизации: `frontend/src/context/authcontext.tsx`, `useAuth.ts`.
|
||||||
|
|
||||||
|
## Важные паттерны и конвенции
|
||||||
|
- **Маршруты API**: начинаются с `/api/`, см. `backend/src/index.ts`.
|
||||||
|
- **Модули backend**: каждый домен — отдельная папка, экспортирует маршруты и сервисы.
|
||||||
|
- **Работа с Proxmox**: все операции (создание контейнера, управление, статистика) через функции из `proxmoxApi.ts`.
|
||||||
|
- **Статусные поля**: для сущностей (Server, Check, Ticket) используются строковые статусы (`creating`, `running`, `pending`, `open` и т.д.).
|
||||||
|
- **Пароли**: генерируются через `generateSecurePassword` (см. `proxmoxApi.ts`).
|
||||||
|
- **Описание тарифа**: парсится для выделения ресурсов (ядра, RAM, SSD) при создании контейнера.
|
||||||
|
|
||||||
|
## Сборка и запуск
|
||||||
|
- **Backend**:
|
||||||
|
- `npm run dev` — запуск с hot-reload (ts-node-dev).
|
||||||
|
- `npm run build` — компиляция TypeScript.
|
||||||
|
- `npm start` — запуск собранного кода.
|
||||||
|
- **Frontend**:
|
||||||
|
- `npm run dev` — запуск Vite dev server.
|
||||||
|
- `npm run build` — сборка.
|
||||||
|
- `npm run preview` — предпросмотр production-сборки.
|
||||||
|
- `npm run lint` — проверка ESLint.
|
||||||
|
|
||||||
|
## Взаимодействие компонентов
|
||||||
|
- **Frontend ↔ Backend**: через REST API, адреса `/api/*`.
|
||||||
|
- **Backend ↔ Proxmox**: через HTTP API, параметры берутся из `.env`.
|
||||||
|
- **Prisma**: миграции и seed-скрипты — в `backend/prisma/`.
|
||||||
|
|
||||||
|
## Внешние зависимости
|
||||||
|
- **Backend**: express, prisma, axios, bcrypt, jsonwebtoken, multer, dotenv.
|
||||||
|
- **Frontend**: react, react-dom, react-router-dom, tailwindcss, axios.
|
||||||
|
|
||||||
|
## Примеры ключевых файлов
|
||||||
|
- `backend/src/index.ts` — точка входа, маршрутизация.
|
||||||
|
- `backend/src/modules/server/proxmoxApi.ts` — интеграция с Proxmox.
|
||||||
|
- `backend/prisma/schema.prisma` — схема данных.
|
||||||
|
- `frontend/src/pages/*` — страницы SPA.
|
||||||
|
- `frontend/src/context/authcontext.tsx` — авторизация.
|
||||||
|
|
||||||
|
## Особенности
|
||||||
|
- **CORS**: разрешены только локальные адреса для разработки.
|
||||||
|
- **Логирование**: каждый запрос логируется с датой и методом.
|
||||||
|
- **Статические файлы**: чеки доступны по `/uploads/checks`.
|
||||||
|
- **Пароли root**: генерируются и меняются через API Proxmox.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Обновите этот файл при изменении архитектуры или ключевых паттернов. Для уточнения разделов — дайте обратную связь!_
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `server` ADD COLUMN `cpuUsage` DOUBLE NULL DEFAULT 0,
|
||||||
|
ADD COLUMN `diskUsage` DOUBLE NULL DEFAULT 0,
|
||||||
|
ADD COLUMN `ipAddress` VARCHAR(191) NULL,
|
||||||
|
ADD COLUMN `lastPing` DATETIME(3) NULL,
|
||||||
|
ADD COLUMN `macAddress` VARCHAR(191) NULL,
|
||||||
|
ADD COLUMN `memoryUsage` DOUBLE NULL DEFAULT 0,
|
||||||
|
ADD COLUMN `networkIn` DOUBLE NULL DEFAULT 0,
|
||||||
|
ADD COLUMN `networkOut` DOUBLE NULL DEFAULT 0,
|
||||||
|
ADD COLUMN `rootPassword` VARCHAR(191) NULL,
|
||||||
|
ADD COLUMN `sshPublicKey` VARCHAR(191) NULL,
|
||||||
|
MODIFY `status` VARCHAR(191) NOT NULL DEFAULT 'creating';
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `Notification` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`userId` INTEGER NOT NULL,
|
||||||
|
`title` VARCHAR(191) NOT NULL,
|
||||||
|
`message` VARCHAR(191) NOT NULL,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `Notification` ADD CONSTRAINT `Notification_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@@ -33,15 +33,33 @@ model Server {
|
|||||||
userId Int
|
userId Int
|
||||||
tariffId Int
|
tariffId Int
|
||||||
osId Int
|
osId Int
|
||||||
status String @default("stopped")
|
status String @default("creating") // creating, running, stopped, suspended, error
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
user User @relation(fields: [userId], references: [id])
|
user User @relation(fields: [userId], references: [id])
|
||||||
tariff Tariff @relation(fields: [tariffId], references: [id])
|
tariff Tariff @relation(fields: [tariffId], references: [id])
|
||||||
os OperatingSystem @relation(fields: [osId], references: [id])
|
os OperatingSystem @relation(fields: [osId], references: [id])
|
||||||
|
|
||||||
|
// Proxmox данные
|
||||||
node String?
|
node String?
|
||||||
diskTemplate String?
|
diskTemplate String?
|
||||||
proxmoxId Int?
|
proxmoxId Int?
|
||||||
|
|
||||||
|
// Сетевые настройки
|
||||||
|
ipAddress String? // Локальный IP адрес
|
||||||
|
macAddress String? // MAC адрес
|
||||||
|
|
||||||
|
// Доступы
|
||||||
|
rootPassword String? // Зашифрованный root пароль
|
||||||
|
sshPublicKey String? // SSH публичный ключ (опционально)
|
||||||
|
|
||||||
|
// Мониторинг
|
||||||
|
lastPing DateTime?
|
||||||
|
cpuUsage Float? @default(0)
|
||||||
|
memoryUsage Float? @default(0)
|
||||||
|
diskUsage Float? @default(0)
|
||||||
|
networkIn Float? @default(0)
|
||||||
|
networkOut Float? @default(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
@@ -57,6 +75,7 @@ model User {
|
|||||||
checks Check[] @relation("UserChecks")
|
checks Check[] @relation("UserChecks")
|
||||||
balance Float @default(0)
|
balance Float @default(0)
|
||||||
servers Server[]
|
servers Server[]
|
||||||
|
notifications Notification[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Check {
|
model Check {
|
||||||
@@ -110,3 +129,11 @@ model Response {
|
|||||||
ticket Ticket @relation("TicketResponses", fields: [ticketId], references: [id])
|
ticket Ticket @relation("TicketResponses", fields: [ticketId], references: [id])
|
||||||
operator User @relation("OperatorResponses", fields: [operatorId], references: [id])
|
operator User @relation("OperatorResponses", fields: [operatorId], references: [id])
|
||||||
}
|
}
|
||||||
|
model Notification {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
userId Int
|
||||||
|
title String
|
||||||
|
message String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
export type Notification = {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
createdAt: Date;
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { getNotifications } from './notification.service';
|
||||||
|
import { authMiddleware } from '../auth/auth.middleware';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/', authMiddleware, getNotifications);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { prisma } from '../../prisma/client';
|
||||||
|
|
||||||
|
export const getNotifications = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
// @ts-ignore
|
||||||
|
const userId = req.user?.id;
|
||||||
|
if (!userId) return res.status(401).json({ error: 'Нет авторизации' });
|
||||||
|
const notifications = await prisma.notification.findMany({
|
||||||
|
where: { userId },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
|
res.json({ notifications });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: 'Ошибка получения уведомлений' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createNotification = async (userId: number, title: string, message: string) => {
|
||||||
|
return prisma.notification.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,60 +1,369 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import crypto from 'crypto';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const PROXMOX_API_URL = process.env.PROXMOX_API_URL;
|
const PROXMOX_API_URL = process.env.PROXMOX_API_URL;
|
||||||
const PROXMOX_TOKEN_ID = process.env.PROXMOX_TOKEN_ID;
|
const PROXMOX_TOKEN_ID = process.env.PROXMOX_TOKEN_ID;
|
||||||
const PROXMOX_TOKEN_SECRET = process.env.PROXMOX_TOKEN_SECRET;
|
const PROXMOX_TOKEN_SECRET = process.env.PROXMOX_TOKEN_SECRET;
|
||||||
|
const PROXMOX_NODE = process.env.PROXMOX_NODE || 'proxmox';
|
||||||
|
|
||||||
export async function createProxmoxContainer({ os, tariff, user }: any) {
|
function getProxmoxHeaders(): Record<string, string> {
|
||||||
try {
|
return {
|
||||||
const node = process.env.PROXMOX_NODE;
|
'Authorization': `PVEAPIToken=${PROXMOX_TOKEN_ID}=${PROXMOX_TOKEN_SECRET}`,
|
||||||
const diskTemplate = process.env.PROXMOX_DISK_TEMPLATE;
|
'Content-Type': 'application/json'
|
||||||
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() {
|
// Генерация случайного пароля
|
||||||
|
export function generateSecurePassword(length: number = 16): string {
|
||||||
|
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
|
||||||
|
let password = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
password += charset.charAt(Math.floor(Math.random() * charset.length));
|
||||||
|
}
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение следующего доступного VMID
|
||||||
|
export async function getNextVMID(): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(
|
const res = await axios.get(
|
||||||
`${PROXMOX_API_URL}/version`,
|
`${PROXMOX_API_URL}/cluster/nextid`,
|
||||||
{
|
{ headers: getProxmoxHeaders() }
|
||||||
headers: {
|
|
||||||
'Authorization': `PVEAPIToken=${PROXMOX_TOKEN_ID}=${PROXMOX_TOKEN_SECRET}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
if (res.data && res.data.data) {
|
return res.data.data || Math.floor(100 + Math.random() * 899);
|
||||||
return { status: 'ok', message: 'Соединение с Proxmox установлено', version: res.data.data.version };
|
} catch (error) {
|
||||||
}
|
console.error('Ошибка получения VMID:', error);
|
||||||
return { status: 'fail', message: 'Не удалось получить версию Proxmox' };
|
return Math.floor(100 + Math.random() * 899);
|
||||||
} catch (err) {
|
}
|
||||||
return { status: 'fail', message: 'Ошибка соединения с Proxmox', error: err };
|
}
|
||||||
|
|
||||||
|
// Создание LXC контейнера
|
||||||
|
export interface CreateContainerParams {
|
||||||
|
os: { template: string; type: string };
|
||||||
|
tariff: { name: string; price: number; description?: string };
|
||||||
|
user: { id: number; username: string };
|
||||||
|
hostname?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createLXContainer({ os, tariff, user }: CreateContainerParams) {
|
||||||
|
try {
|
||||||
|
const vmid = await getNextVMID();
|
||||||
|
const rootPassword = generateSecurePassword();
|
||||||
|
// Используем hostname из параметров, если есть
|
||||||
|
const hostname = arguments[0].hostname || `user${user.id}-${tariff.name.toLowerCase().replace(/\s/g, '-')}`;
|
||||||
|
|
||||||
|
// Определяем ресурсы по названию тарифа (парсим описание)
|
||||||
|
const description = tariff.description || '1 ядро, 1ГБ RAM, 20ГБ SSD';
|
||||||
|
const cores = parseInt(description.match(/(\d+)\s*ядр/)?.[1] || '1');
|
||||||
|
const memory = parseInt(description.match(/(\d+)ГБ\s*RAM/)?.[1] || '1') * 1024; // в MB
|
||||||
|
const diskSize = parseInt(description.match(/(\d+)ГБ\s*SSD/)?.[1] || '20');
|
||||||
|
|
||||||
|
const containerConfig = {
|
||||||
|
vmid,
|
||||||
|
hostname,
|
||||||
|
password: rootPassword,
|
||||||
|
ostemplate: os.template,
|
||||||
|
cores,
|
||||||
|
memory,
|
||||||
|
rootfs: `local:${diskSize}`,
|
||||||
|
net0: 'name=eth0,bridge=vmbr0,ip=dhcp',
|
||||||
|
unprivileged: 1,
|
||||||
|
start: 1, // Автостарт после создания
|
||||||
|
protection: 0,
|
||||||
|
console: 1,
|
||||||
|
cmode: 'console'
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Создание LXC контейнера с параметрами:', containerConfig);
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc`,
|
||||||
|
containerConfig,
|
||||||
|
{ headers: getProxmoxHeaders() }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data?.data) {
|
||||||
|
// Polling статуса контейнера до running или timeout
|
||||||
|
let status = '';
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 10;
|
||||||
|
while (attempts < maxAttempts) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
|
const info = await getContainerStatus(vmid);
|
||||||
|
status = info?.status || '';
|
||||||
|
if (status === 'running' || status === 'stopped' || status === 'created') break;
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
// Получаем IP адрес контейнера
|
||||||
|
const ipAddress = await getContainerIP(vmid);
|
||||||
|
return {
|
||||||
|
status: 'success',
|
||||||
|
vmid,
|
||||||
|
rootPassword,
|
||||||
|
ipAddress,
|
||||||
|
hostname,
|
||||||
|
taskId: response.data.data,
|
||||||
|
containerStatus: status
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Получить статус контейнера по VMID
|
||||||
|
async function getContainerStatus(vmid: number): Promise<{ status: string }> {
|
||||||
|
try {
|
||||||
|
const res = await axios.get(
|
||||||
|
`${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/status/current`,
|
||||||
|
{ headers: getProxmoxHeaders() }
|
||||||
|
);
|
||||||
|
return { status: res.data.data.status };
|
||||||
|
} catch (error) {
|
||||||
|
return { status: 'error' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Не удалось создать контейнер');
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Ошибка создания LXC контейнера:', error);
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
message: error.response?.data?.errors || error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение IP адреса контейнера
|
||||||
|
export async function getContainerIP(vmid: number): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10000)); // Ждём запуска
|
||||||
|
|
||||||
|
const response = await axios.get(
|
||||||
|
`${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/interfaces`,
|
||||||
|
{ headers: getProxmoxHeaders() }
|
||||||
|
);
|
||||||
|
|
||||||
|
const interfaces = response.data?.data;
|
||||||
|
if (interfaces && interfaces.length > 0) {
|
||||||
|
for (const iface of interfaces) {
|
||||||
|
if (iface.inet && iface.inet !== '127.0.0.1') {
|
||||||
|
return iface.inet.split('/')[0]; // Убираем маску подсети
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения IP:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Управление контейнером (старт/стоп/перезагрузка)
|
||||||
|
export async function controlContainer(vmid: number, action: 'start' | 'stop' | 'restart' | 'suspend' | 'resume') {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/status/${action}`,
|
||||||
|
{},
|
||||||
|
{ headers: getProxmoxHeaders() }
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: 'success',
|
||||||
|
action,
|
||||||
|
taskId: response.data?.data
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`Ошибка ${action} контейнера:`, error);
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
message: error.response?.data?.errors || error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаление контейнера
|
||||||
|
export async function deleteContainer(vmid: number) {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(
|
||||||
|
`${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}`,
|
||||||
|
{ headers: getProxmoxHeaders() }
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: 'success',
|
||||||
|
taskId: response.data?.data
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Ошибка удаления контейнера:', error);
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
message: error.response?.data?.errors || error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение статистики контейнера
|
||||||
|
export async function getContainerStats(vmid: number) {
|
||||||
|
try {
|
||||||
|
// Получаем текущий статус
|
||||||
|
const statusResponse = await axios.get(
|
||||||
|
`${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/status/current`,
|
||||||
|
{ headers: getProxmoxHeaders() }
|
||||||
|
);
|
||||||
|
|
||||||
|
const status = statusResponse.data?.data;
|
||||||
|
|
||||||
|
// Получаем статистику RRD (за последний час)
|
||||||
|
let rrdData = [];
|
||||||
|
let latest: any = {};
|
||||||
|
try {
|
||||||
|
const rrdResponse = await axios.get(
|
||||||
|
`${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/rrd?timeframe=hour`,
|
||||||
|
{ headers: getProxmoxHeaders() }
|
||||||
|
);
|
||||||
|
rrdData = rrdResponse.data?.data || [];
|
||||||
|
latest = rrdData[rrdData.length - 1] || {};
|
||||||
|
} catch (err: any) {
|
||||||
|
// Если ошибка 400, возвращаем пустую статистику, но не ошибку
|
||||||
|
if (err?.response?.status === 400) {
|
||||||
|
return {
|
||||||
|
status: 'success',
|
||||||
|
data: {
|
||||||
|
vmid,
|
||||||
|
status: status?.status || 'unknown',
|
||||||
|
uptime: status?.uptime || 0,
|
||||||
|
cpu: 0,
|
||||||
|
memory: {
|
||||||
|
used: status?.mem || 0,
|
||||||
|
max: status?.maxmem || 0,
|
||||||
|
usage: 0
|
||||||
|
},
|
||||||
|
disk: {
|
||||||
|
used: status?.disk || 0,
|
||||||
|
max: status?.maxdisk || 0,
|
||||||
|
usage: 0
|
||||||
|
},
|
||||||
|
network: {
|
||||||
|
in: 0,
|
||||||
|
out: 0
|
||||||
|
},
|
||||||
|
rrdData: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: 'success',
|
||||||
|
data: {
|
||||||
|
vmid,
|
||||||
|
status: status?.status || 'unknown',
|
||||||
|
uptime: status?.uptime || 0,
|
||||||
|
cpu: latest.cpu || 0,
|
||||||
|
memory: {
|
||||||
|
used: status?.mem || 0,
|
||||||
|
max: status?.maxmem || 0,
|
||||||
|
usage: status?.maxmem ? (status.mem / status.maxmem) * 100 : 0
|
||||||
|
},
|
||||||
|
disk: {
|
||||||
|
used: status?.disk || 0,
|
||||||
|
max: status?.maxdisk || 0,
|
||||||
|
usage: status?.maxdisk ? (status.disk / status.maxdisk) * 100 : 0
|
||||||
|
},
|
||||||
|
network: {
|
||||||
|
in: latest.netin || 0,
|
||||||
|
out: latest.netout || 0
|
||||||
|
},
|
||||||
|
rrdData: rrdData.slice(-60) // Последние 60 точек для графиков
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Ошибка получения статистики:', error);
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
message: error.response?.data?.errors || error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Смена root пароля
|
||||||
|
export async function changeRootPassword(vmid: number): Promise<{ status: string; password?: string; message?: string }> {
|
||||||
|
try {
|
||||||
|
const newPassword = generateSecurePassword();
|
||||||
|
|
||||||
|
// Выполняем команду смены пароля в контейнере
|
||||||
|
const response = await axios.post(
|
||||||
|
`${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/status/exec`,
|
||||||
|
{
|
||||||
|
command: `echo 'root:${newPassword}' | chpasswd`
|
||||||
|
},
|
||||||
|
{ headers: getProxmoxHeaders() }
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 'success',
|
||||||
|
password: newPassword
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Ошибка смены пароля:', error);
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
message: error.response?.data?.errors || error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение ссылки на noVNC консоль
|
||||||
|
export async function getConsoleURL(vmid: number): Promise<{ status: string; url?: string; message?: string }> {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${PROXMOX_API_URL}/nodes/${PROXMOX_NODE}/lxc/${vmid}/vncproxy`,
|
||||||
|
{
|
||||||
|
websocket: 1
|
||||||
|
},
|
||||||
|
{ headers: getProxmoxHeaders() }
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = response.data?.data;
|
||||||
|
if (data?.ticket && data?.port) {
|
||||||
|
const consoleUrl = `${process.env.PROXMOX_WEB_URL}/?console=lxc&vmid=${vmid}&node=${PROXMOX_NODE}&resize=off&ticket=${data.ticket}&port=${data.port}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 'success',
|
||||||
|
url: consoleUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Не удалось получить данные для консоли');
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Ошибка получения консоли:', error);
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
message: error.response?.data?.errors || error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка соединения с Proxmox
|
||||||
|
export async function checkProxmoxConnection() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(
|
||||||
|
`${PROXMOX_API_URL}/version`,
|
||||||
|
{ headers: getProxmoxHeaders() }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data?.data) {
|
||||||
|
return {
|
||||||
|
status: 'success',
|
||||||
|
message: 'Соединение с Proxmox установлено',
|
||||||
|
version: response.data.data.version,
|
||||||
|
node: PROXMOX_NODE
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { status: 'error', message: 'Не удалось получить версию Proxmox' };
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
message: 'Ошибка соединения с Proxmox',
|
||||||
|
error: error.response?.data || error.message
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
214
ospabhost/backend/src/modules/server/server.controller.ts
Normal file
214
ospabhost/backend/src/modules/server/server.controller.ts
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import {
|
||||||
|
createLXContainer,
|
||||||
|
controlContainer,
|
||||||
|
getContainerStats,
|
||||||
|
changeRootPassword as proxmoxChangeRootPassword,
|
||||||
|
deleteContainer
|
||||||
|
} from './proxmoxApi';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
// Создание сервера (контейнера)
|
||||||
|
export async function createServer(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { osId, tariffId } = req.body;
|
||||||
|
const userId = req.user?.id;
|
||||||
|
if (!userId) return res.status(401).json({ error: 'Нет авторизации' });
|
||||||
|
|
||||||
|
const os = await prisma.operatingSystem.findUnique({ where: { id: osId } });
|
||||||
|
const tariff = await prisma.tariff.findUnique({ where: { id: tariffId } });
|
||||||
|
const user = await prisma.user.findUnique({ where: { id: userId } });
|
||||||
|
if (!os || !tariff || !user) return res.status(400).json({ error: 'Некорректные параметры' });
|
||||||
|
|
||||||
|
// Проверка баланса пользователя
|
||||||
|
if (user.balance < tariff.price) {
|
||||||
|
return res.status(400).json({ error: 'Недостаточно средств на балансе' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Списываем средства
|
||||||
|
await prisma.user.update({ where: { id: userId }, data: { balance: { decrement: tariff.price } } });
|
||||||
|
|
||||||
|
// Генерация hostname из email
|
||||||
|
let hostname = user.email.split('@')[0];
|
||||||
|
hostname = hostname.replace(/[^a-zA-Z0-9-]/g, '');
|
||||||
|
if (hostname.length < 3) hostname = `user${userId}`;
|
||||||
|
if (hostname.length > 32) hostname = hostname.slice(0, 32);
|
||||||
|
if (/^[0-9-]/.test(hostname)) hostname = `u${hostname}`;
|
||||||
|
|
||||||
|
// Создаём контейнер в Proxmox
|
||||||
|
const result = await createLXContainer({
|
||||||
|
os: { template: os.template || '', type: os.type },
|
||||||
|
tariff: { name: tariff.name, price: tariff.price, description: tariff.description || undefined },
|
||||||
|
user: { id: user.id, username: user.username },
|
||||||
|
hostname
|
||||||
|
});
|
||||||
|
if (result.status !== 'success') {
|
||||||
|
// Возвращаем деньги обратно, если не удалось создать
|
||||||
|
await prisma.user.update({ where: { id: userId }, data: { balance: { increment: tariff.price } } });
|
||||||
|
// Логируем полный текст ошибки в файл
|
||||||
|
const fs = require('fs');
|
||||||
|
const logMsg = `[${new Date().toISOString()}] Ошибка Proxmox: ${JSON.stringify(result, null, 2)}\n`;
|
||||||
|
fs.appendFile('proxmox-errors.log', logMsg, (err: NodeJS.ErrnoException | null) => {
|
||||||
|
if (err) console.error('Ошибка записи лога:', err);
|
||||||
|
});
|
||||||
|
console.error('Ошибка Proxmox:', result.message);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: 'Ошибка создания сервера в Proxmox',
|
||||||
|
details: result.message,
|
||||||
|
fullError: result
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем сервер в БД с реальным статусом
|
||||||
|
const server = await prisma.server.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
tariffId,
|
||||||
|
osId,
|
||||||
|
status: result.containerStatus || 'creating',
|
||||||
|
proxmoxId: Number(result.vmid),
|
||||||
|
ipAddress: result.ipAddress,
|
||||||
|
rootPassword: result.rootPassword,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
res.json(server);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Ошибка покупки сервера:', error);
|
||||||
|
res.status(500).json({ error: error?.message || 'Ошибка покупки сервера' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получить статус сервера
|
||||||
|
export async function getServerStatus(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
const server = await prisma.server.findUnique({ where: { id } });
|
||||||
|
if (!server) return res.status(404).json({ error: 'Сервер не найден' });
|
||||||
|
if (!server.proxmoxId) return res.status(400).json({ error: 'Нет VMID Proxmox' });
|
||||||
|
const stats = await getContainerStats(server.proxmoxId);
|
||||||
|
if (stats.status === 'error') {
|
||||||
|
// Если контейнер не найден в Proxmox, возвращаем статус deleted и пустую статистику
|
||||||
|
return res.json({
|
||||||
|
...server,
|
||||||
|
status: 'deleted',
|
||||||
|
stats: {
|
||||||
|
data: {
|
||||||
|
cpu: 0,
|
||||||
|
memory: { usage: 0 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: 'Контейнер не найден в Proxmox',
|
||||||
|
details: stats.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
res.json({ ...server, stats });
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(500).json({ error: error?.message || 'Ошибка получения статуса' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запустить сервер
|
||||||
|
export async function startServer(req: Request, res: Response) {
|
||||||
|
await handleControl(req, res, 'start');
|
||||||
|
}
|
||||||
|
// Остановить сервер
|
||||||
|
export async function stopServer(req: Request, res: Response) {
|
||||||
|
await handleControl(req, res, 'stop');
|
||||||
|
}
|
||||||
|
// Перезагрузить сервер
|
||||||
|
export async function restartServer(req: Request, res: Response) {
|
||||||
|
await handleControl(req, res, 'restart');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleControl(req: Request, res: Response, action: 'start' | 'stop' | 'restart') {
|
||||||
|
try {
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
const server = await prisma.server.findUnique({ where: { id } });
|
||||||
|
if (!server || !server.proxmoxId) return res.status(404).json({ error: 'Сервер не найден или нет VMID' });
|
||||||
|
// Получаем текущий статус VM
|
||||||
|
const stats = await getContainerStats(server.proxmoxId);
|
||||||
|
const currentStatus = stats.status === 'success' && stats.data ? stats.data.status : server.status;
|
||||||
|
// Ограничения на действия
|
||||||
|
if (action === 'start' && currentStatus === 'running') {
|
||||||
|
return res.status(400).json({ error: 'Сервер уже запущен' });
|
||||||
|
}
|
||||||
|
if (action === 'stop' && currentStatus === 'stopped') {
|
||||||
|
return res.status(400).json({ error: 'Сервер уже остановлен' });
|
||||||
|
}
|
||||||
|
// Выполняем действие
|
||||||
|
const result = await controlContainer(server.proxmoxId, action);
|
||||||
|
// Polling статуса VM после управления
|
||||||
|
let newStatus = server.status;
|
||||||
|
if (result.status === 'success') {
|
||||||
|
let status = '';
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 10;
|
||||||
|
while (attempts < maxAttempts) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
|
const stats = await getContainerStats(server.proxmoxId);
|
||||||
|
if (stats.status === 'success' && stats.data) {
|
||||||
|
status = stats.data.status;
|
||||||
|
if ((action === 'start' && status === 'running') ||
|
||||||
|
(action === 'stop' && status === 'stopped') ||
|
||||||
|
(action === 'restart' && status === 'running')) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
switch (status) {
|
||||||
|
case 'running':
|
||||||
|
newStatus = 'running';
|
||||||
|
break;
|
||||||
|
case 'stopped':
|
||||||
|
newStatus = 'stopped';
|
||||||
|
break;
|
||||||
|
case 'suspended':
|
||||||
|
newStatus = 'suspended';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
newStatus = status || server.status;
|
||||||
|
}
|
||||||
|
await prisma.server.update({ where: { id }, data: { status: newStatus } });
|
||||||
|
}
|
||||||
|
res.json({ ...result, status: newStatus });
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(500).json({ error: error?.message || 'Ошибка управления сервером' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удалить сервер
|
||||||
|
export async function deleteServer(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
const server = await prisma.server.findUnique({ where: { id } });
|
||||||
|
if (!server || !server.proxmoxId) return res.status(404).json({ error: 'Сервер не найден или нет VMID' });
|
||||||
|
// Удаляем контейнер в Proxmox
|
||||||
|
const proxmoxResult = await deleteContainer(server.proxmoxId);
|
||||||
|
if (proxmoxResult.status !== 'success') {
|
||||||
|
return res.status(500).json({ error: 'Ошибка удаления контейнера в Proxmox', details: proxmoxResult });
|
||||||
|
}
|
||||||
|
await prisma.server.delete({ where: { id } });
|
||||||
|
res.json({ status: 'deleted' });
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(500).json({ error: error?.message || 'Ошибка удаления сервера' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сменить root-пароль
|
||||||
|
export async function changeRootPassword(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
const server = await prisma.server.findUnique({ where: { id } });
|
||||||
|
if (!server || !server.proxmoxId) return res.status(404).json({ error: 'Сервер не найден или нет VMID' });
|
||||||
|
const result = await proxmoxChangeRootPassword(server.proxmoxId);
|
||||||
|
if (result?.status === 'success' && result.password) {
|
||||||
|
await prisma.server.update({ where: { id }, data: { rootPassword: result.password } });
|
||||||
|
}
|
||||||
|
res.json(result);
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(500).json({ error: error?.message || 'Ошибка смены пароля' });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,119 +1,75 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import { authMiddleware } from '../auth/auth.middleware';
|
import { authMiddleware } from '../auth/auth.middleware';
|
||||||
import { checkProxmoxConnection, createProxmoxContainer } from './proxmoxApi';
|
import {
|
||||||
|
createServer,
|
||||||
|
startServer,
|
||||||
|
stopServer,
|
||||||
|
restartServer,
|
||||||
|
getServerStatus,
|
||||||
|
deleteServer,
|
||||||
|
changeRootPassword
|
||||||
|
} from './server.controller';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
router.use(authMiddleware);
|
router.use(authMiddleware);
|
||||||
|
|
||||||
router.get('/proxmox-status', async (req, res) => {
|
|
||||||
try {
|
|
||||||
console.log('Попытка подключения к серверу Proxmox...');
|
|
||||||
const status = await checkProxmoxConnection();
|
|
||||||
console.log('Статус подключения к Proxmox:', status);
|
|
||||||
res.json(status);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Ошибка подключения к Proxmox:', err);
|
|
||||||
res.status(500).json({ error: 'Ошибка подключения к Proxmox' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/create', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { tariffId, osId } = req.body;
|
|
||||||
const userId = req.user?.id;
|
|
||||||
if (!userId) return res.status(401).json({ error: 'Нет авторизации' });
|
|
||||||
|
|
||||||
const tariff = await prisma.tariff.findUnique({ where: { id: tariffId } });
|
// Получить список всех серверов (для фронта)
|
||||||
const os = await prisma.operatingSystem.findUnique({ where: { id: osId } });
|
|
||||||
if (!tariff || !os) {
|
|
||||||
return res.status(400).json({ error: 'Тариф или ОС не найдены' });
|
|
||||||
}
|
|
||||||
|
|
||||||
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: 'Недостаточно средств на балансе' });
|
|
||||||
}
|
|
||||||
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.user.update({
|
|
||||||
where: { id: userId },
|
|
||||||
data: {
|
|
||||||
balance: {
|
|
||||||
decrement: tariff.price
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const node = process.env.PROXMOX_NODE;
|
|
||||||
const diskTemplate = process.env.PROXMOX_DISK_TEMPLATE;
|
|
||||||
const server = await prisma.server.create({
|
|
||||||
data: {
|
|
||||||
userId,
|
|
||||||
tariffId,
|
|
||||||
osId,
|
|
||||||
status: 'active',
|
|
||||||
node,
|
|
||||||
diskTemplate,
|
|
||||||
proxmoxId: proxmoxResult.proxmoxId || null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
res.json({ success: true, server });
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Ошибка создания сервера:', err);
|
|
||||||
return res.status(500).json({ error: 'Ошибка создания сервера' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET /api/server — получить все серверы пользователя
|
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
// Если нужен только свои сервера:
|
||||||
|
const where = userId ? { userId } : {};
|
||||||
|
const servers = await prisma.server.findMany({
|
||||||
|
where,
|
||||||
|
include: {
|
||||||
|
os: true,
|
||||||
|
tariff: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
res.json(servers);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получить информацию о сервере (для фронта)
|
||||||
|
router.get('/:id', async (req, res) => {
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
const server = await prisma.server.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
os: true,
|
||||||
|
tariff: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!server) return res.status(404).json({ error: 'Сервер не найден' });
|
||||||
|
res.json(server);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Получить статистику сервера (CPU, RAM и т.д.)
|
||||||
|
router.get('/:id/status', getServerStatus);
|
||||||
|
|
||||||
|
// Получить ссылку на noVNC консоль
|
||||||
|
import { getConsoleURL } from './proxmoxApi';
|
||||||
|
router.post('/console', async (req, res) => {
|
||||||
|
const { vmid } = req.body;
|
||||||
|
if (!vmid) return res.status(400).json({ status: 'error', message: 'Не указан VMID' });
|
||||||
try {
|
try {
|
||||||
const userId = req.user?.id;
|
const result = await getConsoleURL(Number(vmid));
|
||||||
if (!userId) return res.status(401).json({ error: 'Нет авторизации' });
|
res.json(result);
|
||||||
const servers = await prisma.server.findMany({
|
} catch (error: any) {
|
||||||
where: { userId },
|
res.status(500).json({ status: 'error', message: error?.message || 'Ошибка получения консоли' });
|
||||||
include: {
|
|
||||||
os: true,
|
|
||||||
tariff: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log('API /api/server ответ:', servers);
|
|
||||||
res.json(servers);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Ошибка получения серверов:', err);
|
|
||||||
res.status(500).json({ error: 'Ошибка получения серверов' });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/server/:id — получить один сервер пользователя по id
|
router.post('/create', createServer);
|
||||||
router.get('/:id', async (req, res) => {
|
router.post('/:id/start', startServer);
|
||||||
try {
|
router.post('/:id/stop', stopServer);
|
||||||
const userId = req.user?.id;
|
router.post('/:id/restart', restartServer);
|
||||||
if (!userId) return res.status(401).json({ error: 'Нет авторизации' });
|
router.delete('/:id', deleteServer);
|
||||||
const serverId = Number(req.params.id);
|
router.post('/:id/password', changeRootPassword);
|
||||||
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;
|
export default router;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
export const prisma = new PrismaClient();
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 271 KiB |
@@ -32,7 +32,7 @@ const Billing = () => {
|
|||||||
if (!checkFile || amount <= 0) return;
|
if (!checkFile || amount <= 0) return;
|
||||||
setUploadLoading(true);
|
setUploadLoading(true);
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('access_token');
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', checkFile);
|
formData.append('file', checkFile);
|
||||||
formData.append('amount', String(amount));
|
formData.append('amount', String(amount));
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const CheckVerification: React.FC = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('access_token');
|
||||||
const res = await axios.get<ICheck[]>(API_URL, {
|
const res = await axios.get<ICheck[]>(API_URL, {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
@@ -50,17 +50,17 @@ const CheckVerification: React.FC = () => {
|
|||||||
setActionLoading(checkId);
|
setActionLoading(checkId);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('access_token');
|
||||||
await axios.post(`${API_URL}/${action}`, { checkId }, {
|
await axios.post(`${API_URL}/${action}`, { checkId }, {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
setChecks((prevChecks: ICheck[]) => prevChecks.map((c: ICheck) => c.id === checkId ? { ...c, status: action === 'approve' ? 'approved' : 'rejected' } : c));
|
setChecks((prevChecks: ICheck[]) => prevChecks.map((c: ICheck) => c.id === checkId ? { ...c, status: action === 'approve' ? 'approved' : 'rejected' } : c));
|
||||||
// Если подтверждение — обновить баланс пользователя
|
// Если подтверждение — обновить баланс пользователя
|
||||||
if (action === 'approve') {
|
if (action === 'approve') {
|
||||||
try {
|
try {
|
||||||
const userToken = localStorage.getItem('access_token') || token;
|
const token = localStorage.getItem('access_token');
|
||||||
const headers = { Authorization: `Bearer ${userToken}` };
|
const headers = { Authorization: `Bearer ${token}` };
|
||||||
const userRes = await axios.get('http://localhost:5000/api/auth/me', { headers });
|
const userRes = await axios.get('http://localhost:5000/api/auth/me', { headers });
|
||||||
// Глобально обновить userData через типизированное событие (для Dashboard)
|
// Глобально обновить userData через типизированное событие (для Dashboard)
|
||||||
window.dispatchEvent(new CustomEvent<import('./types').UserData>('userDataUpdate', {
|
window.dispatchEvent(new CustomEvent<import('./types').UserData>('userDataUpdate', {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import ServerPanel from './serverpanel';
|
|||||||
import TicketsPage from './tickets';
|
import TicketsPage from './tickets';
|
||||||
import Billing from './billing';
|
import Billing from './billing';
|
||||||
import Settings from './settings';
|
import Settings from './settings';
|
||||||
|
import Notifications from './notificatons';
|
||||||
import CheckVerification from './checkverification';
|
import CheckVerification from './checkverification';
|
||||||
import TicketResponse from './ticketresponse';
|
import TicketResponse from './ticketresponse';
|
||||||
import Checkout from './checkout';
|
import Checkout from './checkout';
|
||||||
@@ -117,6 +118,7 @@ const Dashboard = () => {
|
|||||||
{ key: 'tickets', label: 'Тикеты', to: '/dashboard/tickets' },
|
{ key: 'tickets', label: 'Тикеты', to: '/dashboard/tickets' },
|
||||||
{ key: 'billing', label: 'Баланс', to: '/dashboard/billing' },
|
{ key: 'billing', label: 'Баланс', to: '/dashboard/billing' },
|
||||||
{ key: 'settings', label: 'Настройки', to: '/dashboard/settings' },
|
{ key: 'settings', label: 'Настройки', to: '/dashboard/settings' },
|
||||||
|
{ key: 'notifications', label: 'Уведомления', to: '/dashboard/notifications' },
|
||||||
];
|
];
|
||||||
const adminTabs = [
|
const adminTabs = [
|
||||||
{ key: 'checkverification', label: 'Проверка чеков', to: '/dashboard/checkverification' },
|
{ key: 'checkverification', label: 'Проверка чеков', to: '/dashboard/checkverification' },
|
||||||
@@ -210,6 +212,7 @@ const Dashboard = () => {
|
|||||||
<Route path="billing" element={<Billing />} />
|
<Route path="billing" element={<Billing />} />
|
||||||
)}
|
)}
|
||||||
<Route path="settings" element={<Settings />} />
|
<Route path="settings" element={<Settings />} />
|
||||||
|
<Route path="notifications" element={<Notifications />} />
|
||||||
{isOperator && (
|
{isOperator && (
|
||||||
<>
|
<>
|
||||||
<Route path="checkverification" element={<CheckVerification />} />
|
<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';
|
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 { useParams } from 'react-router-dom';
|
||||||
import axios, { AxiosError } from 'axios';
|
import axios, { AxiosError } from 'axios';
|
||||||
|
|
||||||
@@ -14,6 +61,13 @@ interface Server {
|
|||||||
rootPassword?: string;
|
rootPassword?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ServerStats {
|
||||||
|
data?: {
|
||||||
|
cpu?: number;
|
||||||
|
memory?: { usage?: number };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ key: 'overview', label: 'Обзор' },
|
{ key: 'overview', label: 'Обзор' },
|
||||||
{ key: 'console', label: 'Консоль' },
|
{ key: 'console', label: 'Консоль' },
|
||||||
@@ -22,9 +76,6 @@ const TABS = [
|
|||||||
{ key: 'security', label: 'Безопасность' },
|
{ key: 'security', label: 'Безопасность' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const generatePassword = () => {
|
|
||||||
return Math.random().toString(36).slice(-10) + Math.random().toString(36).slice(-6);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ServerPanel: React.FC = () => {
|
const ServerPanel: React.FC = () => {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
@@ -34,6 +85,7 @@ const ServerPanel: React.FC = () => {
|
|||||||
const [activeTab, setActiveTab] = useState('overview');
|
const [activeTab, setActiveTab] = useState('overview');
|
||||||
const [newRoot, setNewRoot] = useState<string | null>(null);
|
const [newRoot, setNewRoot] = useState<string | null>(null);
|
||||||
const [showRoot, setShowRoot] = useState(false);
|
const [showRoot, setShowRoot] = useState(false);
|
||||||
|
const [stats, setStats] = useState<ServerStats | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchServer = async () => {
|
const fetchServer = async () => {
|
||||||
@@ -42,6 +94,9 @@ const ServerPanel: React.FC = () => {
|
|||||||
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
||||||
const res = await axios.get(`http://localhost:5000/api/server/${id}`, { headers });
|
const res = await axios.get(`http://localhost:5000/api/server/${id}`, { headers });
|
||||||
setServer(res.data);
|
setServer(res.data);
|
||||||
|
// Получаем статистику
|
||||||
|
const statsRes = await axios.get(`http://localhost:5000/api/server/${id}/status`, { headers });
|
||||||
|
setStats(statsRes.data.stats);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err as AxiosError;
|
const error = err as AxiosError;
|
||||||
if (error?.response?.status === 404) {
|
if (error?.response?.status === 404) {
|
||||||
@@ -57,22 +112,47 @@ const ServerPanel: React.FC = () => {
|
|||||||
fetchServer();
|
fetchServer();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
// Генерация root-пароля (только для копирования)
|
// Смена root-пароля через backend
|
||||||
const handleGenerateRoot = () => {
|
const handleGenerateRoot = async () => {
|
||||||
try {
|
try {
|
||||||
const password = generatePassword();
|
const token = localStorage.getItem('access_token');
|
||||||
setNewRoot(password);
|
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
||||||
setShowRoot(true);
|
const res = await axios.post(`http://localhost:5000/api/server/${id}/password`, {}, { headers });
|
||||||
// TODO: отправить новый пароль на backend для смены
|
if (res.data?.status === 'success' && res.data.password) {
|
||||||
|
setNewRoot(res.data.password);
|
||||||
|
setShowRoot(true);
|
||||||
|
} else {
|
||||||
|
setError('Ошибка смены root-пароля');
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Ошибка генерации root-пароля:', err);
|
setError('Ошибка смены root-пароля');
|
||||||
|
console.error('Ошибка смены root-пароля:', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Базовые действия (заглушки)
|
// Реальные действия управления сервером
|
||||||
const handleAction = async (action: 'start' | 'stop' | 'restart') => {
|
const handleAction = async (action: 'start' | 'stop' | 'restart') => {
|
||||||
alert(`Выполнено действие: ${action} (реализовать вызов к backend)`);
|
try {
|
||||||
// TODO: реализовать вызов к backend
|
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) {
|
if (loading) {
|
||||||
@@ -124,19 +204,23 @@ const ServerPanel: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{activeTab === 'console' && (
|
{activeTab === 'console' && (
|
||||||
<div className="bg-gray-100 rounded-xl p-6 text-gray-700 font-mono text-sm">
|
<ConsoleSection serverId={server.id} />
|
||||||
<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' && (
|
{activeTab === 'stats' && (
|
||||||
<div className="bg-gray-100 rounded-xl p-6">
|
<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="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 flex-col items-center justify-center">
|
||||||
<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="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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,10 +1,53 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
const Settings = () => {
|
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 (
|
return (
|
||||||
<div className="p-8 bg-white rounded-3xl shadow-xl">
|
<div className="p-8 bg-white rounded-3xl shadow-xl max-w-xl mx-auto mt-6">
|
||||||
<h2 className="text-3xl font-bold text-gray-800 mb-6">Настройки аккаунта</h2>
|
<h2 className="text-2xl font-bold mb-4">Настройки аккаунта</h2>
|
||||||
<p className="text-lg text-gray-500">
|
<div className="flex space-x-4 mb-6">
|
||||||
Здесь вы сможете изменить свои личные данные, email и пароль.
|
<button
|
||||||
</p>
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ const TicketResponse: React.FC = () => {
|
|||||||
const fetchTickets = async () => {
|
const fetchTickets = async () => {
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('access_token');
|
||||||
const res = await axios.get('http://localhost:5000/api/ticket', {
|
const res = await axios.get('http://localhost:5000/api/ticket', {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
||||||
});
|
});
|
||||||
if (Array.isArray(res.data)) {
|
if (Array.isArray(res.data)) {
|
||||||
setTickets(res.data);
|
setTickets(res.data);
|
||||||
@@ -51,13 +51,13 @@ const TicketResponse: React.FC = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('access_token');
|
||||||
await axios.post('http://localhost:5000/api/ticket/respond', {
|
await axios.post('http://localhost:5000/api/ticket/respond', {
|
||||||
ticketId,
|
ticketId,
|
||||||
message: responseMsg[ticketId]
|
message: responseMsg[ticketId]
|
||||||
}, {
|
}, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
||||||
});
|
});
|
||||||
setResponseMsg(prev => ({ ...prev, [ticketId]: '' }));
|
setResponseMsg(prev => ({ ...prev, [ticketId]: '' }));
|
||||||
fetchTickets();
|
fetchTickets();
|
||||||
@@ -73,10 +73,10 @@ const TicketResponse: React.FC = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('access_token');
|
||||||
await axios.post('http://localhost:5000/api/ticket/close', { ticketId }, {
|
await axios.post('http://localhost:5000/api/ticket/close', { ticketId }, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
||||||
});
|
});
|
||||||
fetchTickets();
|
fetchTickets();
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const TicketsPage: React.FC<TicketsPageProps> = ({ setUserData }) => {
|
|||||||
|
|
||||||
const fetchTickets = async () => {
|
const fetchTickets = async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('access_token');
|
||||||
const res = await axios.get('http://localhost:5000/api/ticket', {
|
const res = await axios.get('http://localhost:5000/api/ticket', {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
||||||
@@ -55,7 +55,7 @@ const TicketsPage: React.FC<TicketsPageProps> = ({ setUserData }) => {
|
|||||||
|
|
||||||
const updateUserData = async () => {
|
const updateUserData = async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('access_token') || localStorage.getItem('token');
|
const token = localStorage.getItem('access_token');
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
const headers = { Authorization: `Bearer ${token}` };
|
const headers = { Authorization: `Bearer ${token}` };
|
||||||
const userRes = await axios.get('http://localhost:5000/api/auth/me', { headers });
|
const userRes = await axios.get('http://localhost:5000/api/auth/me', { headers });
|
||||||
@@ -80,7 +80,7 @@ const TicketsPage: React.FC<TicketsPageProps> = ({ setUserData }) => {
|
|||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('access_token');
|
||||||
await axios.post('http://localhost:5000/api/ticket/create', { title, message }, {
|
await axios.post('http://localhost:5000/api/ticket/create', { title, message }, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
||||||
@@ -98,7 +98,7 @@ const TicketsPage: React.FC<TicketsPageProps> = ({ setUserData }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const respondTicket = async (ticketId: number) => {
|
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 }, {
|
await axios.post('http://localhost:5000/api/ticket/respond', { ticketId, message: responseMsg }, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
||||||
@@ -109,7 +109,7 @@ const TicketsPage: React.FC<TicketsPageProps> = ({ setUserData }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const closeTicket = async (ticketId: number) => {
|
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 }, {
|
await axios.post('http://localhost:5000/api/ticket/close', { ticketId }, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
||||||
|
|||||||
Reference in New Issue
Block a user