Initial commit
This commit is contained in:
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.vite/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Servers
|
||||||
|
backend/servers/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
frontend/.env.local
|
||||||
|
frontend/.env.production.local
|
||||||
|
|
||||||
|
# Build
|
||||||
|
frontend/dist/
|
||||||
|
backend/build/
|
||||||
25
APPLY_FIXES.md
Normal file
25
APPLY_FIXES.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Применение исправлений
|
||||||
|
|
||||||
|
## Что исправлено
|
||||||
|
|
||||||
|
### ✅ Фронтенд компоненты
|
||||||
|
Все компоненты обновлены для передачи токена:
|
||||||
|
- `Console.jsx` ✅
|
||||||
|
- `Stats.jsx` ✅
|
||||||
|
- `FileManager.jsx` ✅
|
||||||
|
- `ServerSettings.jsx` ✅
|
||||||
|
- `CreateServerModal.jsx` ✅
|
||||||
|
|
||||||
|
### ⚠️ Нужно обновить вручную
|
||||||
|
|
||||||
|
#### 1. App.jsx (или App_final.jsx)
|
||||||
|
|
||||||
|
Найдите строку:
|
||||||
|
```jsx
|
||||||
|
{user?.role === 'admin' && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateModal(true)}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 p-2 rounded"
|
||||||
|
title="Создать сервер"
|
||||||
|
>
|
||||||
|
<Plus className="w-4
|
||||||
196
AUTH_SETUP.md
Normal file
196
AUTH_SETUP.md
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
# Настройка системы авторизации
|
||||||
|
|
||||||
|
## Что добавлено
|
||||||
|
|
||||||
|
1. **Система авторизации** - вход и регистрация пользователей
|
||||||
|
2. **Роли пользователей** - администраторы и обычные пользователи
|
||||||
|
3. **Управление доступом** - админы могут выдавать доступ к серверам
|
||||||
|
4. **JWT токены** - безопасная авторизация
|
||||||
|
|
||||||
|
## Первый запуск
|
||||||
|
|
||||||
|
### 1. Установите новые зависимости
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Новые библиотеки:
|
||||||
|
- `passlib[bcrypt]` - хеширование паролей
|
||||||
|
- `python-jose[cryptography]` - JWT токены
|
||||||
|
|
||||||
|
### 2. Переименуйте файл бэкенда
|
||||||
|
|
||||||
|
**ВАЖНО:** Удалите старый `backend/main.py` и переименуйте `backend/main_new.py` в `backend/main.py`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
del main.py
|
||||||
|
ren main_new.py main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Или вручную в проводнике Windows.
|
||||||
|
|
||||||
|
### 3. Запустите бэкенд
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
При первом запуске создастся пользователь по умолчанию:
|
||||||
|
- **Логин:** admin
|
||||||
|
- **Пароль:** admin
|
||||||
|
|
||||||
|
### 4. Запустите фронтенд
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
### Первый вход
|
||||||
|
|
||||||
|
1. Откройте http://localhost:3000
|
||||||
|
2. Войдите как `admin` / `admin`
|
||||||
|
3. **ВАЖНО:** Смените пароль администратора!
|
||||||
|
|
||||||
|
### Создание пользователей
|
||||||
|
|
||||||
|
1. Нажмите кнопку **"Пользователи"** в шапке
|
||||||
|
2. Новые пользователи могут зарегистрироваться самостоятельно
|
||||||
|
3. По умолчанию новые пользователи получают роль "Пользователь"
|
||||||
|
|
||||||
|
### Управление доступом к серверам
|
||||||
|
|
||||||
|
1. Перейдите в **"Пользователи"**
|
||||||
|
2. Найдите нужного пользователя
|
||||||
|
3. Нажмите на названия серверов чтобы выдать/забрать доступ
|
||||||
|
4. Зеленые кнопки = доступ есть
|
||||||
|
5. Серые кнопки = доступа нет
|
||||||
|
|
||||||
|
### Роли пользователей
|
||||||
|
|
||||||
|
**Администратор:**
|
||||||
|
- Видит все серверы
|
||||||
|
- Может создавать/удалять серверы
|
||||||
|
- Может управлять пользователями
|
||||||
|
- Может изменять настройки серверов
|
||||||
|
|
||||||
|
**Пользователь:**
|
||||||
|
- Видит только серверы с доступом
|
||||||
|
- Может запускать/останавливать свои серверы
|
||||||
|
- Может управлять файлами своих серверов
|
||||||
|
- Не может создавать серверы
|
||||||
|
- Не может изменять настройки
|
||||||
|
|
||||||
|
### Изменение роли
|
||||||
|
|
||||||
|
1. Перейдите в **"Пользователи"**
|
||||||
|
2. Нажмите **"Сделать админом"** или **"Сделать пользователем"**
|
||||||
|
3. Подтвердите действие
|
||||||
|
|
||||||
|
### Удаление пользователя
|
||||||
|
|
||||||
|
1. Перейдите в **"Пользователи"**
|
||||||
|
2. Нажмите кнопку с иконкой корзины
|
||||||
|
3. Подтвердите удаление
|
||||||
|
|
||||||
|
**Примечание:** Нельзя удалить самого себя или изменить свою роль.
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
### Смена секретного ключа
|
||||||
|
|
||||||
|
Откройте `backend/main_new.py` (или `main.py` после переименования) и измените:
|
||||||
|
|
||||||
|
```python
|
||||||
|
SECRET_KEY = "your-secret-key-change-this-in-production-12345"
|
||||||
|
```
|
||||||
|
|
||||||
|
На случайную строку, например:
|
||||||
|
```python
|
||||||
|
SECRET_KEY = "super-secret-key-" + str(uuid.uuid4())
|
||||||
|
```
|
||||||
|
|
||||||
|
### Время жизни токена
|
||||||
|
|
||||||
|
По умолчанию токен действует 7 дней. Чтобы изменить:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 дней
|
||||||
|
```
|
||||||
|
|
||||||
|
### Хранение паролей
|
||||||
|
|
||||||
|
Пароли хешируются с помощью bcrypt и хранятся в файле `backend/users.json`.
|
||||||
|
|
||||||
|
**Не удаляйте этот файл!** Иначе потеряете всех пользователей.
|
||||||
|
|
||||||
|
## Файлы
|
||||||
|
|
||||||
|
- `backend/users.json` - база пользователей
|
||||||
|
- `backend/main_new.py` - новый бэкенд с авторизацией
|
||||||
|
- `frontend/src/components/Auth.jsx` - форма входа/регистрации
|
||||||
|
- `frontend/src/components/Users.jsx` - управление пользователями
|
||||||
|
|
||||||
|
## API эндпоинты
|
||||||
|
|
||||||
|
### Авторизация
|
||||||
|
|
||||||
|
- `POST /api/auth/register` - регистрация
|
||||||
|
- `POST /api/auth/login` - вход
|
||||||
|
- `GET /api/auth/me` - информация о текущем пользователе
|
||||||
|
|
||||||
|
### Пользователи (только админы)
|
||||||
|
|
||||||
|
- `GET /api/users` - список пользователей
|
||||||
|
- `PUT /api/users/{username}/servers` - изменить доступ к серверам
|
||||||
|
- `PUT /api/users/{username}/role` - изменить роль
|
||||||
|
- `DELETE /api/users/{username}` - удалить пользователя
|
||||||
|
|
||||||
|
### Серверы (с проверкой доступа)
|
||||||
|
|
||||||
|
Все существующие эндпоинты теперь требуют токен в заголовке:
|
||||||
|
```
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Ошибка "Требуется авторизация"
|
||||||
|
|
||||||
|
Токен истек или недействителен. Выйдите и войдите заново.
|
||||||
|
|
||||||
|
### Не могу войти как admin
|
||||||
|
|
||||||
|
Удалите файл `backend/users.json` и перезапустите бэкенд. Создастся новый админ с паролем `admin`.
|
||||||
|
|
||||||
|
### Забыл пароль
|
||||||
|
|
||||||
|
Удалите файл `backend/users.json` - все пользователи будут удалены и создастся новый админ.
|
||||||
|
|
||||||
|
Или отредактируйте `users.json` вручную, удалив нужного пользователя.
|
||||||
|
|
||||||
|
### Пользователь не видит серверы
|
||||||
|
|
||||||
|
Проверьте что админ выдал ему доступ в разделе "Пользователи".
|
||||||
|
|
||||||
|
## Миграция со старой версии
|
||||||
|
|
||||||
|
Если у вас уже есть серверы:
|
||||||
|
|
||||||
|
1. Сделайте backup папки `backend/servers/`
|
||||||
|
2. Установите новые зависимости
|
||||||
|
3. Замените `main.py` на новый
|
||||||
|
4. Запустите бэкенд
|
||||||
|
5. Войдите как admin/admin
|
||||||
|
6. Все серверы будут доступны админу автоматически
|
||||||
|
7. Создайте пользователей и выдайте им доступ
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Готово!** Теперь у вас есть полноценная система авторизации и управления доступом! 🔐
|
||||||
145
BUGFIX.md
Normal file
145
BUGFIX.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# Исправление багов
|
||||||
|
|
||||||
|
## Исправленные проблемы
|
||||||
|
|
||||||
|
### 1. ✅ Обычные пользователи теперь могут создавать серверы
|
||||||
|
|
||||||
|
**Что изменилось:**
|
||||||
|
- Убрана проверка роли при создании сервера
|
||||||
|
- Любой авторизованный пользователь может создать сервер
|
||||||
|
- При создании сервера обычным пользователем, ему автоматически выдается доступ к этому серверу
|
||||||
|
- Кнопка "+" теперь видна всем пользователям
|
||||||
|
|
||||||
|
**Файлы:**
|
||||||
|
- `backend/main_new.py` - убрана проверка `if user["role"] != "admin"`
|
||||||
|
- `frontend/src/App_final.jsx` - кнопка создания доступна всем
|
||||||
|
|
||||||
|
### 2. ✅ Админ теперь может просматривать файлы, статистику, настройки и консоль
|
||||||
|
|
||||||
|
**Проблема:**
|
||||||
|
Компоненты не передавали токен авторизации в запросы к API.
|
||||||
|
|
||||||
|
**Что исправлено:**
|
||||||
|
- Все компоненты теперь принимают prop `token`
|
||||||
|
- Все запросы к API включают заголовок `Authorization: Bearer ${token}`
|
||||||
|
|
||||||
|
**Исправленные компоненты:**
|
||||||
|
- `Console.jsx` - добавлен токен в запросы команд
|
||||||
|
- `Stats.jsx` - добавлен токен в запросы статистики
|
||||||
|
- `FileManager.jsx` - добавлен токен во все файловые операции
|
||||||
|
- `ServerSettings.jsx` - добавлен токен в настройки
|
||||||
|
- `CreateServerModal.jsx` - добавлен токен при создании
|
||||||
|
|
||||||
|
## Что нужно сделать
|
||||||
|
|
||||||
|
### Если вы еще не переименовали файлы:
|
||||||
|
|
||||||
|
1. **Удалите старые файлы:**
|
||||||
|
```
|
||||||
|
backend/main.py (если есть)
|
||||||
|
frontend/src/App.jsx (если есть)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Переименуйте новые файлы:**
|
||||||
|
```
|
||||||
|
backend/main_new.py → backend/main.py
|
||||||
|
frontend/src/App_final.jsx → frontend/src/App.jsx
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Перезапустите панель:**
|
||||||
|
```bash
|
||||||
|
START_PANEL.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
### Если файлы уже переименованы:
|
||||||
|
|
||||||
|
Просто перезапустите панель - изменения уже применены в `main_new.py` и `App_final.jsx`.
|
||||||
|
|
||||||
|
## Проверка исправлений
|
||||||
|
|
||||||
|
### Тест 1: Создание сервера обычным пользователем
|
||||||
|
|
||||||
|
1. Зарегистрируйте нового пользователя
|
||||||
|
2. Войдите под ним
|
||||||
|
3. Нажмите кнопку "+" в списке серверов
|
||||||
|
4. Создайте сервер
|
||||||
|
5. ✅ Сервер должен появиться в списке
|
||||||
|
|
||||||
|
### Тест 2: Просмотр файлов админом
|
||||||
|
|
||||||
|
1. Войдите как admin
|
||||||
|
2. Выберите любой сервер
|
||||||
|
3. Перейдите на вкладку "Файлы"
|
||||||
|
4. ✅ Должен отобразиться список файлов
|
||||||
|
|
||||||
|
### Тест 3: Просмотр статистики
|
||||||
|
|
||||||
|
1. Выберите сервер
|
||||||
|
2. Перейдите на вкладку "Статистика"
|
||||||
|
3. ✅ Должна отобразиться статистика (CPU, RAM, Disk)
|
||||||
|
|
||||||
|
### Тест 4: Консоль
|
||||||
|
|
||||||
|
1. Запустите сервер
|
||||||
|
2. Перейдите на вкладку "Консоль"
|
||||||
|
3. ✅ Должны появиться логи сервера
|
||||||
|
4. Отправьте команду (например, "list")
|
||||||
|
5. ✅ Команда должна выполниться
|
||||||
|
|
||||||
|
### Тест 5: Настройки
|
||||||
|
|
||||||
|
1. Перейдите на вкладку "Настройки"
|
||||||
|
2. ✅ Должны отобразиться настройки сервера
|
||||||
|
3. Измените что-нибудь и сохраните
|
||||||
|
4. ✅ Изменения должны сохраниться
|
||||||
|
|
||||||
|
## Дополнительные улучшения
|
||||||
|
|
||||||
|
### Автоматический доступ к созданным серверам
|
||||||
|
|
||||||
|
Теперь когда обычный пользователь создает сервер:
|
||||||
|
1. Сервер создается
|
||||||
|
2. Пользователю автоматически выдается доступ к этому серверу
|
||||||
|
3. Сервер сразу появляется в его списке
|
||||||
|
|
||||||
|
Админу не нужно вручную выдавать доступ!
|
||||||
|
|
||||||
|
### Логирование ошибок
|
||||||
|
|
||||||
|
Все ошибки API теперь выводятся в консоль браузера (F12) для отладки.
|
||||||
|
|
||||||
|
## Если что-то не работает
|
||||||
|
|
||||||
|
### Ошибка "Требуется авторизация"
|
||||||
|
|
||||||
|
**Причина:** Токен не передается в запросах
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
1. Убедитесь что используете обновленные файлы
|
||||||
|
2. Очистите кэш браузера (Ctrl+Shift+Delete)
|
||||||
|
3. Выйдите и войдите заново
|
||||||
|
|
||||||
|
### Ошибка "Нет доступа к этому серверу"
|
||||||
|
|
||||||
|
**Причина:** У пользователя нет прав на сервер
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
1. Если вы админ - проверьте что сервер существует
|
||||||
|
2. Если вы пользователь - попросите админа выдать доступ
|
||||||
|
3. Или создайте свой сервер - доступ выдастся автоматически
|
||||||
|
|
||||||
|
### Пустой список файлов
|
||||||
|
|
||||||
|
**Причина:** Токен не передается или сервер пустой
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
1. Проверьте консоль браузера (F12) на ошибки
|
||||||
|
2. Убедитесь что используете обновленный FileManager.jsx
|
||||||
|
3. Загрузите файлы через кнопку "Загрузить"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Готово!** Все баги исправлены. Теперь:
|
||||||
|
- ✅ Любой пользователь может создавать серверы
|
||||||
|
- ✅ Админ может просматривать все вкладки
|
||||||
|
- ✅ Все запросы включают токен авторизации
|
||||||
155
DEBUG_GUIDE.md
Normal file
155
DEBUG_GUIDE.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# Руководство по отладке проблем
|
||||||
|
|
||||||
|
## Проблема: После запуска сервера пропадают файлы/настройки/статистика
|
||||||
|
|
||||||
|
### Причина
|
||||||
|
Процесс сервера блокирует выполнение или завершается с ошибкой.
|
||||||
|
|
||||||
|
### Диагностика
|
||||||
|
|
||||||
|
1. **Проверьте логи бэкенда** (терминал где запущен `python main.py`):
|
||||||
|
```
|
||||||
|
Сервер test_server запущен с PID 12345
|
||||||
|
Начало чтения вывода для сервера test_server
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Проверьте консоль браузера** (F12):
|
||||||
|
```javascript
|
||||||
|
// Должны быть логи:
|
||||||
|
Сервер запущен: {message: "Сервер запущен", pid: 12345}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Проверьте, запустился ли Java процесс**:
|
||||||
|
```bash
|
||||||
|
# Windows
|
||||||
|
tasklist | findstr java
|
||||||
|
|
||||||
|
# Должен показать процесс java.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Проверьте наличие server.jar**:
|
||||||
|
- Откройте папку `backend/servers/ИМЯ_СЕРВЕРА/`
|
||||||
|
- Убедитесь, что там есть файл `server.jar` или другой .jar файл
|
||||||
|
- Проверьте команду запуска в настройках сервера
|
||||||
|
|
||||||
|
### Решение
|
||||||
|
|
||||||
|
#### Если server.jar отсутствует:
|
||||||
|
|
||||||
|
1. Скачайте server.jar для Minecraft
|
||||||
|
2. Загрузите через менеджер файлов в панели
|
||||||
|
3. Убедитесь, что команда запуска правильная
|
||||||
|
|
||||||
|
#### Если Java не установлена:
|
||||||
|
|
||||||
|
1. Установите Java 17 или новее
|
||||||
|
2. Проверьте установку:
|
||||||
|
```bash
|
||||||
|
java -version
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Если процесс запускается но сразу завершается:
|
||||||
|
|
||||||
|
1. Проверьте логи в консоли панели
|
||||||
|
2. Возможно нужно принять EULA:
|
||||||
|
- Откройте файл `eula.txt` через редактор в панели
|
||||||
|
- Измените `eula=false` на `eula=true`
|
||||||
|
- Сохраните и перезапустите сервер
|
||||||
|
|
||||||
|
#### Если команда запуска неправильная:
|
||||||
|
|
||||||
|
1. Перейдите в Настройки сервера
|
||||||
|
2. Измените команду запуска, например:
|
||||||
|
```
|
||||||
|
java -Xmx2G -Xms1G -jar server.jar nogui
|
||||||
|
```
|
||||||
|
3. Сохраните настройки
|
||||||
|
4. Запустите сервер
|
||||||
|
|
||||||
|
## Проблема: Сервер не останавливается
|
||||||
|
|
||||||
|
### Причина
|
||||||
|
Процесс не отвечает на команду stop.
|
||||||
|
|
||||||
|
### Решение
|
||||||
|
|
||||||
|
1. **Через панель**: Подождите 30 секунд, процесс будет принудительно завершен
|
||||||
|
|
||||||
|
2. **Вручную через Task Manager**:
|
||||||
|
- Откройте Диспетчер задач (Ctrl+Shift+Esc)
|
||||||
|
- Найдите процесс `java.exe`
|
||||||
|
- Завершите процесс
|
||||||
|
- Обновите страницу панели
|
||||||
|
|
||||||
|
## Проблема: Консоль не показывает логи
|
||||||
|
|
||||||
|
### Причина
|
||||||
|
WebSocket не подключается или процесс не выводит логи.
|
||||||
|
|
||||||
|
### Диагностика
|
||||||
|
|
||||||
|
1. **Проверьте консоль браузера**:
|
||||||
|
```
|
||||||
|
WebSocket подключен
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Проверьте логи бэкенда**:
|
||||||
|
```
|
||||||
|
WebSocket подключен для сервера: test_server
|
||||||
|
Отправка X существующих логов
|
||||||
|
```
|
||||||
|
|
||||||
|
### Решение
|
||||||
|
|
||||||
|
1. Перезапустите сервер
|
||||||
|
2. Обновите страницу панели (F5)
|
||||||
|
3. Проверьте, что сервер действительно запущен
|
||||||
|
|
||||||
|
## Проблема: Статистика показывает неправильный статус
|
||||||
|
|
||||||
|
### Причина
|
||||||
|
Процесс завершился, но панель не обновилась.
|
||||||
|
|
||||||
|
### Решение
|
||||||
|
|
||||||
|
1. Обновите страницу (F5)
|
||||||
|
2. Статус обновляется автоматически каждые 5 секунд
|
||||||
|
3. Проверьте логи бэкенда на наличие ошибок
|
||||||
|
|
||||||
|
## Полезные команды для отладки
|
||||||
|
|
||||||
|
### Проверка портов
|
||||||
|
```bash
|
||||||
|
# Windows
|
||||||
|
netstat -ano | findstr :8000
|
||||||
|
netstat -ano | findstr :3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка процессов Java
|
||||||
|
```bash
|
||||||
|
# Windows
|
||||||
|
tasklist | findstr java
|
||||||
|
|
||||||
|
# Убить все процессы Java (ОСТОРОЖНО!)
|
||||||
|
taskkill /F /IM java.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Очистка и перезапуск
|
||||||
|
|
||||||
|
1. Остановите все серверы в панели
|
||||||
|
2. Закройте бэкенд (Ctrl+C)
|
||||||
|
3. Закройте фронтенд (Ctrl+C)
|
||||||
|
4. Убейте все процессы Java если нужно
|
||||||
|
5. Запустите бэкенд заново
|
||||||
|
6. Запустите фронтенд заново
|
||||||
|
7. Обновите страницу в браузере
|
||||||
|
|
||||||
|
## Логи для отправки при обращении за помощью
|
||||||
|
|
||||||
|
Если проблема не решается, соберите следующую информацию:
|
||||||
|
|
||||||
|
1. **Логи бэкенда** (последние 50 строк из терминала)
|
||||||
|
2. **Консоль браузера** (F12 → Console, скриншот или текст)
|
||||||
|
3. **Network вкладка** (F12 → Network, покажите неудачные запросы)
|
||||||
|
4. **Содержимое папки сервера** (список файлов)
|
||||||
|
5. **Команда запуска** из настроек сервера
|
||||||
163
FINAL_STEPS.md
Normal file
163
FINAL_STEPS.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# Финальные шаги для запуска панели с авторизацией
|
||||||
|
|
||||||
|
## Шаг 1: Переименуйте файлы
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
1. Откройте папку `backend`
|
||||||
|
2. Удалите файл `main.py` (если есть)
|
||||||
|
3. Переименуйте `main_new.py` в `main.py`
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
1. Откройте папку `frontend/src`
|
||||||
|
2. Удалите файл `App.jsx` (если есть)
|
||||||
|
3. Переименуйте `App_final.jsx` в `App.jsx`
|
||||||
|
|
||||||
|
## Шаг 2: Установите зависимости
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Новые зависимости:
|
||||||
|
- passlib[bcrypt] - для хеширования паролей
|
||||||
|
- python-jose[cryptography] - для JWT токенов
|
||||||
|
|
||||||
|
## Шаг 3: Запустите панель
|
||||||
|
|
||||||
|
### Вариант 1: Автоматический запуск
|
||||||
|
```bash
|
||||||
|
START_PANEL.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вариант 2: Ручной запуск
|
||||||
|
|
||||||
|
**Терминал 1 - Бэкенд:**
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Терминал 2 - Фронтенд:**
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Шаг 4: Первый вход
|
||||||
|
|
||||||
|
1. Откройте http://localhost:3000
|
||||||
|
2. Войдите с учетными данными:
|
||||||
|
- **Логин:** admin
|
||||||
|
- **Пароль:** admin
|
||||||
|
3. Вы попадете в панель управления
|
||||||
|
|
||||||
|
## Что нового
|
||||||
|
|
||||||
|
### ✅ Система авторизации
|
||||||
|
- Вход и регистрация пользователей
|
||||||
|
- JWT токены для безопасности
|
||||||
|
- Автоматический выход при истечении токена
|
||||||
|
|
||||||
|
### ✅ Роли пользователей
|
||||||
|
- **Администратор** - полный доступ ко всем функциям
|
||||||
|
- **Пользователь** - доступ только к назначенным серверам
|
||||||
|
|
||||||
|
### ✅ Управление пользователями
|
||||||
|
- Кнопка "Пользователи" в шапке (только для админов)
|
||||||
|
- Выдача/отзыв доступа к серверам
|
||||||
|
- Изменение ролей пользователей
|
||||||
|
- Удаление пользователей
|
||||||
|
|
||||||
|
### ✅ Контроль доступа
|
||||||
|
- Пользователи видят только свои серверы
|
||||||
|
- Админы видят все серверы
|
||||||
|
- Проверка прав на каждое действие
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
### Создание нового пользователя
|
||||||
|
|
||||||
|
**Вариант 1: Регистрация**
|
||||||
|
1. На странице входа нажмите "Регистрация"
|
||||||
|
2. Введите логин и пароль
|
||||||
|
3. Новый пользователь создастся с ролью "Пользователь"
|
||||||
|
|
||||||
|
**Вариант 2: Админ создает**
|
||||||
|
1. Попросите пользователя зарегистрироваться
|
||||||
|
2. Админ выдает ему доступ к нужным серверам
|
||||||
|
|
||||||
|
### Выдача доступа к серверу
|
||||||
|
|
||||||
|
1. Войдите как администратор
|
||||||
|
2. Нажмите кнопку "Пользователи"
|
||||||
|
3. Найдите нужного пользователя
|
||||||
|
4. Нажмите на название сервера (станет зеленым)
|
||||||
|
5. Пользователь сразу увидит этот сервер
|
||||||
|
|
||||||
|
### Изменение роли
|
||||||
|
|
||||||
|
1. В разделе "Пользователи"
|
||||||
|
2. Нажмите "Сделать админом" или "Сделать пользователем"
|
||||||
|
3. Подтвердите действие
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
### Смените секретный ключ!
|
||||||
|
|
||||||
|
Откройте `backend/main.py` и измените:
|
||||||
|
```python
|
||||||
|
SECRET_KEY = "your-secret-key-change-this-in-production-12345"
|
||||||
|
```
|
||||||
|
|
||||||
|
На случайную строку длиной минимум 32 символа.
|
||||||
|
|
||||||
|
### Смените пароль администратора
|
||||||
|
|
||||||
|
1. Войдите как admin
|
||||||
|
2. Создайте нового администратора с другим паролем
|
||||||
|
3. Войдите под новым админом
|
||||||
|
4. Удалите старого admin
|
||||||
|
|
||||||
|
## Файлы данных
|
||||||
|
|
||||||
|
- `backend/users.json` - база пользователей (НЕ УДАЛЯЙТЕ!)
|
||||||
|
- `backend/servers/` - папки серверов
|
||||||
|
- `backend/servers/*/panel_config.json` - настройки каждого сервера
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Требуется авторизация"
|
||||||
|
Токен истек. Выйдите и войдите заново.
|
||||||
|
|
||||||
|
### Не могу войти
|
||||||
|
Удалите `backend/users.json` и перезапустите бэкенд. Создастся новый admin/admin.
|
||||||
|
|
||||||
|
### Пользователь не видит серверы
|
||||||
|
Проверьте что админ выдал ему доступ в разделе "Пользователи".
|
||||||
|
|
||||||
|
### Ошибка импорта passlib или jose
|
||||||
|
Установите зависимости:
|
||||||
|
```bash
|
||||||
|
pip install passlib[bcrypt] python-jose[cryptography]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Доступ через сеть
|
||||||
|
|
||||||
|
Всё работает так же как раньше:
|
||||||
|
|
||||||
|
1. Узнайте ваш IP в Radmin VPN: `ipconfig`
|
||||||
|
2. Друг открывает: `http://ВАШ_IP:3000`
|
||||||
|
3. Друг регистрируется
|
||||||
|
4. Вы выдаете ему доступ к нужным серверам
|
||||||
|
|
||||||
|
## Готово!
|
||||||
|
|
||||||
|
Теперь у вас полноценная панель управления с:
|
||||||
|
- ✅ Авторизацией и регистрацией
|
||||||
|
- ✅ Ролями и правами доступа
|
||||||
|
- ✅ Управлением пользователями
|
||||||
|
- ✅ Контролем доступа к серверам
|
||||||
|
- ✅ Всеми предыдущими функциями
|
||||||
|
|
||||||
|
Подробнее см. `AUTH_SETUP.md`
|
||||||
187
INSTALLATION_COMPLETE.md
Normal file
187
INSTALLATION_COMPLETE.md
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
# ✅ Установка завершена!
|
||||||
|
|
||||||
|
## Что было создано
|
||||||
|
|
||||||
|
### Backend (FastAPI)
|
||||||
|
- ✅ Система авторизации с JWT токенами
|
||||||
|
- ✅ Управление пользователями и ролями
|
||||||
|
- ✅ Контроль доступа к серверам
|
||||||
|
- ✅ API для всех операций с серверами
|
||||||
|
- ✅ WebSocket для консоли в реальном времени
|
||||||
|
- ✅ Файловый менеджер с редактором
|
||||||
|
- ✅ Мониторинг ресурсов
|
||||||
|
|
||||||
|
### Frontend (React)
|
||||||
|
- ✅ Форма входа и регистрации
|
||||||
|
- ✅ Управление пользователями (для админов)
|
||||||
|
- ✅ Панель управления серверами
|
||||||
|
- ✅ Консоль с логами
|
||||||
|
- ✅ Файловый менеджер
|
||||||
|
- ✅ Статистика ресурсов
|
||||||
|
- ✅ Настройки серверов
|
||||||
|
|
||||||
|
## Финальные шаги
|
||||||
|
|
||||||
|
### 1. Переименуйте файлы
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
```
|
||||||
|
backend/main_new.py → backend/main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
```
|
||||||
|
frontend/src/App_final.jsx → frontend/src/App.jsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Установите зависимости
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Новые зависимости:
|
||||||
|
- `passlib[bcrypt]` - хеширование паролей
|
||||||
|
- `python-jose[cryptography]` - JWT токены
|
||||||
|
|
||||||
|
### 3. Запустите панель
|
||||||
|
|
||||||
|
**Автоматически:**
|
||||||
|
```bash
|
||||||
|
START_PANEL.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
**Вручную:**
|
||||||
|
```bash
|
||||||
|
# Терминал 1
|
||||||
|
cd backend
|
||||||
|
python main.py
|
||||||
|
|
||||||
|
# Терминал 2
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Первый вход
|
||||||
|
|
||||||
|
1. Откройте http://localhost:3000
|
||||||
|
2. Войдите:
|
||||||
|
- Логин: `admin`
|
||||||
|
- Пароль: `admin`
|
||||||
|
|
||||||
|
## Основные функции
|
||||||
|
|
||||||
|
### Для администраторов
|
||||||
|
|
||||||
|
1. **Создание серверов** - кнопка "+" в боковой панели
|
||||||
|
2. **Управление пользователями** - кнопка "Пользователи" в шапке
|
||||||
|
3. **Выдача доступа** - нажимайте на названия серверов в карточке пользователя
|
||||||
|
4. **Изменение ролей** - кнопка "Сделать админом/пользователем"
|
||||||
|
5. **Настройки серверов** - вкладка "Настройки"
|
||||||
|
|
||||||
|
### Для пользователей
|
||||||
|
|
||||||
|
1. **Просмотр своих серверов** - только те, к которым есть доступ
|
||||||
|
2. **Запуск/остановка** - кнопки на карточке сервера
|
||||||
|
3. **Консоль** - просмотр логов и отправка команд
|
||||||
|
4. **Файлы** - управление файлами сервера
|
||||||
|
5. **Статистика** - мониторинг ресурсов
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
### ⚠️ ВАЖНО: Смените секретный ключ!
|
||||||
|
|
||||||
|
Откройте `backend/main.py` и измените:
|
||||||
|
```python
|
||||||
|
SECRET_KEY = "your-secret-key-change-this-in-production-12345"
|
||||||
|
```
|
||||||
|
|
||||||
|
На случайную строку минимум 32 символа.
|
||||||
|
|
||||||
|
### Смените пароль администратора
|
||||||
|
|
||||||
|
1. Создайте нового администратора
|
||||||
|
2. Войдите под ним
|
||||||
|
3. Удалите старого admin
|
||||||
|
|
||||||
|
## Доступ через сеть (Radmin VPN)
|
||||||
|
|
||||||
|
### На вашем компьютере:
|
||||||
|
1. Узнайте IP: `ipconfig` (ищите Radmin VPN, обычно 26.x.x.x)
|
||||||
|
2. Запустите панель
|
||||||
|
3. Откройте: http://localhost:3000
|
||||||
|
|
||||||
|
### На компьютере друга:
|
||||||
|
1. Откройте: http://ВАШ_IP:3000
|
||||||
|
2. Зарегистрируйтесь
|
||||||
|
3. Попросите вас выдать доступ к серверам
|
||||||
|
|
||||||
|
### Откройте порты (если не работает):
|
||||||
|
```powershell
|
||||||
|
netsh advfirewall firewall add rule name="MC Panel Backend" dir=in action=allow protocol=TCP localport=8000
|
||||||
|
netsh advfirewall firewall add rule name="MC Panel Frontend" dir=in action=allow protocol=TCP localport=3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура файлов
|
||||||
|
|
||||||
|
```
|
||||||
|
mc-panel/
|
||||||
|
├── START_PANEL.bat # Автозапуск
|
||||||
|
├── FINAL_STEPS.md # Инструкция
|
||||||
|
├── AUTH_SETUP.md # Руководство по авторизации
|
||||||
|
├── backend/
|
||||||
|
│ ├── main_new.py # Новый бэкенд (переименуйте в main.py)
|
||||||
|
│ ├── requirements.txt # Зависимости
|
||||||
|
│ ├── users.json # База пользователей (создастся автоматически)
|
||||||
|
│ └── servers/ # Папки серверов
|
||||||
|
└── frontend/
|
||||||
|
├── src/
|
||||||
|
│ ├── App_final.jsx # Новый App (переименуйте в App.jsx)
|
||||||
|
│ └── components/
|
||||||
|
│ ├── Auth.jsx # Форма входа
|
||||||
|
│ ├── Users.jsx # Управление пользователями
|
||||||
|
│ ├── Console.jsx # Консоль (обновлен)
|
||||||
|
│ ├── FileManager.jsx # Файлы (обновлен)
|
||||||
|
│ ├── Stats.jsx # Статистика (обновлен)
|
||||||
|
│ └── ...
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Ошибка импорта passlib или jose
|
||||||
|
```bash
|
||||||
|
pip install passlib[bcrypt] python-jose[cryptography]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Не могу войти
|
||||||
|
Удалите `backend/users.json` и перезапустите бэкенд.
|
||||||
|
|
||||||
|
### Пользователь не видит серверы
|
||||||
|
Админ должен выдать доступ в разделе "Пользователи".
|
||||||
|
|
||||||
|
### Токен истек
|
||||||
|
Выйдите и войдите заново.
|
||||||
|
|
||||||
|
## Документация
|
||||||
|
|
||||||
|
- `FINAL_STEPS.md` - пошаговая инструкция
|
||||||
|
- `AUTH_SETUP.md` - полное руководство по авторизации
|
||||||
|
- `QUICK_START.md` - быстрый старт
|
||||||
|
- `DEBUG_GUIDE.md` - отладка проблем
|
||||||
|
- `NETWORK_SETUP.md` - настройка сети
|
||||||
|
|
||||||
|
## Готово! 🎉
|
||||||
|
|
||||||
|
Теперь у вас есть полноценная панель управления Minecraft серверами с:
|
||||||
|
- ✅ Авторизацией и регистрацией
|
||||||
|
- ✅ Ролями и правами доступа
|
||||||
|
- ✅ Управлением пользователями
|
||||||
|
- ✅ Контролем доступа к серверам
|
||||||
|
- ✅ Консолью в реальном времени
|
||||||
|
- ✅ Файловым менеджером с редактором
|
||||||
|
- ✅ Мониторингом ресурсов
|
||||||
|
- ✅ Поддержкой удаленного доступа
|
||||||
|
|
||||||
|
**Приятного использования!** 🚀
|
||||||
114
NETWORK_SETUP.md
Normal file
114
NETWORK_SETUP.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# Настройка доступа через сеть
|
||||||
|
|
||||||
|
## Быстрый старт для Radmin VPN
|
||||||
|
|
||||||
|
### 1. Узнайте ваш IP адрес в Radmin VPN
|
||||||
|
|
||||||
|
Откройте командную строку и выполните:
|
||||||
|
```bash
|
||||||
|
ipconfig
|
||||||
|
```
|
||||||
|
|
||||||
|
Найдите адаптер Radmin VPN, IP обычно выглядит как `26.x.x.x`
|
||||||
|
|
||||||
|
### 2. Запустите бэкенд
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Бэкенд автоматически слушает на всех сетевых интерфейсах (0.0.0.0:8000)
|
||||||
|
|
||||||
|
### 3. Запустите фронтенд
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь фронтенд запускается с флагом `--host` по умолчанию.
|
||||||
|
|
||||||
|
### 4. Откройте в браузере
|
||||||
|
|
||||||
|
**На вашем компьютере:**
|
||||||
|
- http://localhost:3000
|
||||||
|
|
||||||
|
**На компьютере друга:**
|
||||||
|
- http://ВАШ_RADMIN_IP:3000
|
||||||
|
- Например: http://26.123.45.67:3000
|
||||||
|
|
||||||
|
## Автоматическое определение API
|
||||||
|
|
||||||
|
Фронтенд автоматически определяет правильный API URL:
|
||||||
|
- Если открыто через `localhost` → подключится к `http://localhost:8000`
|
||||||
|
- Если открыто через IP → подключится к `http://ВАШ_IP:8000`
|
||||||
|
|
||||||
|
## Ручная настройка (если автоматика не работает)
|
||||||
|
|
||||||
|
Создайте файл `frontend/.env.local`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
VITE_API_URL=http://26.123.45.67:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Замените `26.123.45.67` на ваш реальный IP в Radmin VPN.
|
||||||
|
|
||||||
|
Перезапустите фронтенд:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Проверка подключения
|
||||||
|
|
||||||
|
1. Откройте консоль браузера (F12)
|
||||||
|
2. Проверьте, нет ли ошибок подключения
|
||||||
|
3. Убедитесь, что запросы идут на правильный IP адрес
|
||||||
|
|
||||||
|
## Возможные проблемы
|
||||||
|
|
||||||
|
### Серверы не загружаются
|
||||||
|
|
||||||
|
**Причина:** Фронтенд не может подключиться к бэкенду
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
1. Убедитесь, что бэкенд запущен
|
||||||
|
2. Проверьте, что используется правильный IP
|
||||||
|
3. Проверьте брандмауэр Windows (порты 8000 и 3000 должны быть открыты)
|
||||||
|
|
||||||
|
### Ошибка при создании сервера
|
||||||
|
|
||||||
|
**Причина:** CORS или неправильный API URL
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
1. Перезапустите бэкенд
|
||||||
|
2. Очистите кэш браузера (Ctrl+Shift+Delete)
|
||||||
|
3. Проверьте консоль браузера на ошибки
|
||||||
|
|
||||||
|
### WebSocket не подключается (консоль не работает)
|
||||||
|
|
||||||
|
**Причина:** WebSocket использует неправильный адрес
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
1. Проверьте файл `frontend/src/config.js`
|
||||||
|
2. WebSocket должен использовать `ws://` вместо `http://`
|
||||||
|
3. Перезапустите фронтенд
|
||||||
|
|
||||||
|
## Открытие портов в брандмауэре Windows
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Откройте PowerShell от имени администратора
|
||||||
|
|
||||||
|
# Порт для бэкенда
|
||||||
|
netsh advfirewall firewall add rule name="MC Panel Backend" dir=in action=allow protocol=TCP localport=8000
|
||||||
|
|
||||||
|
# Порт для фронтенда
|
||||||
|
netsh advfirewall firewall add rule name="MC Panel Frontend" dir=in action=allow protocol=TCP localport=3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Проверка работы
|
||||||
|
|
||||||
|
На компьютере друга откройте:
|
||||||
|
- http://ВАШ_IP:3000
|
||||||
|
|
||||||
|
Вы должны увидеть панель управления, и она должна показывать ваши серверы.
|
||||||
201
QUICK_START.md
Normal file
201
QUICK_START.md
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
# 🚀 Быстрый старт MC Panel
|
||||||
|
|
||||||
|
## Первый запуск
|
||||||
|
|
||||||
|
### Вариант 1: Автоматический запуск (Windows)
|
||||||
|
|
||||||
|
Просто запустите файл:
|
||||||
|
```
|
||||||
|
START_PANEL.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
Откроются два окна:
|
||||||
|
- **MC Panel Backend** - бэкенд сервер
|
||||||
|
- **MC Panel Frontend** - фронтенд сервер
|
||||||
|
|
||||||
|
Подождите 10-15 секунд и откройте в браузере:
|
||||||
|
```
|
||||||
|
http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вариант 2: Ручной запуск
|
||||||
|
|
||||||
|
**Терминал 1 - Бэкенд:**
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Терминал 2 - Фронтенд:**
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Создание первого сервера
|
||||||
|
|
||||||
|
1. Нажмите кнопку **"+"** в левой панели
|
||||||
|
2. Заполните форму:
|
||||||
|
- **Имя папки**: `my_server` (только латиница)
|
||||||
|
- **Отображаемое имя**: `Мой сервер`
|
||||||
|
- **Команда запуска**: `java -Xmx2G -Xms1G -jar server.jar nogui`
|
||||||
|
3. Нажмите **"Создать"**
|
||||||
|
|
||||||
|
## Загрузка файлов сервера
|
||||||
|
|
||||||
|
1. Выберите созданный сервер в списке
|
||||||
|
2. Перейдите на вкладку **"Файлы"**
|
||||||
|
3. Нажмите **"Загрузить"**
|
||||||
|
4. Выберите `server.jar` (скачайте с официального сайта Minecraft)
|
||||||
|
5. Если нужно, создайте файл `eula.txt`:
|
||||||
|
- Нажмите **"Загрузить"**
|
||||||
|
- Создайте текстовый файл с содержимым: `eula=true`
|
||||||
|
- Загрузите его
|
||||||
|
|
||||||
|
## Запуск сервера
|
||||||
|
|
||||||
|
1. Нажмите кнопку **"Старт"** на карточке сервера
|
||||||
|
2. Перейдите на вкладку **"Консоль"** чтобы видеть логи
|
||||||
|
3. Дождитесь сообщения `Done!` в консоли
|
||||||
|
4. Сервер готов к подключению!
|
||||||
|
|
||||||
|
## Управление сервером
|
||||||
|
|
||||||
|
### Консоль
|
||||||
|
- Просмотр логов в реальном времени
|
||||||
|
- Отправка команд серверу
|
||||||
|
- Примеры команд: `list`, `say Hello`, `stop`
|
||||||
|
|
||||||
|
### Файлы
|
||||||
|
- Просмотр и редактирование конфигов
|
||||||
|
- Загрузка плагинов/модов
|
||||||
|
- Скачивание файлов
|
||||||
|
- Переименование и удаление
|
||||||
|
|
||||||
|
### Статистика
|
||||||
|
- Использование CPU
|
||||||
|
- Потребление ОЗУ
|
||||||
|
- Размер на диске
|
||||||
|
- Статус сервера
|
||||||
|
|
||||||
|
### Настройки
|
||||||
|
- Изменение отображаемого имени
|
||||||
|
- Настройка команды запуска
|
||||||
|
- Удаление сервера
|
||||||
|
|
||||||
|
## Доступ через сеть (Radmin VPN)
|
||||||
|
|
||||||
|
### На вашем компьютере:
|
||||||
|
|
||||||
|
1. Узнайте ваш IP в Radmin VPN:
|
||||||
|
```bash
|
||||||
|
ipconfig
|
||||||
|
```
|
||||||
|
Ищите адаптер Radmin VPN (обычно `26.x.x.x`)
|
||||||
|
|
||||||
|
2. Запустите панель как обычно
|
||||||
|
|
||||||
|
3. Откройте в браузере:
|
||||||
|
```
|
||||||
|
http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### На компьютере друга:
|
||||||
|
|
||||||
|
1. Откройте в браузере:
|
||||||
|
```
|
||||||
|
http://ВАШ_RADMIN_IP:3000
|
||||||
|
```
|
||||||
|
Например: `http://26.62.117.104:3000`
|
||||||
|
|
||||||
|
2. Панель автоматически подключится к вашему бэкенду
|
||||||
|
|
||||||
|
### Если не работает:
|
||||||
|
|
||||||
|
Откройте порты в брандмауэре Windows (от имени администратора):
|
||||||
|
```powershell
|
||||||
|
netsh advfirewall firewall add rule name="MC Panel Backend" dir=in action=allow protocol=TCP localport=8000
|
||||||
|
netsh advfirewall firewall add rule name="MC Panel Frontend" dir=in action=allow protocol=TCP localport=3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Типичные проблемы
|
||||||
|
|
||||||
|
### Java не найдена
|
||||||
|
|
||||||
|
**Ошибка:** `'java' is not recognized...`
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
1. Установите Java 17+: https://adoptium.net/
|
||||||
|
2. Перезапустите терминал
|
||||||
|
3. Проверьте: `java -version`
|
||||||
|
|
||||||
|
### Сервер не запускается
|
||||||
|
|
||||||
|
**Причины:**
|
||||||
|
- Отсутствует `server.jar`
|
||||||
|
- Не принят EULA
|
||||||
|
- Неправильная команда запуска
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
1. Проверьте наличие `server.jar` в файлах
|
||||||
|
2. Создайте `eula.txt` с содержимым `eula=true`
|
||||||
|
3. Проверьте команду запуска в настройках
|
||||||
|
|
||||||
|
### Порт уже занят
|
||||||
|
|
||||||
|
**Ошибка:** `Address already in use`
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
```bash
|
||||||
|
# Найти процесс на порту 8000
|
||||||
|
netstat -ano | findstr :8000
|
||||||
|
|
||||||
|
# Убить процесс (замените PID)
|
||||||
|
taskkill /F /PID <PID>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Не видно файлов/настроек
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
1. Откройте консоль браузера (F12)
|
||||||
|
2. Проверьте вкладку Network на ошибки
|
||||||
|
3. Обновите страницу (F5)
|
||||||
|
4. Перезапустите бэкенд
|
||||||
|
|
||||||
|
## Полезные ссылки
|
||||||
|
|
||||||
|
- **Скачать Minecraft Server**: https://www.minecraft.net/en-us/download/server
|
||||||
|
- **Документация Minecraft**: https://minecraft.fandom.com/wiki/Server
|
||||||
|
- **Java Download**: https://adoptium.net/
|
||||||
|
- **Radmin VPN**: https://www.radmin-vpn.com/
|
||||||
|
|
||||||
|
## Команды Minecraft
|
||||||
|
|
||||||
|
Полезные команды для консоли:
|
||||||
|
|
||||||
|
```
|
||||||
|
list # Список игроков
|
||||||
|
say <message> # Сообщение всем
|
||||||
|
kick <player> # Кикнуть игрока
|
||||||
|
ban <player> # Забанить игрока
|
||||||
|
op <player> # Дать права оператора
|
||||||
|
deop <player> # Забрать права оператора
|
||||||
|
whitelist add <player> # Добавить в белый список
|
||||||
|
stop # Остановить сервер
|
||||||
|
```
|
||||||
|
|
||||||
|
## Конфигурационные файлы
|
||||||
|
|
||||||
|
Основные файлы для редактирования:
|
||||||
|
|
||||||
|
- **server.properties** - основные настройки сервера
|
||||||
|
- **eula.txt** - принятие лицензии
|
||||||
|
- **ops.json** - список операторов
|
||||||
|
- **whitelist.json** - белый список игроков
|
||||||
|
- **banned-players.json** - забаненные игроки
|
||||||
|
|
||||||
|
Редактируйте их через вкладку "Файлы" в панели!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Готово!** Теперь у вас есть полноценная панель управления Minecraft серверами! 🎮
|
||||||
128
README.md
Normal file
128
README.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# MC Panel - Панель управления Minecraft серверами
|
||||||
|
|
||||||
|
Панель управления для Minecraft серверов с FastAPI бэкендом и React фронтендом.
|
||||||
|
|
||||||
|
## Возможности
|
||||||
|
|
||||||
|
- ➕ Создание новых серверов
|
||||||
|
- 🎮 Запуск и остановка серверов
|
||||||
|
- 💻 Консоль с отправкой команд в реальном времени
|
||||||
|
- 📁 Менеджер файлов:
|
||||||
|
- Загрузка и скачивание файлов
|
||||||
|
- Просмотр содержимого файлов
|
||||||
|
- Редактирование текстовых файлов
|
||||||
|
- Переименование файлов и папок
|
||||||
|
- Удаление файлов и папок
|
||||||
|
- 📊 Мониторинг ресурсов (CPU, ОЗУ, диск)
|
||||||
|
- ⚙️ Настройки сервера (название, команда запуска)
|
||||||
|
- 🗑️ Удаление серверов
|
||||||
|
- 🔄 Автообновление статистики
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
### Бэкенд
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Сервер запустится на http://0.0.0.0:8000
|
||||||
|
|
||||||
|
### Фронтенд
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run dev -- --host
|
||||||
|
```
|
||||||
|
|
||||||
|
Приложение откроется на http://localhost:3000
|
||||||
|
|
||||||
|
## Доступ через сеть (Radmin VPN, Hamachi и т.д.)
|
||||||
|
|
||||||
|
### Вариант 1: Автоматическое определение (рекомендуется)
|
||||||
|
|
||||||
|
Фронтенд автоматически определит IP адрес и подключится к бэкенду.
|
||||||
|
|
||||||
|
1. Запустите бэкенд (он слушает на всех интерфейсах)
|
||||||
|
2. Запустите фронтенд с флагом `--host`:
|
||||||
|
```bash
|
||||||
|
npm run dev -- --host
|
||||||
|
```
|
||||||
|
3. Откройте в браузере: `http://ВАШ_IP:3000`
|
||||||
|
- Например: `http://26.123.45.67:3000` (Radmin VPN IP)
|
||||||
|
|
||||||
|
### Вариант 2: Ручная настройка
|
||||||
|
|
||||||
|
Создайте файл `frontend/.env.local`:
|
||||||
|
```
|
||||||
|
VITE_API_URL=http://26.123.45.67:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Замените `26.123.45.67` на ваш IP адрес в Radmin VPN.
|
||||||
|
|
||||||
|
### Проверка IP адреса
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
```bash
|
||||||
|
ipconfig
|
||||||
|
```
|
||||||
|
|
||||||
|
Ищите адрес адаптера Radmin VPN (обычно начинается с 26.x.x.x)
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/
|
||||||
|
main.py # FastAPI сервер
|
||||||
|
requirements.txt # Зависимости Python
|
||||||
|
servers/ # Папка с серверами Minecraft
|
||||||
|
|
||||||
|
frontend/
|
||||||
|
src/
|
||||||
|
components/
|
||||||
|
Console.jsx # Компонент консоли
|
||||||
|
FileManager.jsx # Менеджер файлов
|
||||||
|
Stats.jsx # Статистика
|
||||||
|
App.jsx # Главный компонент
|
||||||
|
main.jsx # Точка входа
|
||||||
|
package.json # Зависимости Node.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
### Быстрый старт
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
```bash
|
||||||
|
START_PANEL.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
**Вручную:**
|
||||||
|
```bash
|
||||||
|
# Терминал 1 - Бэкенд
|
||||||
|
cd backend
|
||||||
|
python main.py
|
||||||
|
|
||||||
|
# Терминал 2 - Фронтенд
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Откройте в браузере: http://localhost:3000
|
||||||
|
|
||||||
|
### Создание сервера
|
||||||
|
|
||||||
|
1. Нажмите кнопку "+" для создания нового сервера
|
||||||
|
2. Укажите имя, отображаемое название и команду запуска
|
||||||
|
3. Загрузите файлы сервера (server.jar и т.д.) через менеджер файлов
|
||||||
|
4. Создайте файл `eula.txt` с содержимым `eula=true`
|
||||||
|
5. Запустите сервер и управляйте им через вкладки:
|
||||||
|
- **Консоль** - просмотр логов и отправка команд
|
||||||
|
- **Файлы** - управление файлами сервера
|
||||||
|
- **Статистика** - мониторинг ресурсов
|
||||||
|
- **Настройки** - изменение параметров сервера
|
||||||
|
|
||||||
|
Подробнее: см. `QUICK_START.md`
|
||||||
138
README_FINAL.md
Normal file
138
README_FINAL.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# MC Panel - Финальная версия с авторизацией
|
||||||
|
|
||||||
|
## ✅ Что готово
|
||||||
|
|
||||||
|
Полноценная панель управления Minecraft серверами с системой авторизации и управлением пользователями.
|
||||||
|
|
||||||
|
## 🚀 Быстрый старт
|
||||||
|
|
||||||
|
### 1. Переименуйте файлы
|
||||||
|
|
||||||
|
**ВАЖНО! Сделайте это вручную в проводнике Windows:**
|
||||||
|
|
||||||
|
1. `backend/main_new.py` → `backend/main.py`
|
||||||
|
2. `frontend/src/App_final.jsx` → `frontend/src/App.jsx`
|
||||||
|
|
||||||
|
### 2. Установите зависимости
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Запустите
|
||||||
|
|
||||||
|
```bash
|
||||||
|
START_PANEL.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
Или вручную:
|
||||||
|
```bash
|
||||||
|
# Терминал 1
|
||||||
|
cd backend
|
||||||
|
python main.py
|
||||||
|
|
||||||
|
# Терминал 2
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Войдите
|
||||||
|
|
||||||
|
Откройте http://localhost:3000
|
||||||
|
|
||||||
|
- Логин: `admin`
|
||||||
|
- Пароль: `admin`
|
||||||
|
|
||||||
|
## 📚 Документация
|
||||||
|
|
||||||
|
- **INSTALLATION_COMPLETE.md** - полная инструкция по установке
|
||||||
|
- **AUTH_SETUP.md** - руководство по авторизации
|
||||||
|
- **FINAL_STEPS.md** - пошаговые инструкции
|
||||||
|
- **QUICK_START.md** - быстрый старт для новичков
|
||||||
|
- **DEBUG_GUIDE.md** - решение проблем
|
||||||
|
- **NETWORK_SETUP.md** - настройка удаленного доступа
|
||||||
|
|
||||||
|
## 🎯 Основные функции
|
||||||
|
|
||||||
|
### Авторизация
|
||||||
|
- Вход и регистрация
|
||||||
|
- JWT токены
|
||||||
|
- Автоматический выход при истечении
|
||||||
|
|
||||||
|
### Роли
|
||||||
|
- **Администратор** - полный доступ
|
||||||
|
- **Пользователь** - доступ к назначенным серверам
|
||||||
|
|
||||||
|
### Управление пользователями
|
||||||
|
- Выдача/отзыв доступа к серверам
|
||||||
|
- Изменение ролей
|
||||||
|
- Удаление пользователей
|
||||||
|
|
||||||
|
### Управление серверами
|
||||||
|
- Создание/удаление серверов
|
||||||
|
- Запуск/остановка
|
||||||
|
- Консоль в реальном времени
|
||||||
|
- Файловый менеджер с редактором
|
||||||
|
- Мониторинг ресурсов
|
||||||
|
- Настройки
|
||||||
|
|
||||||
|
## ⚠️ Важно
|
||||||
|
|
||||||
|
### Смените секретный ключ!
|
||||||
|
|
||||||
|
Откройте `backend/main.py` и измените:
|
||||||
|
```python
|
||||||
|
SECRET_KEY = "your-secret-key-change-this-in-production-12345"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Смените пароль admin
|
||||||
|
|
||||||
|
1. Создайте нового администратора
|
||||||
|
2. Войдите под ним
|
||||||
|
3. Удалите старого admin
|
||||||
|
|
||||||
|
## 🌐 Удаленный доступ
|
||||||
|
|
||||||
|
1. Узнайте IP: `ipconfig` (Radmin VPN обычно 26.x.x.x)
|
||||||
|
2. Друг открывает: `http://ВАШ_IP:3000`
|
||||||
|
3. Друг регистрируется
|
||||||
|
4. Вы выдаете ему доступ к серверам
|
||||||
|
|
||||||
|
## 📁 Структура
|
||||||
|
|
||||||
|
```
|
||||||
|
mc-panel/
|
||||||
|
├── backend/
|
||||||
|
│ ├── main_new.py → main.py # Переименуйте!
|
||||||
|
│ ├── requirements.txt
|
||||||
|
│ ├── users.json # Создастся автоматически
|
||||||
|
│ └── servers/
|
||||||
|
└── frontend/
|
||||||
|
├── src/
|
||||||
|
│ ├── App_final.jsx → App.jsx # Переименуйте!
|
||||||
|
│ └── components/
|
||||||
|
│ ├── Auth.jsx
|
||||||
|
│ ├── Users.jsx
|
||||||
|
│ └── ...
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🆘 Помощь
|
||||||
|
|
||||||
|
### Не могу войти
|
||||||
|
Удалите `backend/users.json` и перезапустите бэкенд.
|
||||||
|
|
||||||
|
### Ошибка импорта
|
||||||
|
```bash
|
||||||
|
pip install passlib[bcrypt] python-jose[cryptography]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пользователь не видит серверы
|
||||||
|
Админ должен выдать доступ в разделе "Пользователи".
|
||||||
|
|
||||||
|
## 🎉 Готово!
|
||||||
|
|
||||||
|
Теперь у вас полноценная панель с авторизацией!
|
||||||
|
|
||||||
|
Подробнее см. **INSTALLATION_COMPLETE.md**
|
||||||
61
READY_TO_USE.md
Normal file
61
READY_TO_USE.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# 🚀 MC Panel готова к использованию!
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
### 1. Запустите бэкенд
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python main_new.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Запустите фронтенд
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Откройте панель
|
||||||
|
Откройте http://localhost:3000 в браузере
|
||||||
|
|
||||||
|
### 4. Войдите в систему
|
||||||
|
- **Логин**: admin
|
||||||
|
- **Пароль**: admin
|
||||||
|
|
||||||
|
## ✨ Возможности
|
||||||
|
|
||||||
|
### 🎨 Темы
|
||||||
|
- 5 тем на выбор: Тёмная, Светлая, Фиолетовая, Синяя, Зелёная
|
||||||
|
- Градиентный логотип "MC Panel" для каждой темы
|
||||||
|
- Автоматическое сохранение выбранной темы
|
||||||
|
|
||||||
|
### 🖥️ Управление серверами
|
||||||
|
- Создание и удаление серверов
|
||||||
|
- Запуск и остановка серверов
|
||||||
|
- Просмотр консоли в реальном времени
|
||||||
|
- Менеджер файлов с редактированием
|
||||||
|
- Мониторинг ресурсов (RAM, диск)
|
||||||
|
- Настройки сервера
|
||||||
|
|
||||||
|
### 👥 Пользователи
|
||||||
|
- Регистрация и авторизация
|
||||||
|
- Роли: Админ и Пользователь
|
||||||
|
- Управление доступом к серверам
|
||||||
|
- Владельцы серверов могут выдавать доступ другим пользователям
|
||||||
|
|
||||||
|
### 🌐 Сетевой доступ
|
||||||
|
- Работает через Radmin VPN
|
||||||
|
- Автоматическое определение API URL
|
||||||
|
- Поддержка локальной и сетевой работы
|
||||||
|
|
||||||
|
## 📱 Интерфейс
|
||||||
|
|
||||||
|
Современный дизайн в стиле TimeWeb Cloud:
|
||||||
|
- Карточки с тенями и анимациями
|
||||||
|
- Плавные переходы
|
||||||
|
- Адаптивный дизайн для мобильных
|
||||||
|
- Sticky header
|
||||||
|
- Анимированные индикаторы статуса
|
||||||
|
|
||||||
|
## 🎯 Готово!
|
||||||
|
|
||||||
|
Панель полностью настроена и готова к использованию. Наслаждайтесь! 🎉
|
||||||
8
RENAME_FILE.txt
Normal file
8
RENAME_FILE.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
ВАЖНО: Переименуйте файл backend/main_new.py в backend/main.py
|
||||||
|
|
||||||
|
Удалите старый backend/main.py (если есть) и переименуйте backend/main_new.py в backend/main.py
|
||||||
|
|
||||||
|
Это можно сделать вручную в проводнике Windows или командой:
|
||||||
|
cd backend
|
||||||
|
del main.py
|
||||||
|
ren main_new.py main.py
|
||||||
23
START_PANEL.bat
Normal file
23
START_PANEL.bat
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
@echo off
|
||||||
|
title MC Panel Launcher
|
||||||
|
echo ========================================
|
||||||
|
echo MC Panel - Launcher
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo [1/2] Starting Backend...
|
||||||
|
start "MC Panel Backend" cmd /k "cd backend && python main.py"
|
||||||
|
timeout /t 3 /nobreak >nul
|
||||||
|
|
||||||
|
echo [2/2] Starting Frontend...
|
||||||
|
start "MC Panel Frontend" cmd /k "cd frontend && npm run dev"
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo Panel is starting...
|
||||||
|
echo Backend: http://localhost:8000
|
||||||
|
echo Frontend: http://localhost:3000
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
echo Press any key to exit launcher...
|
||||||
|
pause >nul
|
||||||
80
TEST_API.md
Normal file
80
TEST_API.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# Тестирование API
|
||||||
|
|
||||||
|
## Проверка работы API
|
||||||
|
|
||||||
|
Откройте браузер и проверьте следующие URL (замените IP на ваш):
|
||||||
|
|
||||||
|
### 1. Проверка списка серверов
|
||||||
|
```
|
||||||
|
http://26.123.45.67:8000/api/servers
|
||||||
|
```
|
||||||
|
|
||||||
|
Должен вернуть JSON с массивом серверов.
|
||||||
|
|
||||||
|
### 2. Проверка конфигурации сервера
|
||||||
|
```
|
||||||
|
http://26.123.45.67:8000/api/servers/ИМЯ_СЕРВЕРА/config
|
||||||
|
```
|
||||||
|
|
||||||
|
Должен вернуть JSON с настройками сервера.
|
||||||
|
|
||||||
|
### 3. Проверка файлов сервера
|
||||||
|
```
|
||||||
|
http://26.123.45.67:8000/api/servers/ИМЯ_СЕРВЕРА/files
|
||||||
|
```
|
||||||
|
|
||||||
|
Должен вернуть JSON с массивом файлов.
|
||||||
|
|
||||||
|
## Проверка в консоли браузера
|
||||||
|
|
||||||
|
Откройте консоль браузера (F12) и выполните:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Проверка API URL
|
||||||
|
console.log('API URL:', window.location.protocol + '//' + window.location.hostname + ':8000');
|
||||||
|
|
||||||
|
// Проверка серверов
|
||||||
|
fetch('http://' + window.location.hostname + ':8000/api/servers')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => console.log('Серверы:', data))
|
||||||
|
.catch(err => console.error('Ошибка:', err));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Проверка логов бэкенда
|
||||||
|
|
||||||
|
В терминале где запущен бэкенд должны появляться сообщения:
|
||||||
|
- `Найдено серверов: X`
|
||||||
|
- `Загружена конфигурация для ...`
|
||||||
|
- `WebSocket подключен для сервера: ...`
|
||||||
|
|
||||||
|
Если сообщений нет, значит запросы не доходят до бэкенда.
|
||||||
|
|
||||||
|
## Возможные проблемы
|
||||||
|
|
||||||
|
### Проблема: Серверы показываются, но файлы/настройки не загружаются
|
||||||
|
|
||||||
|
**Причина:** Запросы идут на неправильный URL
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
1. Откройте консоль браузера (F12)
|
||||||
|
2. Перейдите на вкладку Network
|
||||||
|
3. Попробуйте открыть файлы или настройки
|
||||||
|
4. Посмотрите на URL запросов - они должны начинаться с `http://ВАШ_IP:8000/api/`
|
||||||
|
|
||||||
|
### Проблема: CORS ошибки
|
||||||
|
|
||||||
|
**Причина:** Браузер блокирует запросы
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
1. Перезапустите бэкенд
|
||||||
|
2. Убедитесь, что в логах бэкенда нет ошибок
|
||||||
|
3. Очистите кэш браузера
|
||||||
|
|
||||||
|
### Проблема: WebSocket не подключается
|
||||||
|
|
||||||
|
**Причина:** WebSocket использует неправильный протокол
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
1. Проверьте файл `frontend/src/config.js`
|
||||||
|
2. WebSocket URL должен быть `ws://ВАШ_IP:8000`
|
||||||
|
3. Перезапустите фронтенд
|
||||||
47
THEME_APPLIED.md
Normal file
47
THEME_APPLIED.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# ✅ Тема успешно применена!
|
||||||
|
|
||||||
|
## Что было сделано
|
||||||
|
|
||||||
|
### 🎨 Система тем
|
||||||
|
- ✅ Создано 5 тем: Тёмная, Светлая, Фиолетовая, Синяя, Зелёная
|
||||||
|
- ✅ Каждая тема имеет уникальный градиент для логотипа "MC Panel"
|
||||||
|
- ✅ Селектор тем добавлен в header
|
||||||
|
- ✅ Выбранная тема сохраняется в localStorage
|
||||||
|
|
||||||
|
### 🎯 Градиенты для "MC Panel"
|
||||||
|
- **Тёмная**: синий → фиолетовый (from-blue-400 to-purple-600)
|
||||||
|
- **Светлая**: синий → фиолетовый (from-blue-600 to-purple-600)
|
||||||
|
- **Фиолетовая**: фиолетовый → розовый (from-purple-400 to-pink-600)
|
||||||
|
- **Синяя**: голубой → синий (from-cyan-400 to-blue-600)
|
||||||
|
- **Зелёная**: изумрудный → зелёный (from-emerald-400 to-green-600)
|
||||||
|
|
||||||
|
### 🎨 Современный интерфейс
|
||||||
|
- ✅ Дизайн в стиле TimeWeb Cloud
|
||||||
|
- ✅ Карточки с тенями и анимациями
|
||||||
|
- ✅ Плавные переходы между темами
|
||||||
|
- ✅ Адаптивный дизайн для мобильных устройств
|
||||||
|
- ✅ Sticky header с информацией о пользователе
|
||||||
|
- ✅ Анимированные индикаторы статуса серверов
|
||||||
|
|
||||||
|
### 📱 Адаптивность
|
||||||
|
- ✅ Скрываемая боковая панель на мобильных
|
||||||
|
- ✅ Адаптивные кнопки (текст скрывается на маленьких экранах)
|
||||||
|
- ✅ Горизонтальная прокрутка вкладок
|
||||||
|
|
||||||
|
## Как использовать
|
||||||
|
|
||||||
|
### Смена темы
|
||||||
|
1. Нажмите на селектор тем в правом верхнем углу
|
||||||
|
2. Выберите нужную тему из списка
|
||||||
|
3. Тема применится мгновенно и сохранится автоматически
|
||||||
|
|
||||||
|
### Файлы с темами
|
||||||
|
- `frontend/src/themes.js` - конфигурация всех тем
|
||||||
|
- `frontend/src/App.jsx` - главный компонент с темами
|
||||||
|
- `frontend/src/components/Auth.jsx` - страница входа с темами
|
||||||
|
- `frontend/src/components/ThemeSelector.jsx` - селектор тем
|
||||||
|
|
||||||
|
## Готово! 🎉
|
||||||
|
|
||||||
|
Панель теперь имеет современный интерфейс с 5 темами и градиентным логотипом "MC Panel".
|
||||||
|
Все компоненты автоматически используют цвета выбранной темы.
|
||||||
88
THEME_COMPLETE.md
Normal file
88
THEME_COMPLETE.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# ✅ Система тем полностью готова!
|
||||||
|
|
||||||
|
## Что было исправлено и улучшено
|
||||||
|
|
||||||
|
### 🎨 Градиентный логотип "MC Panel"
|
||||||
|
- ✅ Добавлен градиент в Auth.jsx (страница входа)
|
||||||
|
- ✅ Добавлен градиент в App.jsx (главная панель)
|
||||||
|
- ✅ Каждая тема имеет свой уникальный градиент:
|
||||||
|
- **Тёмная**: синий → фиолетовый
|
||||||
|
- **Светлая**: синий → фиолетовый
|
||||||
|
- **Фиолетовая**: фиолетовый → розовый
|
||||||
|
- **Синяя**: голубой → синий
|
||||||
|
- **Зелёная**: изумрудный → зелёный
|
||||||
|
|
||||||
|
### 🎯 Обновлённые компоненты
|
||||||
|
1. **ThemeSelector.jsx** - теперь использует динамические цвета из текущей темы
|
||||||
|
2. **CreateServerModal.jsx** - обновлён для использования тем с современным дизайном
|
||||||
|
3. **App.jsx** - добавлен градиент для логотипа
|
||||||
|
4. **Auth.jsx** - добавлен градиент для логотипа
|
||||||
|
|
||||||
|
### 📁 Структура файлов
|
||||||
|
```
|
||||||
|
frontend/src/
|
||||||
|
├── themes.js # Конфигурация всех тем
|
||||||
|
├── App.jsx # Главный компонент с темами ✅
|
||||||
|
├── components/
|
||||||
|
│ ├── Auth.jsx # Страница входа с темами ✅
|
||||||
|
│ ├── ThemeSelector.jsx # Селектор тем ✅
|
||||||
|
│ ├── CreateServerModal.jsx # Модальное окно создания сервера ✅
|
||||||
|
│ ├── Console.jsx # Получает theme prop
|
||||||
|
│ ├── FileManager.jsx # Получает theme prop
|
||||||
|
│ ├── Stats.jsx # Получает theme prop
|
||||||
|
│ ├── ServerSettings.jsx # Получает theme prop
|
||||||
|
│ └── Users.jsx # Получает theme prop
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Как использовать
|
||||||
|
|
||||||
|
### Запуск панели
|
||||||
|
```bash
|
||||||
|
# Терминал 1 - Бэкенд
|
||||||
|
cd backend
|
||||||
|
python main_new.py
|
||||||
|
|
||||||
|
# Терминал 2 - Фронтенд
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Смена темы
|
||||||
|
1. Откройте панель в браузере
|
||||||
|
2. Нажмите на иконку палитры (🎨) в правом верхнем углу
|
||||||
|
3. Выберите нужную тему из выпадающего меню
|
||||||
|
4. Тема применится мгновенно и сохранится автоматически
|
||||||
|
|
||||||
|
### Доступные темы
|
||||||
|
- 🌑 **Тёмная** - классическая тёмная тема (по умолчанию)
|
||||||
|
- ☀️ **Светлая** - светлая тема для дневного использования
|
||||||
|
- 💜 **Фиолетовая** - стильная фиолетовая палитра
|
||||||
|
- 💙 **Синяя** - холодная синяя тема
|
||||||
|
- 💚 **Зелёная** - природная зелёная тема
|
||||||
|
|
||||||
|
## ✨ Особенности
|
||||||
|
|
||||||
|
### Градиентный логотип
|
||||||
|
Логотип "MC Panel" теперь использует градиент, который меняется в зависимости от выбранной темы:
|
||||||
|
```jsx
|
||||||
|
<h1 className={`text-xl font-bold bg-gradient-to-r ${currentTheme.gradient} bg-clip-text text-transparent`}>
|
||||||
|
MC Panel
|
||||||
|
</h1>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Автоматическое сохранение
|
||||||
|
Выбранная тема автоматически сохраняется в `localStorage` и применяется при следующем входе.
|
||||||
|
|
||||||
|
### Плавные переходы
|
||||||
|
Все элементы интерфейса имеют плавные переходы при смене темы благодаря классу `transition-colors duration-300`.
|
||||||
|
|
||||||
|
### Адаптивный дизайн
|
||||||
|
- Скрываемая боковая панель на мобильных устройствах
|
||||||
|
- Адаптивные кнопки (текст скрывается на маленьких экранах)
|
||||||
|
- Горизонтальная прокрутка вкладок на мобильных
|
||||||
|
|
||||||
|
## 🎉 Готово!
|
||||||
|
|
||||||
|
Панель MC Panel теперь имеет полноценную систему тем с градиентным логотипом и современным интерфейсом в стиле TimeWeb Cloud. Все компоненты используют цвета из выбранной темы, обеспечивая единообразный и красивый дизайн.
|
||||||
|
|
||||||
|
Наслаждайтесь использованием! 🚀
|
||||||
125
THEME_UPDATE.md
Normal file
125
THEME_UPDATE.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# Обновление: Система тем и современный интерфейс
|
||||||
|
|
||||||
|
## Что добавлено
|
||||||
|
|
||||||
|
### 🎨 Система тем
|
||||||
|
- **5 тем на выбор:**
|
||||||
|
- Тёмная (по умолчанию)
|
||||||
|
- Светлая
|
||||||
|
- Фиолетовая
|
||||||
|
- Синяя
|
||||||
|
- Зелёная
|
||||||
|
|
||||||
|
### 🎯 Современный интерфейс в стиле TimeWeb Cloud
|
||||||
|
- Чистый и минималистичный дизайн
|
||||||
|
- Плавные переходы и анимации
|
||||||
|
- Адаптивная вёрстка
|
||||||
|
- Улучшенная типографика
|
||||||
|
- Современные карточки и кнопки
|
||||||
|
- Sticky header
|
||||||
|
- Анимированные индикаторы статуса
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
### 1. Замените App.jsx
|
||||||
|
|
||||||
|
Переименуйте файлы:
|
||||||
|
```
|
||||||
|
frontend/src/App.jsx → frontend/src/App_old.jsx (бэкап)
|
||||||
|
frontend/src/App_modern.jsx → frontend/src/App.jsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Перезапустите фронтенд
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
### Смена темы
|
||||||
|
|
||||||
|
1. Нажмите на иконку палитры (🎨) в правом верхнем углу
|
||||||
|
2. Выберите нужную тему из выпадающего меню
|
||||||
|
3. Тема сохраняется автоматически
|
||||||
|
|
||||||
|
### Особенности интерфейса
|
||||||
|
|
||||||
|
**Header:**
|
||||||
|
- Sticky (прилипает к верху при прокрутке)
|
||||||
|
- Показывает статус подключения
|
||||||
|
- Отображает текущего пользователя и роль
|
||||||
|
- Кнопки быстрого доступа
|
||||||
|
|
||||||
|
**Sidebar:**
|
||||||
|
- Список серверов с карточками
|
||||||
|
- Кнопки запуска/остановки на каждой карточке
|
||||||
|
- Анимированный индикатор статуса (пульсирует когда запущен)
|
||||||
|
- Скрывается на мобильных устройствах
|
||||||
|
|
||||||
|
**Вкладки:**
|
||||||
|
- Современный дизайн с подчёркиванием
|
||||||
|
- Иконки для каждой вкладки
|
||||||
|
- Плавные переходы
|
||||||
|
|
||||||
|
**Карточки серверов:**
|
||||||
|
- Закруглённые углы
|
||||||
|
- Тени при наведении
|
||||||
|
- Цветовая индикация выбранного сервера
|
||||||
|
- Плавные анимации
|
||||||
|
|
||||||
|
## Темы
|
||||||
|
|
||||||
|
### Тёмная (Dark)
|
||||||
|
- Основной: Серый 900
|
||||||
|
- Вторичный: Серый 800
|
||||||
|
- Акцент: Синий 600
|
||||||
|
|
||||||
|
### Светлая (Light)
|
||||||
|
- Основной: Серый 50
|
||||||
|
- Вторичный: Белый
|
||||||
|
- Акцент: Синий 600
|
||||||
|
|
||||||
|
### Фиолетовая (Purple)
|
||||||
|
- Основной: Фиолетовый 950
|
||||||
|
- Вторичный: Фиолетовый 900
|
||||||
|
- Акцент: Фиолетовый 600
|
||||||
|
|
||||||
|
### Синяя (Blue)
|
||||||
|
- Основной: Синий 950
|
||||||
|
- Вторичный: Синий 900
|
||||||
|
- Акцент: Синий 500
|
||||||
|
|
||||||
|
### Зелёная (Green)
|
||||||
|
- Основной: Зелёный 950
|
||||||
|
- Вторичный: Зелёный 900
|
||||||
|
- Акцент: Зелёный 600
|
||||||
|
|
||||||
|
## Файлы
|
||||||
|
|
||||||
|
- `frontend/src/App_modern.jsx` - новый App с темами
|
||||||
|
- `frontend/src/themes.js` - конфигурация тем
|
||||||
|
- `frontend/src/components/ThemeSelector.jsx` - селектор тем
|
||||||
|
|
||||||
|
## Что дальше
|
||||||
|
|
||||||
|
Компоненты (Console, FileManager, Stats, ServerSettings) нужно обновить для поддержки тем.
|
||||||
|
Они получают `theme` prop с текущей темой.
|
||||||
|
|
||||||
|
Пример использования в компоненте:
|
||||||
|
```jsx
|
||||||
|
export default function MyComponent({ theme }) {
|
||||||
|
return (
|
||||||
|
<div className={`${theme.primary} ${theme.text}`}>
|
||||||
|
<button className={`${theme.accent} ${theme.accentHover}`}>
|
||||||
|
Кнопка
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Готово! 🎉
|
||||||
|
|
||||||
|
Теперь у вас современный интерфейс с системой тем!
|
||||||
8
backend/.env.example
Normal file
8
backend/.env.example
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Секретный ключ для JWT (сгенерируйте свой!)
|
||||||
|
SECRET_KEY=your-secret-key-here-change-this-in-production
|
||||||
|
|
||||||
|
# Алгоритм шифрования
|
||||||
|
ALGORITHM=HS256
|
||||||
|
|
||||||
|
# Время жизни токена в минутах
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES=43200
|
||||||
152
backend/auth.py
Normal file
152
backend/auth.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional
|
||||||
|
from jose import JWTError, jwt
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
from fastapi import Depends, HTTPException, status
|
||||||
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SECRET_KEY = "mc-panel-secret-key-change-in-production"
|
||||||
|
ALGORITHM = "HS256"
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES = 43200 # 30 дней
|
||||||
|
|
||||||
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
security = HTTPBearer()
|
||||||
|
|
||||||
|
USERS_FILE = Path("data/users.json")
|
||||||
|
USERS_FILE.parent.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
def load_users():
|
||||||
|
if USERS_FILE.exists():
|
||||||
|
with open(USERS_FILE, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save_users(users):
|
||||||
|
with open(USERS_FILE, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(users, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
def verify_password(plain_password, hashed_password):
|
||||||
|
return pwd_context.verify(plain_password, hashed_password)
|
||||||
|
|
||||||
|
def get_password_hash(password):
|
||||||
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
|
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||||
|
to_encode = data.copy()
|
||||||
|
if expires_delta:
|
||||||
|
expire = datetime.utcnow() + expires_delta
|
||||||
|
else:
|
||||||
|
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
to_encode.update({"exp": expire})
|
||||||
|
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||||
|
return encoded_jwt
|
||||||
|
|
||||||
|
def decode_token(token: str):
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
|
return payload
|
||||||
|
except JWTError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
||||||
|
token = credentials.credentials
|
||||||
|
payload = decode_token(token)
|
||||||
|
|
||||||
|
if payload is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Неверный токен авторизации"
|
||||||
|
)
|
||||||
|
|
||||||
|
username: str = payload.get("sub")
|
||||||
|
if username is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Неверный токен авторизации"
|
||||||
|
)
|
||||||
|
|
||||||
|
users = load_users()
|
||||||
|
if username not in users:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Пользователь не найден"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"username": username, "role": users[username].get("role", "user")}
|
||||||
|
|
||||||
|
def authenticate_user(username: str, password: str):
|
||||||
|
users = load_users()
|
||||||
|
if username not in users:
|
||||||
|
return False
|
||||||
|
user = users[username]
|
||||||
|
if not verify_password(password, user["password"]):
|
||||||
|
return False
|
||||||
|
return user
|
||||||
|
|
||||||
|
def create_user(username: str, password: str, role: str = "user"):
|
||||||
|
users = load_users()
|
||||||
|
if username in users:
|
||||||
|
return False
|
||||||
|
|
||||||
|
users[username] = {
|
||||||
|
"password": get_password_hash(password),
|
||||||
|
"role": role,
|
||||||
|
"created_at": datetime.utcnow().isoformat(),
|
||||||
|
"servers": [] # Список серверов к которым есть доступ
|
||||||
|
}
|
||||||
|
save_users(users)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_user_servers(username: str):
|
||||||
|
"""Получить список серверов пользователя"""
|
||||||
|
users = load_users()
|
||||||
|
if username not in users:
|
||||||
|
return []
|
||||||
|
return users[username].get("servers", [])
|
||||||
|
|
||||||
|
def add_server_to_user(username: str, server_name: str):
|
||||||
|
"""Добавить сервер пользователю"""
|
||||||
|
users = load_users()
|
||||||
|
if username not in users:
|
||||||
|
return False
|
||||||
|
if "servers" not in users[username]:
|
||||||
|
users[username]["servers"] = []
|
||||||
|
if server_name not in users[username]["servers"]:
|
||||||
|
users[username]["servers"].append(server_name)
|
||||||
|
save_users(users)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def remove_server_from_user(username: str, server_name: str):
|
||||||
|
"""Удалить сервер у пользователя"""
|
||||||
|
users = load_users()
|
||||||
|
if username not in users:
|
||||||
|
return False
|
||||||
|
if "servers" in users[username] and server_name in users[username]["servers"]:
|
||||||
|
users[username]["servers"].remove(server_name)
|
||||||
|
save_users(users)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_server_users(server_name: str):
|
||||||
|
"""Получить список пользователей с доступом к серверу"""
|
||||||
|
users = load_users()
|
||||||
|
result = []
|
||||||
|
for username, user_data in users.items():
|
||||||
|
if server_name in user_data.get("servers", []):
|
||||||
|
result.append({
|
||||||
|
"username": username,
|
||||||
|
"role": user_data.get("role", "user")
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
||||||
|
def has_server_access(username: str, server_name: str):
|
||||||
|
"""Проверить есть ли доступ к серверу"""
|
||||||
|
users = load_users()
|
||||||
|
if username not in users:
|
||||||
|
return False
|
||||||
|
user = users[username]
|
||||||
|
# Админы имеют доступ ко всем серверам
|
||||||
|
if user.get("role") == "admin":
|
||||||
|
return True
|
||||||
|
return server_name in user.get("servers", [])
|
||||||
757
backend/main.py
Normal file
757
backend/main.py
Normal file
@@ -0,0 +1,757 @@
|
|||||||
|
from fastapi import FastAPI, WebSocket, UploadFile, File, HTTPException, Depends, status
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
import asyncio
|
||||||
|
import subprocess
|
||||||
|
import psutil
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
import json
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
from jose import JWTError, jwt
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
app = FastAPI(title="MC Panel")
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Настройки безопасности
|
||||||
|
SECRET_KEY = "your-secret-key-change-this-in-production-12345"
|
||||||
|
ALGORITHM = "HS256"
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 дней
|
||||||
|
|
||||||
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
security = HTTPBearer(auto_error=False)
|
||||||
|
|
||||||
|
SERVERS_DIR = Path("servers")
|
||||||
|
SERVERS_DIR.mkdir(exist_ok=True)
|
||||||
|
USERS_FILE = Path("users.json")
|
||||||
|
|
||||||
|
server_processes: dict[str, subprocess.Popen] = {}
|
||||||
|
server_logs: dict[str, list[str]] = {}
|
||||||
|
|
||||||
|
IS_WINDOWS = sys.platform == 'win32'
|
||||||
|
|
||||||
|
# Инициализация файла пользователей
|
||||||
|
def init_users():
|
||||||
|
if not USERS_FILE.exists():
|
||||||
|
admin_user = {
|
||||||
|
"username": "admin",
|
||||||
|
"password": pwd_context.hash("admin"),
|
||||||
|
"role": "admin",
|
||||||
|
"servers": []
|
||||||
|
}
|
||||||
|
save_users({"admin": admin_user})
|
||||||
|
print("Создан пользователь по умолчанию: admin / admin")
|
||||||
|
|
||||||
|
def load_users() -> dict:
|
||||||
|
if USERS_FILE.exists():
|
||||||
|
with open(USERS_FILE, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save_users(users: dict):
|
||||||
|
with open(USERS_FILE, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(users, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
def load_server_config(server_name: str) -> dict:
|
||||||
|
config_path = SERVERS_DIR / server_name / "panel_config.json"
|
||||||
|
if config_path.exists():
|
||||||
|
with open(config_path, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
return {
|
||||||
|
"name": server_name,
|
||||||
|
"displayName": server_name,
|
||||||
|
"startCommand": "java -Xmx2G -Xms1G -jar server.jar nogui"
|
||||||
|
}
|
||||||
|
|
||||||
|
def save_server_config(server_name: str, config: dict):
|
||||||
|
config_path = SERVERS_DIR / server_name / "panel_config.json"
|
||||||
|
with open(config_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(config, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
init_users()
|
||||||
|
|
||||||
|
# Функции аутентификации
|
||||||
|
def verify_password(plain_password, hashed_password):
|
||||||
|
return pwd_context.verify(plain_password, hashed_password)
|
||||||
|
|
||||||
|
def get_password_hash(password):
|
||||||
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
|
def create_access_token(data: dict):
|
||||||
|
to_encode = data.copy()
|
||||||
|
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
to_encode.update({"exp": expire})
|
||||||
|
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||||
|
return encoded_jwt
|
||||||
|
|
||||||
|
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
||||||
|
if not credentials:
|
||||||
|
raise HTTPException(status_code=401, detail="Требуется авторизация")
|
||||||
|
|
||||||
|
token = credentials.credentials
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
|
username: str = payload.get("sub")
|
||||||
|
if username is None:
|
||||||
|
raise HTTPException(status_code=401, detail="Неверный токен")
|
||||||
|
|
||||||
|
users = load_users()
|
||||||
|
if username not in users:
|
||||||
|
raise HTTPException(status_code=401, detail="Пользователь не найден")
|
||||||
|
|
||||||
|
return users[username]
|
||||||
|
except JWTError:
|
||||||
|
raise HTTPException(status_code=401, detail="Неверный токен")
|
||||||
|
|
||||||
|
def check_server_access(user: dict, server_name: str):
|
||||||
|
if user["role"] == "admin":
|
||||||
|
return True
|
||||||
|
return server_name in user.get("servers", [])
|
||||||
|
|
||||||
|
# API для аутентификации
|
||||||
|
@app.post("/api/auth/register")
|
||||||
|
async def register(data: dict):
|
||||||
|
users = load_users()
|
||||||
|
username = data.get("username", "").strip()
|
||||||
|
password = data.get("password", "").strip()
|
||||||
|
|
||||||
|
if not username or not password:
|
||||||
|
raise HTTPException(400, "Имя пользователя и пароль обязательны")
|
||||||
|
|
||||||
|
if username in users:
|
||||||
|
raise HTTPException(400, "Пользователь уже существует")
|
||||||
|
|
||||||
|
role = "admin" if len(users) == 0 else "user"
|
||||||
|
|
||||||
|
users[username] = {
|
||||||
|
"username": username,
|
||||||
|
"password": get_password_hash(password),
|
||||||
|
"role": role,
|
||||||
|
"servers": []
|
||||||
|
}
|
||||||
|
|
||||||
|
save_users(users)
|
||||||
|
|
||||||
|
access_token = create_access_token(data={"sub": username})
|
||||||
|
return {
|
||||||
|
"access_token": access_token,
|
||||||
|
"token_type": "bearer",
|
||||||
|
"username": username,
|
||||||
|
"role": role
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.post("/api/auth/login")
|
||||||
|
async def login(data: dict):
|
||||||
|
users = load_users()
|
||||||
|
username = data.get("username", "").strip()
|
||||||
|
password = data.get("password", "").strip()
|
||||||
|
|
||||||
|
if username not in users:
|
||||||
|
raise HTTPException(401, "Неверное имя пользователя или пароль")
|
||||||
|
|
||||||
|
user = users[username]
|
||||||
|
if not verify_password(password, user["password"]):
|
||||||
|
raise HTTPException(401, "Неверное имя пользователя или пароль")
|
||||||
|
|
||||||
|
access_token = create_access_token(data={"sub": username})
|
||||||
|
return {
|
||||||
|
"access_token": access_token,
|
||||||
|
"token_type": "bearer",
|
||||||
|
"username": username,
|
||||||
|
"role": user["role"]
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/api/auth/me")
|
||||||
|
async def get_me(user: dict = Depends(get_current_user)):
|
||||||
|
return {
|
||||||
|
"username": user["username"],
|
||||||
|
"role": user["role"],
|
||||||
|
"servers": user.get("servers", [])
|
||||||
|
}
|
||||||
|
|
||||||
|
# API для управления пользователями
|
||||||
|
@app.get("/api/users")
|
||||||
|
async def get_users(user: dict = Depends(get_current_user)):
|
||||||
|
# Админы видят всех пользователей
|
||||||
|
# Обычные пользователи тоже видят всех (для управления доступом к своим серверам)
|
||||||
|
users = load_users()
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"username": u["username"],
|
||||||
|
"role": u["role"],
|
||||||
|
"servers": u.get("servers", [])
|
||||||
|
}
|
||||||
|
for u in users.values()
|
||||||
|
]
|
||||||
|
|
||||||
|
@app.put("/api/users/{username}/servers")
|
||||||
|
async def update_user_servers(username: str, data: dict, user: dict = Depends(get_current_user)):
|
||||||
|
users = load_users()
|
||||||
|
if username not in users:
|
||||||
|
raise HTTPException(404, "Пользователь не найден")
|
||||||
|
|
||||||
|
# Админы могут управлять доступом к любым серверам
|
||||||
|
if user["role"] == "admin":
|
||||||
|
users[username]["servers"] = data.get("servers", [])
|
||||||
|
save_users(users)
|
||||||
|
return {"message": "Доступ обновлен"}
|
||||||
|
|
||||||
|
# Обычные пользователи могут управлять доступом только к своим серверам
|
||||||
|
requested_servers = data.get("servers", [])
|
||||||
|
current_servers = users[username].get("servers", [])
|
||||||
|
|
||||||
|
# Проверяем, что пользователь пытается изменить доступ только к своим серверам
|
||||||
|
for server_name in requested_servers:
|
||||||
|
if server_name not in current_servers:
|
||||||
|
# Проверяем, является ли текущий пользователь владельцем этого сервера
|
||||||
|
config = load_server_config(server_name)
|
||||||
|
if config.get("owner") != user["username"]:
|
||||||
|
raise HTTPException(403, f"Вы не можете выдать доступ к серверу {server_name}")
|
||||||
|
|
||||||
|
users[username]["servers"] = requested_servers
|
||||||
|
save_users(users)
|
||||||
|
return {"message": "Доступ обновлен"}
|
||||||
|
|
||||||
|
@app.delete("/api/users/{username}")
|
||||||
|
async def delete_user(username: str, user: dict = Depends(get_current_user)):
|
||||||
|
if user["role"] != "admin":
|
||||||
|
raise HTTPException(403, "Доступ запрещен")
|
||||||
|
|
||||||
|
if username == user["username"]:
|
||||||
|
raise HTTPException(400, "Нельзя удалить самого себя")
|
||||||
|
|
||||||
|
users = load_users()
|
||||||
|
if username not in users:
|
||||||
|
raise HTTPException(404, "Пользователь не найден")
|
||||||
|
|
||||||
|
del users[username]
|
||||||
|
save_users(users)
|
||||||
|
|
||||||
|
return {"message": "Пользователь удален"}
|
||||||
|
|
||||||
|
@app.put("/api/users/{username}/role")
|
||||||
|
async def update_user_role(username: str, data: dict, user: dict = Depends(get_current_user)):
|
||||||
|
if user["role"] != "admin":
|
||||||
|
raise HTTPException(403, "Доступ запрещен")
|
||||||
|
|
||||||
|
if username == user["username"]:
|
||||||
|
raise HTTPException(400, "Нельзя изменить свою роль")
|
||||||
|
|
||||||
|
users = load_users()
|
||||||
|
if username not in users:
|
||||||
|
raise HTTPException(404, "Пользователь не найден")
|
||||||
|
|
||||||
|
new_role = data.get("role")
|
||||||
|
if new_role not in ["admin", "user"]:
|
||||||
|
raise HTTPException(400, "Неверная роль")
|
||||||
|
|
||||||
|
users[username]["role"] = new_role
|
||||||
|
save_users(users)
|
||||||
|
|
||||||
|
return {"message": "Роль обновлена"}
|
||||||
|
|
||||||
|
# API для серверов
|
||||||
|
@app.get("/api/servers")
|
||||||
|
async def get_servers(user: dict = Depends(get_current_user)):
|
||||||
|
servers = []
|
||||||
|
try:
|
||||||
|
for server_dir in SERVERS_DIR.iterdir():
|
||||||
|
if server_dir.is_dir():
|
||||||
|
if user["role"] != "admin" and server_dir.name not in user.get("servers", []):
|
||||||
|
continue
|
||||||
|
|
||||||
|
config = load_server_config(server_dir.name)
|
||||||
|
|
||||||
|
is_running = False
|
||||||
|
if server_dir.name in server_processes:
|
||||||
|
process = server_processes[server_dir.name]
|
||||||
|
if process.poll() is None:
|
||||||
|
is_running = True
|
||||||
|
else:
|
||||||
|
del server_processes[server_dir.name]
|
||||||
|
|
||||||
|
servers.append({
|
||||||
|
"name": server_dir.name,
|
||||||
|
"displayName": config.get("displayName", server_dir.name),
|
||||||
|
"status": "running" if is_running else "stopped"
|
||||||
|
})
|
||||||
|
print(f"Найдено серверов для {user['username']}: {len(servers)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка загрузки серверов: {e}")
|
||||||
|
return servers
|
||||||
|
|
||||||
|
@app.post("/api/servers/create")
|
||||||
|
async def create_server(data: dict, user: dict = Depends(get_current_user)):
|
||||||
|
server_name = data.get("name", "").strip()
|
||||||
|
if not server_name or not server_name.replace("_", "").replace("-", "").isalnum():
|
||||||
|
raise HTTPException(400, "Недопустимое имя сервера")
|
||||||
|
|
||||||
|
server_path = SERVERS_DIR / server_name
|
||||||
|
if server_path.exists():
|
||||||
|
raise HTTPException(400, "Сервер с таким именем уже существует")
|
||||||
|
|
||||||
|
server_path.mkdir(parents=True)
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"name": server_name,
|
||||||
|
"displayName": data.get("displayName", server_name),
|
||||||
|
"startCommand": data.get("startCommand", "java -Xmx2G -Xms1G -jar server.jar nogui"),
|
||||||
|
"owner": user["username"] # Сохраняем владельца
|
||||||
|
}
|
||||||
|
save_server_config(server_name, config)
|
||||||
|
|
||||||
|
# Если пользователь не админ, автоматически выдаем ему доступ
|
||||||
|
if user["role"] != "admin":
|
||||||
|
users = load_users()
|
||||||
|
if user["username"] in users:
|
||||||
|
if "servers" not in users[user["username"]]:
|
||||||
|
users[user["username"]]["servers"] = []
|
||||||
|
if server_name not in users[user["username"]]["servers"]:
|
||||||
|
users[user["username"]]["servers"].append(server_name)
|
||||||
|
save_users(users)
|
||||||
|
|
||||||
|
return {"message": "Сервер создан", "name": server_name}
|
||||||
|
|
||||||
|
@app.get("/api/servers/{server_name}/config")
|
||||||
|
async def get_server_config(server_name: str, user: dict = Depends(get_current_user)):
|
||||||
|
if not check_server_access(user, server_name):
|
||||||
|
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||||
|
|
||||||
|
server_path = SERVERS_DIR / server_name
|
||||||
|
if not server_path.exists():
|
||||||
|
raise HTTPException(404, "Сервер не найден")
|
||||||
|
|
||||||
|
config = load_server_config(server_name)
|
||||||
|
print(f"Загружена конфигурация для {server_name}: {config}")
|
||||||
|
return config
|
||||||
|
|
||||||
|
@app.put("/api/servers/{server_name}/config")
|
||||||
|
async def update_server_config(server_name: str, config: dict, user: dict = Depends(get_current_user)):
|
||||||
|
if not check_server_access(user, server_name):
|
||||||
|
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||||
|
|
||||||
|
server_path = SERVERS_DIR / server_name
|
||||||
|
if not server_path.exists():
|
||||||
|
raise HTTPException(404, "Сервер не найден")
|
||||||
|
|
||||||
|
if server_name in server_processes:
|
||||||
|
raise HTTPException(400, "Остановите сервер перед изменением настроек")
|
||||||
|
|
||||||
|
save_server_config(server_name, config)
|
||||||
|
return {"message": "Настройки сохранены"}
|
||||||
|
|
||||||
|
@app.delete("/api/servers/{server_name}")
|
||||||
|
async def delete_server(server_name: str, user: dict = Depends(get_current_user)):
|
||||||
|
if user["role"] != "admin":
|
||||||
|
raise HTTPException(403, "Только администраторы могут удалять серверы")
|
||||||
|
|
||||||
|
server_path = SERVERS_DIR / server_name
|
||||||
|
if not server_path.exists():
|
||||||
|
raise HTTPException(404, "Сервер не найден")
|
||||||
|
|
||||||
|
if server_name in server_processes:
|
||||||
|
raise HTTPException(400, "Остановите сервер перед удалением")
|
||||||
|
|
||||||
|
shutil.rmtree(server_path)
|
||||||
|
return {"message": "Сервер удален"}
|
||||||
|
|
||||||
|
# Управление процессами серверов
|
||||||
|
async def read_server_output(server_name: str, process: subprocess.Popen):
|
||||||
|
try:
|
||||||
|
print(f"Начало чтения вывода для сервера {server_name}")
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if process.poll() is not None:
|
||||||
|
print(f"Процесс сервера {server_name} завершился с кодом {process.poll()}")
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
line = await loop.run_in_executor(None, process.stdout.readline)
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
|
||||||
|
line = line.strip()
|
||||||
|
if line:
|
||||||
|
if server_name not in server_logs:
|
||||||
|
server_logs[server_name] = []
|
||||||
|
server_logs[server_name].append(line)
|
||||||
|
|
||||||
|
if len(server_logs[server_name]) > 1000:
|
||||||
|
server_logs[server_name].pop(0)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка чтения строки для {server_name}: {e}")
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка чтения вывода сервера {server_name}: {e}")
|
||||||
|
finally:
|
||||||
|
print(f"Чтение вывода для сервера {server_name} завершено")
|
||||||
|
if server_name in server_processes and process.poll() is not None:
|
||||||
|
del server_processes[server_name]
|
||||||
|
print(f"Сервер {server_name} удален из списка процессов")
|
||||||
|
|
||||||
|
@app.post("/api/servers/{server_name}/start")
|
||||||
|
async def start_server(server_name: str, user: dict = Depends(get_current_user)):
|
||||||
|
if not check_server_access(user, server_name):
|
||||||
|
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||||
|
|
||||||
|
server_path = SERVERS_DIR / server_name
|
||||||
|
if not server_path.exists():
|
||||||
|
raise HTTPException(404, "Сервер не найден")
|
||||||
|
|
||||||
|
if server_name in server_processes:
|
||||||
|
raise HTTPException(400, "Сервер уже запущен")
|
||||||
|
|
||||||
|
config = load_server_config(server_name)
|
||||||
|
start_command = config.get("startCommand", "java -Xmx2G -Xms1G -jar server.jar nogui")
|
||||||
|
|
||||||
|
cmd_parts = start_command.split()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if IS_WINDOWS:
|
||||||
|
process = subprocess.Popen(
|
||||||
|
cmd_parts,
|
||||||
|
cwd=server_path,
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
bufsize=1,
|
||||||
|
creationflags=subprocess.CREATE_NO_WINDOW
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
process = subprocess.Popen(
|
||||||
|
cmd_parts,
|
||||||
|
cwd=server_path,
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
bufsize=1
|
||||||
|
)
|
||||||
|
|
||||||
|
server_processes[server_name] = process
|
||||||
|
server_logs[server_name] = []
|
||||||
|
|
||||||
|
asyncio.create_task(read_server_output(server_name, process))
|
||||||
|
|
||||||
|
print(f"Сервер {server_name} запущен с PID {process.pid}")
|
||||||
|
return {"message": "Сервер запущен", "pid": process.pid}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка запуска сервера {server_name}: {e}")
|
||||||
|
raise HTTPException(500, f"Ошибка запуска сервера: {str(e)}")
|
||||||
|
|
||||||
|
@app.post("/api/servers/{server_name}/stop")
|
||||||
|
async def stop_server(server_name: str, user: dict = Depends(get_current_user)):
|
||||||
|
if not check_server_access(user, server_name):
|
||||||
|
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||||
|
|
||||||
|
if server_name not in server_processes:
|
||||||
|
raise HTTPException(400, "Сервер не запущен")
|
||||||
|
|
||||||
|
process = server_processes[server_name]
|
||||||
|
|
||||||
|
try:
|
||||||
|
if process.stdin and not process.stdin.closed:
|
||||||
|
process.stdin.write("stop\n")
|
||||||
|
process.stdin.flush()
|
||||||
|
|
||||||
|
try:
|
||||||
|
process.wait(timeout=30)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
print(f"Сервер {server_name} не остановился за 30 секунд, принудительное завершение")
|
||||||
|
process.kill()
|
||||||
|
process.wait()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при остановке сервера {server_name}: {e}")
|
||||||
|
try:
|
||||||
|
process.kill()
|
||||||
|
process.wait()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
if server_name in server_processes:
|
||||||
|
del server_processes[server_name]
|
||||||
|
print(f"Сервер {server_name} остановлен")
|
||||||
|
|
||||||
|
return {"message": "Сервер остановлен"}
|
||||||
|
|
||||||
|
@app.post("/api/servers/{server_name}/command")
|
||||||
|
async def send_command(server_name: str, command: dict, user: dict = Depends(get_current_user)):
|
||||||
|
if not check_server_access(user, server_name):
|
||||||
|
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||||
|
|
||||||
|
if server_name not in server_processes:
|
||||||
|
raise HTTPException(400, "Сервер не запущен")
|
||||||
|
|
||||||
|
process = server_processes[server_name]
|
||||||
|
|
||||||
|
if process.poll() is not None:
|
||||||
|
del server_processes[server_name]
|
||||||
|
raise HTTPException(400, "Сервер не запущен")
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmd = command["command"]
|
||||||
|
if process.stdin and not process.stdin.closed:
|
||||||
|
process.stdin.write(cmd + "\n")
|
||||||
|
process.stdin.flush()
|
||||||
|
print(f"Команда отправлена серверу {server_name}: {cmd}")
|
||||||
|
return {"message": "Команда отправлена"}
|
||||||
|
else:
|
||||||
|
raise HTTPException(400, "Невозможно отправить команду")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка отправки команды серверу {server_name}: {e}")
|
||||||
|
raise HTTPException(500, f"Ошибка отправки команды: {str(e)}")
|
||||||
|
|
||||||
|
@app.get("/api/servers/{server_name}/stats")
|
||||||
|
async def get_server_stats(server_name: str, user: dict = Depends(get_current_user)):
|
||||||
|
if not check_server_access(user, server_name):
|
||||||
|
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||||
|
|
||||||
|
server_path = SERVERS_DIR / server_name
|
||||||
|
|
||||||
|
try:
|
||||||
|
disk_usage = sum(f.stat().st_size for f in server_path.rglob('*') if f.is_file())
|
||||||
|
disk_mb = disk_usage / 1024 / 1024
|
||||||
|
except:
|
||||||
|
disk_mb = 0
|
||||||
|
|
||||||
|
if server_name not in server_processes:
|
||||||
|
return {
|
||||||
|
"status": "stopped",
|
||||||
|
"cpu": 0,
|
||||||
|
"memory": 0,
|
||||||
|
"disk": round(disk_mb, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
process = server_processes[server_name]
|
||||||
|
try:
|
||||||
|
if process.poll() is not None:
|
||||||
|
del server_processes[server_name]
|
||||||
|
return {
|
||||||
|
"status": "stopped",
|
||||||
|
"cpu": 0,
|
||||||
|
"memory": 0,
|
||||||
|
"disk": round(disk_mb, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
proc = psutil.Process(process.pid)
|
||||||
|
memory_mb = proc.memory_info().rss / 1024 / 1024
|
||||||
|
cpu_percent = proc.cpu_percent(interval=0.1)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "running",
|
||||||
|
"cpu": round(cpu_percent, 2),
|
||||||
|
"memory": round(memory_mb, 2),
|
||||||
|
"disk": round(disk_mb, 2)
|
||||||
|
}
|
||||||
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||||
|
if server_name in server_processes:
|
||||||
|
del server_processes[server_name]
|
||||||
|
return {
|
||||||
|
"status": "stopped",
|
||||||
|
"cpu": 0,
|
||||||
|
"memory": 0,
|
||||||
|
"disk": round(disk_mb, 2)
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка получения статистики для {server_name}: {e}")
|
||||||
|
return {
|
||||||
|
"status": "unknown",
|
||||||
|
"cpu": 0,
|
||||||
|
"memory": 0,
|
||||||
|
"disk": round(disk_mb, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.websocket("/ws/servers/{server_name}/console")
|
||||||
|
async def console_websocket(websocket: WebSocket, server_name: str):
|
||||||
|
await websocket.accept()
|
||||||
|
print(f"WebSocket подключен для сервера: {server_name}")
|
||||||
|
|
||||||
|
if server_name in server_logs:
|
||||||
|
print(f"Отправка {len(server_logs[server_name])} существующих логов")
|
||||||
|
for log in server_logs[server_name]:
|
||||||
|
await websocket.send_text(log)
|
||||||
|
else:
|
||||||
|
print(f"Логов для сервера {server_name} пока нет")
|
||||||
|
await websocket.send_text(f"[Панель] Ожидание логов от сервера {server_name}...")
|
||||||
|
|
||||||
|
last_sent_index = len(server_logs.get(server_name, []))
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
if server_name in server_logs:
|
||||||
|
current_logs = server_logs[server_name]
|
||||||
|
if len(current_logs) > last_sent_index:
|
||||||
|
for log in current_logs[last_sent_index:]:
|
||||||
|
await websocket.send_text(log)
|
||||||
|
last_sent_index = len(current_logs)
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"WebSocket ошибка: {e}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
# API для файлов
|
||||||
|
@app.get("/api/servers/{server_name}/files")
|
||||||
|
async def list_files(server_name: str, path: str = "", user: dict = Depends(get_current_user)):
|
||||||
|
if not check_server_access(user, server_name):
|
||||||
|
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||||
|
|
||||||
|
server_path = SERVERS_DIR / server_name
|
||||||
|
if not server_path.exists():
|
||||||
|
raise HTTPException(404, "Сервер не найден")
|
||||||
|
|
||||||
|
target_path = server_path / path if path else server_path
|
||||||
|
|
||||||
|
try:
|
||||||
|
target_path = target_path.resolve()
|
||||||
|
server_path = server_path.resolve()
|
||||||
|
if not str(target_path).startswith(str(server_path)):
|
||||||
|
raise HTTPException(403, "Доступ запрещен")
|
||||||
|
except:
|
||||||
|
raise HTTPException(404, "Путь не найден")
|
||||||
|
|
||||||
|
if not target_path.exists():
|
||||||
|
raise HTTPException(404, "Путь не найден")
|
||||||
|
|
||||||
|
if not target_path.is_dir():
|
||||||
|
raise HTTPException(400, "Путь не является директорией")
|
||||||
|
|
||||||
|
files = []
|
||||||
|
try:
|
||||||
|
for item in target_path.iterdir():
|
||||||
|
files.append({
|
||||||
|
"name": item.name,
|
||||||
|
"type": "directory" if item.is_dir() else "file",
|
||||||
|
"size": item.stat().st_size if item.is_file() else 0
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка чтения директории: {e}")
|
||||||
|
raise HTTPException(500, f"Ошибка чтения директории: {str(e)}")
|
||||||
|
|
||||||
|
return files
|
||||||
|
|
||||||
|
@app.get("/api/servers/{server_name}/files/download")
|
||||||
|
async def download_file(server_name: str, path: str, user: dict = Depends(get_current_user)):
|
||||||
|
if not check_server_access(user, server_name):
|
||||||
|
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||||
|
|
||||||
|
server_path = SERVERS_DIR / server_name
|
||||||
|
file_path = server_path / path
|
||||||
|
|
||||||
|
if not file_path.exists() or not str(file_path).startswith(str(server_path)):
|
||||||
|
raise HTTPException(404, "Файл не найден")
|
||||||
|
|
||||||
|
return FileResponse(file_path, filename=file_path.name)
|
||||||
|
|
||||||
|
@app.post("/api/servers/{server_name}/files/upload")
|
||||||
|
async def upload_file(server_name: str, path: str, file: UploadFile = File(...), user: dict = Depends(get_current_user)):
|
||||||
|
if not check_server_access(user, server_name):
|
||||||
|
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||||
|
|
||||||
|
server_path = SERVERS_DIR / server_name
|
||||||
|
target_path = server_path / path / file.filename
|
||||||
|
|
||||||
|
if not str(target_path).startswith(str(server_path)):
|
||||||
|
raise HTTPException(400, "Недопустимый путь")
|
||||||
|
|
||||||
|
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with open(target_path, "wb") as f:
|
||||||
|
content = await file.read()
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
return {"message": "Файл загружен"}
|
||||||
|
|
||||||
|
@app.delete("/api/servers/{server_name}/files")
|
||||||
|
async def delete_file(server_name: str, path: str, user: dict = Depends(get_current_user)):
|
||||||
|
if not check_server_access(user, server_name):
|
||||||
|
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||||
|
|
||||||
|
server_path = SERVERS_DIR / server_name
|
||||||
|
target_path = server_path / path
|
||||||
|
|
||||||
|
if not target_path.exists() or not str(target_path).startswith(str(server_path)):
|
||||||
|
raise HTTPException(404, "Файл не найден")
|
||||||
|
|
||||||
|
if target_path.is_dir():
|
||||||
|
shutil.rmtree(target_path)
|
||||||
|
else:
|
||||||
|
target_path.unlink()
|
||||||
|
|
||||||
|
return {"message": "Файл удален"}
|
||||||
|
|
||||||
|
@app.get("/api/servers/{server_name}/files/content")
|
||||||
|
async def get_file_content(server_name: str, path: str, user: dict = Depends(get_current_user)):
|
||||||
|
if not check_server_access(user, server_name):
|
||||||
|
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||||
|
|
||||||
|
server_path = SERVERS_DIR / server_name
|
||||||
|
file_path = server_path / path
|
||||||
|
|
||||||
|
if not file_path.exists() or not file_path.is_file() or not str(file_path).startswith(str(server_path)):
|
||||||
|
raise HTTPException(404, "Файл не найден")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
return {"content": content}
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
raise HTTPException(400, "Файл не является текстовым")
|
||||||
|
|
||||||
|
@app.put("/api/servers/{server_name}/files/content")
|
||||||
|
async def update_file_content(server_name: str, path: str, data: dict, user: dict = Depends(get_current_user)):
|
||||||
|
if not check_server_access(user, server_name):
|
||||||
|
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||||
|
|
||||||
|
server_path = SERVERS_DIR / server_name
|
||||||
|
file_path = server_path / path
|
||||||
|
|
||||||
|
if not file_path.exists() or not file_path.is_file() or not str(file_path).startswith(str(server_path)):
|
||||||
|
raise HTTPException(404, "Файл не найден")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(data.get("content", ""))
|
||||||
|
return {"message": "Файл сохранен"}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(400, f"Ошибка сохранения файла: {str(e)}")
|
||||||
|
|
||||||
|
@app.put("/api/servers/{server_name}/files/rename")
|
||||||
|
async def rename_file(server_name: str, old_path: str, new_name: str, user: dict = Depends(get_current_user)):
|
||||||
|
if not check_server_access(user, server_name):
|
||||||
|
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||||
|
|
||||||
|
server_path = SERVERS_DIR / server_name
|
||||||
|
old_file_path = server_path / old_path
|
||||||
|
|
||||||
|
if not old_file_path.exists() or not str(old_file_path).startswith(str(server_path)):
|
||||||
|
raise HTTPException(404, "Файл не найден")
|
||||||
|
|
||||||
|
new_file_path = old_file_path.parent / new_name
|
||||||
|
|
||||||
|
if new_file_path.exists():
|
||||||
|
raise HTTPException(400, "Файл с таким именем уже существует")
|
||||||
|
|
||||||
|
if not str(new_file_path).startswith(str(server_path)):
|
||||||
|
raise HTTPException(400, "Недопустимое имя файла")
|
||||||
|
|
||||||
|
old_file_path.rename(new_file_path)
|
||||||
|
return {"message": "Файл переименован"}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
23
backend/models.py
Normal file
23
backend/models.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
class UserRegister(BaseModel):
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
class UserLogin(BaseModel):
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
class Token(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
token_type: str
|
||||||
|
username: str
|
||||||
|
role: str
|
||||||
|
|
||||||
|
class ServerAccess(BaseModel):
|
||||||
|
username: str
|
||||||
|
server_name: str
|
||||||
|
|
||||||
|
class ServerAccessList(BaseModel):
|
||||||
|
users: List[dict]
|
||||||
12
backend/requirements.txt
Normal file
12
backend/requirements.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
fastapi==0.109.0
|
||||||
|
uvicorn[standard]==0.27.0
|
||||||
|
websockets==12.0
|
||||||
|
psutil==5.9.8
|
||||||
|
aiofiles==23.2.1
|
||||||
|
python-multipart==0.0.6
|
||||||
|
pydantic==2.5.3
|
||||||
|
passlib[bcrypt]==1.7.4
|
||||||
|
python-jose[cryptography]==3.3.0
|
||||||
|
python-jose[cryptography]==3.3.0
|
||||||
|
passlib[bcrypt]==1.7.4
|
||||||
|
python-dotenv==1.0.0
|
||||||
5
backend/start.bat
Normal file
5
backend/start.bat
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@echo off
|
||||||
|
echo Starting MC Panel Backend...
|
||||||
|
cd /d "%~dp0"
|
||||||
|
python main.py
|
||||||
|
pause
|
||||||
17
backend/users.json
Normal file
17
backend/users.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"admin": {
|
||||||
|
"username": "admin",
|
||||||
|
"password": "$2b$12$0AJU/Cc6vI.gqUY6BfU8E.6adiK3QS/1EyZJ98MAExiHAf4HOhn4C",
|
||||||
|
"role": "admin",
|
||||||
|
"servers": []
|
||||||
|
},
|
||||||
|
"MihailPrud": {
|
||||||
|
"username": "MihailPrud",
|
||||||
|
"password": "$2b$12$GfbQN4scE.b.mtUHofWWE.Dn1tQpT1zwLAxeICv90sHP4zGv0dc2G",
|
||||||
|
"role": "user",
|
||||||
|
"servers": [
|
||||||
|
"test",
|
||||||
|
"nya"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
1
frontend/.env
Normal file
1
frontend/.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_URL=http://26.62.117.104:8000
|
||||||
3
frontend/.env.example
Normal file
3
frontend/.env.example
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# API URL (необязательно, по умолчанию определяется автоматически)
|
||||||
|
# Раскомментируйте и укажите ваш IP для удаленного доступа
|
||||||
|
# VITE_API_URL=http://26.123.45.67:8000
|
||||||
10
frontend/.env.local.example
Normal file
10
frontend/.env.local.example
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Создайте файл .env.local и раскомментируйте нужную строку
|
||||||
|
|
||||||
|
# Для локального использования (по умолчанию)
|
||||||
|
# VITE_API_URL=http://localhost:8000
|
||||||
|
|
||||||
|
# Для Radmin VPN (замените на ваш IP)
|
||||||
|
# VITE_API_URL=http://26.62.117.104:8000
|
||||||
|
|
||||||
|
# Для Hamachi (замените на ваш IP)
|
||||||
|
# VITE_API_URL=http://25.123.45.67:8000
|
||||||
12
frontend/index.html
Normal file
12
frontend/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>MC Panel</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2988
frontend/package-lock.json
generated
Normal file
2988
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
frontend/package.json
Normal file
28
frontend/package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "mc-panel",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview --host"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.22.0",
|
||||||
|
"axios": "^1.6.7",
|
||||||
|
"@xterm/xterm": "^5.3.0",
|
||||||
|
"lucide-react": "^0.323.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.55",
|
||||||
|
"@types/react-dom": "^18.2.19",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"autoprefixer": "^10.4.17",
|
||||||
|
"postcss": "^8.4.35",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"vite": "^5.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
392
frontend/src/App.jsx
Normal file
392
frontend/src/App.jsx
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Server, Play, Square, Terminal, FolderOpen, HardDrive, Settings, Plus, Users as UsersIcon, LogOut, Menu, X } from 'lucide-react';
|
||||||
|
import Console from './components/Console';
|
||||||
|
import FileManager from './components/FileManager';
|
||||||
|
import Stats from './components/Stats';
|
||||||
|
import ServerSettings from './components/ServerSettings';
|
||||||
|
import CreateServerModal from './components/CreateServerModal';
|
||||||
|
import Users from './components/Users';
|
||||||
|
import Auth from './components/Auth';
|
||||||
|
import ErrorBoundary from './components/ErrorBoundary';
|
||||||
|
import ThemeSelector from './components/ThemeSelector';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { API_URL } from './config';
|
||||||
|
import { getTheme } from './themes';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [token, setToken] = useState(localStorage.getItem('token'));
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const [servers, setServers] = useState([]);
|
||||||
|
const [selectedServer, setSelectedServer] = useState(null);
|
||||||
|
const [activeTab, setActiveTab] = useState('console');
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
|
const [showUsers, setShowUsers] = useState(false);
|
||||||
|
const [connectionError, setConnectionError] = useState(false);
|
||||||
|
const [theme, setTheme] = useState(localStorage.getItem('theme') || 'dark');
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
|
|
||||||
|
const currentTheme = getTheme(theme);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (token) {
|
||||||
|
loadUser();
|
||||||
|
loadServers();
|
||||||
|
const interval = setInterval(loadServers, 5000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
const loadUser = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${API_URL}/api/auth/me`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
setUser(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки пользователя:', error);
|
||||||
|
handleLogout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadServers = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${API_URL}/api/servers`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
setServers(data);
|
||||||
|
setConnectionError(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки серверов:', error);
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
handleLogout();
|
||||||
|
} else {
|
||||||
|
setConnectionError(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = async (username, password, isLogin) => {
|
||||||
|
const endpoint = isLogin ? '/api/auth/login' : '/api/auth/register';
|
||||||
|
const { data } = await axios.post(`${API_URL}${endpoint}`, {
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
|
||||||
|
localStorage.setItem('token', data.access_token);
|
||||||
|
setToken(data.access_token);
|
||||||
|
setUser({ username: data.username, role: data.role });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
setToken(null);
|
||||||
|
setUser(null);
|
||||||
|
setServers([]);
|
||||||
|
setSelectedServer(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleServerDeleted = () => {
|
||||||
|
setSelectedServer(null);
|
||||||
|
loadServers();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleThemeChange = (newTheme) => {
|
||||||
|
setTheme(newTheme);
|
||||||
|
localStorage.setItem('theme', newTheme);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startServer = async (serverName) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${API_URL}/api/servers/${serverName}/start`,
|
||||||
|
{},
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
console.log('Сервер запущен:', response.data);
|
||||||
|
setTimeout(() => {
|
||||||
|
loadServers();
|
||||||
|
}, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка запуска сервера:', error);
|
||||||
|
alert(error.response?.data?.detail || 'Ошибка запуска сервера');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopServer = async (serverName) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${API_URL}/api/servers/${serverName}/stop`,
|
||||||
|
{},
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
console.log('Сервер остановлен:', response.data);
|
||||||
|
setTimeout(() => {
|
||||||
|
loadServers();
|
||||||
|
}, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка остановки сервера:', error);
|
||||||
|
alert(error.response?.data?.detail || 'Ошибка остановки сервера');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return <Auth onLogin={handleLogin} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showUsers) {
|
||||||
|
return (
|
||||||
|
<div className={`min-h-screen ${currentTheme.primary} ${currentTheme.text} transition-colors duration-300`}>
|
||||||
|
<header className={`${currentTheme.secondary} ${currentTheme.border} border-b backdrop-blur-sm bg-opacity-95 sticky top-0 z-40`}>
|
||||||
|
<div className="px-6 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`${currentTheme.accent} p-2 rounded-lg`}>
|
||||||
|
<Server className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-bold">MC Panel</h1>
|
||||||
|
<p className={`text-xs ${currentTheme.textSecondary}`}>Управление серверами</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`px-3 py-1.5 rounded-lg ${currentTheme.card} ${currentTheme.border} border`}>
|
||||||
|
<span className={`text-sm ${currentTheme.textSecondary}`}>
|
||||||
|
{user?.username}
|
||||||
|
</span>
|
||||||
|
<span className={`ml-2 text-xs px-2 py-0.5 rounded ${currentTheme.accent} text-white`}>
|
||||||
|
{user?.role === 'admin' ? 'Админ' : 'Пользователь'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ThemeSelector currentTheme={theme} onThemeChange={handleThemeChange} />
|
||||||
|
<button
|
||||||
|
onClick={() => setShowUsers(false)}
|
||||||
|
className={`${currentTheme.card} ${currentTheme.hover} px-4 py-2 rounded-lg transition flex items-center gap-2`}
|
||||||
|
>
|
||||||
|
<Server className="w-4 h-4" />
|
||||||
|
Серверы
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className={`${currentTheme.danger} hover:opacity-90 px-4 py-2 rounded-lg transition flex items-center gap-2 text-white`}
|
||||||
|
>
|
||||||
|
<LogOut className="w-4 h-4" />
|
||||||
|
Выход
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<Users token={token} theme={currentTheme} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`min-h-screen ${currentTheme.primary} ${currentTheme.text} transition-colors duration-300`}>
|
||||||
|
{/* Header */}
|
||||||
|
<header className={`${currentTheme.secondary} ${currentTheme.border} border-b backdrop-blur-sm bg-opacity-95 sticky top-0 z-40`}>
|
||||||
|
<div className="px-6 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||||
|
className={`lg:hidden ${currentTheme.hover} p-2 rounded-lg transition`}
|
||||||
|
>
|
||||||
|
{sidebarOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
|
||||||
|
</button>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`${currentTheme.accent} p-2 rounded-lg`}>
|
||||||
|
<Server className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className={`text-xl font-bold bg-gradient-to-r ${currentTheme.gradient} bg-clip-text text-transparent`}>MC Panel</h1>
|
||||||
|
<p className={`text-xs ${currentTheme.textSecondary}`}>Управление серверами</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{connectionError && (
|
||||||
|
<div className="bg-red-500 bg-opacity-20 border border-red-500 px-3 py-1.5 rounded-lg text-sm text-red-400">
|
||||||
|
⚠️ Нет связи
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={`px-3 py-1.5 rounded-lg ${currentTheme.card} ${currentTheme.border} border`}>
|
||||||
|
<span className={`text-sm ${currentTheme.textSecondary}`}>
|
||||||
|
{user?.username}
|
||||||
|
</span>
|
||||||
|
<span className={`ml-2 text-xs px-2 py-0.5 rounded ${currentTheme.accent} text-white`}>
|
||||||
|
{user?.role === 'admin' ? 'Админ' : 'Пользователь'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ThemeSelector currentTheme={theme} onThemeChange={handleThemeChange} />
|
||||||
|
{user?.role === 'admin' && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowUsers(true)}
|
||||||
|
className={`${currentTheme.accent} ${currentTheme.accentHover} px-4 py-2 rounded-lg transition flex items-center gap-2 text-white`}
|
||||||
|
>
|
||||||
|
<UsersIcon className="w-4 h-4" />
|
||||||
|
<span className="hidden sm:inline">Пользователи</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className={`${currentTheme.danger} hover:opacity-90 px-4 py-2 rounded-lg transition flex items-center gap-2 text-white`}
|
||||||
|
>
|
||||||
|
<LogOut className="w-4 h-4" />
|
||||||
|
<span className="hidden sm:inline">Выход</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="flex h-[calc(100vh-89px)]">
|
||||||
|
{/* Sidebar */}
|
||||||
|
<aside className={`${sidebarOpen ? 'w-80' : 'w-0'} ${currentTheme.secondary} ${currentTheme.border} border-r transition-all duration-300 overflow-hidden`}>
|
||||||
|
<div className="p-4 h-full flex flex-col">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-lg font-semibold">Мои серверы</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateModal(true)}
|
||||||
|
className={`${currentTheme.accent} ${currentTheme.accentHover} p-2 rounded-lg transition text-white`}
|
||||||
|
title="Создать сервер"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto space-y-2">
|
||||||
|
{servers.map((server) => (
|
||||||
|
<div
|
||||||
|
key={server.name}
|
||||||
|
className={`p-4 rounded-lg cursor-pointer transition-all duration-200 ${
|
||||||
|
selectedServer === server.name
|
||||||
|
? `${currentTheme.accent} text-white shadow-lg`
|
||||||
|
: `${currentTheme.card} ${currentTheme.hover}`
|
||||||
|
}`}
|
||||||
|
onClick={() => setSelectedServer(server.name)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<span className="font-medium truncate">{server.displayName}</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={`w-2 h-2 rounded-full ${
|
||||||
|
server.status === 'running' ? 'bg-green-400 animate-pulse' : 'bg-gray-500'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{server.status === 'stopped' ? (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
startServer(server.name);
|
||||||
|
}}
|
||||||
|
className="flex-1 bg-green-600 hover:bg-green-700 py-1.5 rounded-lg text-sm flex items-center justify-center gap-1.5 text-white transition"
|
||||||
|
>
|
||||||
|
<Play className="w-3.5 h-3.5" />
|
||||||
|
Запустить
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
stopServer(server.name);
|
||||||
|
}}
|
||||||
|
className="flex-1 bg-red-600 hover:bg-red-700 py-1.5 rounded-lg text-sm flex items-center justify-center gap-1.5 text-white transition"
|
||||||
|
>
|
||||||
|
<Square className="w-3.5 h-3.5" />
|
||||||
|
Остановить
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{servers.length === 0 && (
|
||||||
|
<div className={`text-center py-12 ${currentTheme.textSecondary}`}>
|
||||||
|
<Server className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||||
|
<p className="text-sm">Нет серверов</p>
|
||||||
|
<p className="text-xs mt-1">Создайте первый сервер</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="flex-1 flex flex-col overflow-hidden">
|
||||||
|
{selectedServer ? (
|
||||||
|
<>
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className={`${currentTheme.secondary} ${currentTheme.border} border-b flex overflow-x-auto`}>
|
||||||
|
{[
|
||||||
|
{ id: 'console', icon: Terminal, label: 'Консоль' },
|
||||||
|
{ id: 'files', icon: FolderOpen, label: 'Файлы' },
|
||||||
|
{ id: 'stats', icon: HardDrive, label: 'Статистика' },
|
||||||
|
{ id: 'settings', icon: Settings, label: 'Настройки' },
|
||||||
|
].map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={`px-6 py-4 flex items-center gap-2 transition-all duration-200 border-b-2 ${
|
||||||
|
activeTab === tab.id
|
||||||
|
? `${currentTheme.accent.replace('bg-', 'border-')} ${currentTheme.text}`
|
||||||
|
: `border-transparent ${currentTheme.textSecondary} ${currentTheme.hover}`
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<tab.icon className="w-4 h-4" />
|
||||||
|
<span className="font-medium">{tab.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
<ErrorBoundary>
|
||||||
|
{activeTab === 'console' && <Console serverName={selectedServer} token={token} theme={currentTheme} />}
|
||||||
|
{activeTab === 'files' && <FileManager serverName={selectedServer} token={token} theme={currentTheme} />}
|
||||||
|
{activeTab === 'stats' && <Stats serverName={selectedServer} token={token} theme={currentTheme} />}
|
||||||
|
{activeTab === 'settings' && (
|
||||||
|
<ServerSettings
|
||||||
|
serverName={selectedServer}
|
||||||
|
token={token}
|
||||||
|
user={user}
|
||||||
|
theme={currentTheme}
|
||||||
|
onDeleted={handleServerDeleted}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ErrorBoundary>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className={`${currentTheme.card} p-8 rounded-2xl ${currentTheme.border} border`}>
|
||||||
|
<Server className={`w-16 h-16 mx-auto mb-4 ${currentTheme.textSecondary} opacity-50`} />
|
||||||
|
<p className="text-xl font-medium mb-2">Выберите сервер</p>
|
||||||
|
<p className={`text-sm ${currentTheme.textSecondary}`}>
|
||||||
|
Выберите сервер из списка слева или создайте новый
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showCreateModal && (
|
||||||
|
<CreateServerModal
|
||||||
|
token={token}
|
||||||
|
theme={currentTheme}
|
||||||
|
onClose={() => setShowCreateModal(false)}
|
||||||
|
onCreated={loadServers}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
343
frontend/src/App1.jsx
Normal file
343
frontend/src/App1.jsx
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Server, Play, Square, Terminal, FolderOpen, HardDrive, Settings, Plus, Users as UsersIcon, LogOut } from 'lucide-react';
|
||||||
|
import Console from './components/Console';
|
||||||
|
import FileManager from './components/FileManager';
|
||||||
|
import Stats from './components/Stats';
|
||||||
|
import ServerSettings from './components/ServerSettings';
|
||||||
|
import CreateServerModal from './components/CreateServerModal';
|
||||||
|
import Users from './components/Users';
|
||||||
|
import Auth from './components/Auth';
|
||||||
|
import ErrorBoundary from './components/ErrorBoundary';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { API_URL } from './config';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [token, setToken] = useState(localStorage.getItem('token'));
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const [servers, setServers] = useState([]);
|
||||||
|
const [selectedServer, setSelectedServer] = useState(null);
|
||||||
|
const [activeTab, setActiveTab] = useState('console');
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
|
const [showUsers, setShowUsers] = useState(false);
|
||||||
|
const [connectionError, setConnectionError] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (token) {
|
||||||
|
loadUser();
|
||||||
|
loadServers();
|
||||||
|
const interval = setInterval(loadServers, 5000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
const loadUser = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${API_URL}/api/auth/me`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
setUser(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки пользователя:', error);
|
||||||
|
handleLogout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadServers = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${API_URL}/api/servers`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
setServers(data);
|
||||||
|
setConnectionError(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки серверов:', error);
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
handleLogout();
|
||||||
|
} else {
|
||||||
|
setConnectionError(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = async (username, password, isLogin) => {
|
||||||
|
const endpoint = isLogin ? '/api/auth/login' : '/api/auth/register';
|
||||||
|
const { data } = await axios.post(`${API_URL}${endpoint}`, {
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
|
||||||
|
localStorage.setItem('token', data.access_token);
|
||||||
|
setToken(data.access_token);
|
||||||
|
setUser({ username: data.username, role: data.role });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
setToken(null);
|
||||||
|
setUser(null);
|
||||||
|
setServers([]);
|
||||||
|
setSelectedServer(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleServerDeleted = () => {
|
||||||
|
setSelectedServer(null);
|
||||||
|
loadServers();
|
||||||
|
};
|
||||||
|
|
||||||
|
const startServer = async (serverName) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${API_URL}/api/servers/${serverName}/start`,
|
||||||
|
{},
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
console.log('Сервер запущен:', response.data);
|
||||||
|
setTimeout(() => {
|
||||||
|
loadServers();
|
||||||
|
}, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка запуска сервера:', error);
|
||||||
|
alert(error.response?.data?.detail || 'Ошибка запуска сервера');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopServer = async (serverName) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${API_URL}/api/servers/${serverName}/stop`,
|
||||||
|
{},
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
console.log('Сервер остановлен:', response.data);
|
||||||
|
setTimeout(() => {
|
||||||
|
loadServers();
|
||||||
|
}, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка остановки сервера:', error);
|
||||||
|
alert(error.response?.data?.detail || 'Ошибка остановки сервера');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return <Auth onLogin={handleLogin} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showUsers) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 text-white">
|
||||||
|
<header className="bg-gray-800 border-b border-gray-700 p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-2xl font-bold flex items-center gap-2">
|
||||||
|
<Server className="w-8 h-8" />
|
||||||
|
MC Panel
|
||||||
|
</h1>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<span className="text-gray-400">
|
||||||
|
{user?.username} ({user?.role === 'admin' ? 'Админ' : 'Пользователь'})
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowUsers(false)}
|
||||||
|
className="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded"
|
||||||
|
>
|
||||||
|
← Назад к серверам
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="bg-red-600 hover:bg-red-700 px-4 py-2 rounded flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<LogOut className="w-4 h-4" />
|
||||||
|
Выход
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<Users token={token} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 text-white">
|
||||||
|
<header className="bg-gray-800 border-b border-gray-700 p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-2xl font-bold flex items-center gap-2">
|
||||||
|
<Server className="w-8 h-8" />
|
||||||
|
MC Panel
|
||||||
|
</h1>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{connectionError && (
|
||||||
|
<div className="bg-red-600 px-4 py-2 rounded text-sm">
|
||||||
|
⚠️ Нет связи с сервером
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<span className="text-gray-400">
|
||||||
|
{user?.username} ({user?.role === 'admin' ? 'Админ' : 'Пользователь'})
|
||||||
|
</span>
|
||||||
|
{user?.role === 'admin' && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowUsers(true)}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<UsersIcon className="w-4 h-4" />
|
||||||
|
Пользователи
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="bg-red-600 hover:bg-red-700 px-4 py-2 rounded flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<LogOut className="w-4 h-4" />
|
||||||
|
Выход
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="flex h-[calc(100vh-73px)]">
|
||||||
|
<aside className="w-64 bg-gray-800 border-r border-gray-700 p-4 overflow-y-auto">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-lg font-semibold">Серверы</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateModal(true)}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 p-2 rounded"
|
||||||
|
title="Создать сервер"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{servers.map((server) => (
|
||||||
|
<div
|
||||||
|
key={server.name}
|
||||||
|
className={`p-3 mb-2 rounded cursor-pointer transition ${
|
||||||
|
selectedServer === server.name
|
||||||
|
? 'bg-blue-600'
|
||||||
|
: 'bg-gray-700 hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
onClick={() => setSelectedServer(server.name)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="font-medium truncate">{server.displayName}</span>
|
||||||
|
<span
|
||||||
|
className={`w-2 h-2 rounded-full flex-shrink-0 ${
|
||||||
|
server.status === 'running' ? 'bg-green-500' : 'bg-red-500'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 mt-2">
|
||||||
|
{server.status === 'stopped' ? (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
startServer(server.name);
|
||||||
|
}}
|
||||||
|
className="flex-1 bg-green-600 hover:bg-green-700 p-1 rounded text-sm flex items-center justify-center gap-1"
|
||||||
|
>
|
||||||
|
<Play className="w-3 h-3" />
|
||||||
|
Старт
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
stopServer(server.name);
|
||||||
|
}}
|
||||||
|
className="flex-1 bg-red-600 hover:bg-red-700 p-1 rounded text-sm flex items-center justify-center gap-1"
|
||||||
|
>
|
||||||
|
<Square className="w-3 h-3" />
|
||||||
|
Стоп
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main className="flex-1 flex flex-col">
|
||||||
|
{selectedServer ? (
|
||||||
|
<>
|
||||||
|
<div className="bg-gray-800 border-b border-gray-700 flex">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('console')}
|
||||||
|
className={`px-6 py-3 flex items-center gap-2 ${
|
||||||
|
activeTab === 'console'
|
||||||
|
? 'bg-gray-900 border-b-2 border-blue-500'
|
||||||
|
: 'hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Terminal className="w-4 h-4" />
|
||||||
|
Консоль
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('files')}
|
||||||
|
className={`px-6 py-3 flex items-center gap-2 ${
|
||||||
|
activeTab === 'files'
|
||||||
|
? 'bg-gray-900 border-b-2 border-blue-500'
|
||||||
|
: 'hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FolderOpen className="w-4 h-4" />
|
||||||
|
Файлы
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('stats')}
|
||||||
|
className={`px-6 py-3 flex items-center gap-2 ${
|
||||||
|
activeTab === 'stats'
|
||||||
|
? 'bg-gray-900 border-b-2 border-blue-500'
|
||||||
|
: 'hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<HardDrive className="w-4 h-4" />
|
||||||
|
Статистика
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('settings')}
|
||||||
|
className={`px-6 py-3 flex items-center gap-2 ${
|
||||||
|
activeTab === 'settings'
|
||||||
|
? 'bg-gray-900 border-b-2 border-blue-500'
|
||||||
|
: 'hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Settings className="w-4 h-4" />
|
||||||
|
Настройки
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
<ErrorBoundary>
|
||||||
|
{activeTab === 'console' && <Console serverName={selectedServer} token={token} />}
|
||||||
|
{activeTab === 'files' && <FileManager serverName={selectedServer} token={token} />}
|
||||||
|
{activeTab === 'stats' && <Stats serverName={selectedServer} token={token} />}
|
||||||
|
{activeTab === 'settings' && (
|
||||||
|
<ServerSettings
|
||||||
|
serverName={selectedServer}
|
||||||
|
token={token}
|
||||||
|
user={user}
|
||||||
|
onDeleted={handleServerDeleted}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ErrorBoundary>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 flex items-center justify-center text-gray-500">
|
||||||
|
<div className="text-center">
|
||||||
|
<Server className="w-16 h-16 mx-auto mb-4 opacity-50" />
|
||||||
|
<p className="text-xl">Выберите сервер</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showCreateModal && (
|
||||||
|
<CreateServerModal
|
||||||
|
token={token}
|
||||||
|
onClose={() => setShowCreateModal(false)}
|
||||||
|
onCreated={loadServers}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
148
frontend/src/components/Auth.jsx
Normal file
148
frontend/src/components/Auth.jsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Server, Eye, EyeOff } from 'lucide-react';
|
||||||
|
import { getTheme } from '../themes';
|
||||||
|
|
||||||
|
export default function Auth({ onLogin }) {
|
||||||
|
const [isLogin, setIsLogin] = useState(true);
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [theme] = useState(localStorage.getItem('theme') || 'dark');
|
||||||
|
|
||||||
|
const currentTheme = getTheme(theme);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await onLogin(username, password, isLogin);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || 'Ошибка авторизации');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`min-h-screen ${currentTheme.primary} flex items-center justify-center p-4 transition-colors duration-300`}>
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<div className={`${currentTheme.accent} w-16 h-16 rounded-2xl flex items-center justify-center mx-auto mb-4 shadow-lg`}>
|
||||||
|
<Server className="w-10 h-10 text-white" />
|
||||||
|
</div>
|
||||||
|
<h1 className={`text-3xl font-bold bg-gradient-to-r ${currentTheme.gradient} bg-clip-text text-transparent mb-2`}>MC Panel</h1>
|
||||||
|
<p className={`${currentTheme.textSecondary}`}>Панель управления Minecraft серверами</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form Card */}
|
||||||
|
<div className={`${currentTheme.secondary} rounded-2xl shadow-2xl ${currentTheme.border} border p-8`}>
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex gap-2 mb-6">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsLogin(true)}
|
||||||
|
className={`flex-1 py-3 rounded-xl font-medium transition-all duration-200 ${
|
||||||
|
isLogin
|
||||||
|
? `${currentTheme.accent} text-white shadow-lg`
|
||||||
|
: `${currentTheme.card} ${currentTheme.text} ${currentTheme.hover}`
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Вход
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsLogin(false)}
|
||||||
|
className={`flex-1 py-3 rounded-xl font-medium transition-all duration-200 ${
|
||||||
|
!isLogin
|
||||||
|
? `${currentTheme.accent} text-white shadow-lg`
|
||||||
|
: `${currentTheme.card} ${currentTheme.text} ${currentTheme.hover}`
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Регистрация
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
{/* Username */}
|
||||||
|
<div>
|
||||||
|
<label className={`block text-sm font-medium ${currentTheme.text} mb-2`}>
|
||||||
|
Имя пользователя
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
className={`w-full ${currentTheme.input} ${currentTheme.border} border rounded-xl px-4 py-3 ${currentTheme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`}
|
||||||
|
placeholder="admin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Password */}
|
||||||
|
<div>
|
||||||
|
<label className={`block text-sm font-medium ${currentTheme.text} mb-2`}>
|
||||||
|
Пароль
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
className={`w-full ${currentTheme.input} ${currentTheme.border} border rounded-xl px-4 py-3 pr-12 ${currentTheme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`}
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className={`absolute right-3 top-1/2 -translate-y-1/2 ${currentTheme.textSecondary} hover:${currentTheme.text} transition`}
|
||||||
|
>
|
||||||
|
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error */}
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-500 bg-opacity-10 border border-red-500 rounded-xl p-3 text-red-400 text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className={`w-full ${currentTheme.accent} ${currentTheme.accentHover} text-white py-3 rounded-xl font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-lg hover:shadow-xl`}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||||
|
Загрузка...
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
isLogin ? 'Войти' : 'Зарегистрироваться'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Default Credentials */}
|
||||||
|
{isLogin && (
|
||||||
|
<div className={`mt-6 text-center text-sm ${currentTheme.textSecondary}`}>
|
||||||
|
<p>Учётные данные по умолчанию:</p>
|
||||||
|
<p className={`${currentTheme.text} font-mono mt-1`}>admin / admin</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className={`text-center mt-6 text-sm ${currentTheme.textSecondary}`}>
|
||||||
|
<p>© 2024 MC Panel. Все права защищены.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
109
frontend/src/components/Auth1.jsx
Normal file
109
frontend/src/components/Auth1.jsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Server } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function Auth({ onLogin }) {
|
||||||
|
const [isLogin, setIsLogin] = useState(true);
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await onLogin(username, password, isLogin);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || 'Ошибка авторизации');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
|
||||||
|
<div className="bg-gray-800 rounded-lg p-8 w-full max-w-md border border-gray-700">
|
||||||
|
<div className="flex items-center justify-center mb-8">
|
||||||
|
<Server className="w-12 h-12 text-blue-500 mr-3" />
|
||||||
|
<h1 className="text-3xl font-bold text-white">MC Panel</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 mb-6">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsLogin(true)}
|
||||||
|
className={`flex-1 py-2 rounded ${
|
||||||
|
isLogin
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-gray-700 text-gray-400 hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Вход
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsLogin(false)}
|
||||||
|
className={`flex-1 py-2 rounded ${
|
||||||
|
!isLogin
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-gray-700 text-gray-400 hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Регистрация
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
Имя пользователя
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
className="w-full bg-gray-700 border border-gray-600 rounded px-4 py-2 text-white focus:outline-none focus:border-blue-500"
|
||||||
|
placeholder="admin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
Пароль
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
className="w-full bg-gray-700 border border-gray-600 rounded px-4 py-2 text-white focus:outline-none focus:border-blue-500"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-600 bg-opacity-20 border border-red-600 rounded p-3 text-red-400 text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 rounded font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{loading ? 'Загрузка...' : isLogin ? 'Войти' : 'Зарегистрироваться'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{isLogin && (
|
||||||
|
<div className="mt-4 text-center text-sm text-gray-400">
|
||||||
|
<p>По умолчанию:</p>
|
||||||
|
<p className="text-gray-300">admin / admin</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
90
frontend/src/components/Console.jsx
Normal file
90
frontend/src/components/Console.jsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Send } from 'lucide-react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { API_URL, WS_URL } from '../config';
|
||||||
|
|
||||||
|
export default function Console({ serverName, token, theme }) {
|
||||||
|
const [logs, setLogs] = useState([]);
|
||||||
|
const [command, setCommand] = useState('');
|
||||||
|
const logsEndRef = useRef(null);
|
||||||
|
const wsRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLogs([]);
|
||||||
|
|
||||||
|
const ws = new WebSocket(`${WS_URL}/ws/servers/${serverName}/console`);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('WebSocket подключен');
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
setLogs((prev) => [...prev, event.data]);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('WebSocket ошибка:', error);
|
||||||
|
};
|
||||||
|
|
||||||
|
wsRef.current = ws;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ws.close();
|
||||||
|
};
|
||||||
|
}, [serverName]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
logsEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}, [logs]);
|
||||||
|
|
||||||
|
const sendCommand = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!command.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
`${API_URL}/api/servers/${serverName}/command`,
|
||||||
|
{ command: command.trim() },
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
setCommand('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка отправки команды:', error);
|
||||||
|
alert(error.response?.data?.detail || 'Ошибка отправки команды');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col h-full ${theme.primary}`}>
|
||||||
|
<div className={`flex-1 overflow-y-auto p-4 font-mono text-sm ${theme.secondary}`}>
|
||||||
|
{logs.length === 0 ? (
|
||||||
|
<div className={theme.textSecondary}>Консоль пуста. Запустите сервер для просмотра логов.</div>
|
||||||
|
) : (
|
||||||
|
logs.map((log, index) => (
|
||||||
|
<div key={index} className={`${theme.text} whitespace-pre-wrap leading-relaxed`}>
|
||||||
|
{log}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
<div ref={logsEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={sendCommand} className={`${theme.border} border-t p-4 flex gap-2`}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={command}
|
||||||
|
onChange={(e) => setCommand(e.target.value)}
|
||||||
|
placeholder="Введите команду..."
|
||||||
|
className={`flex-1 ${theme.input} ${theme.border} border rounded-xl px-4 py-2 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={`${theme.accent} ${theme.accentHover} px-6 py-2 rounded-xl flex items-center gap-2 text-white transition`}
|
||||||
|
>
|
||||||
|
<Send className="w-4 h-4" />
|
||||||
|
Отправить
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
108
frontend/src/components/CreateServerModal.jsx
Normal file
108
frontend/src/components/CreateServerModal.jsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { API_URL } from '../config';
|
||||||
|
|
||||||
|
export default function CreateServerModal({ token, theme, onClose, onCreated }) {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
displayName: '',
|
||||||
|
startCommand: 'java -Xmx2G -Xms1G -jar server.jar nogui'
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
`${API_URL}/api/servers/create`,
|
||||||
|
formData,
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
onCreated();
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.response?.data?.detail || 'Ошибка создания сервера');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className={`${theme.secondary} rounded-2xl p-6 w-full max-w-md shadow-2xl ${theme.border} border`}>
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h2 className={`text-xl font-bold ${theme.text}`}>Создать сервер</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className={`${theme.textSecondary} hover:${theme.text} transition`}
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className={`block text-sm font-medium mb-2 ${theme.text}`}>
|
||||||
|
Имя папки (только латиница, цифры, _ и -)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
className={`w-full ${theme.input} ${theme.border} border rounded-xl px-4 py-2 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`}
|
||||||
|
placeholder="my_server"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className={`block text-sm font-medium mb-2 ${theme.text}`}>
|
||||||
|
Отображаемое имя
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={formData.displayName}
|
||||||
|
onChange={(e) => setFormData({ ...formData, displayName: e.target.value })}
|
||||||
|
className={`w-full ${theme.input} ${theme.border} border rounded-xl px-4 py-2 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`}
|
||||||
|
placeholder="Мой сервер"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className={`block text-sm font-medium mb-2 ${theme.text}`}>
|
||||||
|
Команда запуска
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={formData.startCommand}
|
||||||
|
onChange={(e) => setFormData({ ...formData, startCommand: e.target.value })}
|
||||||
|
className={`w-full ${theme.input} ${theme.border} border rounded-xl px-4 py-2 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 pt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className={`flex-1 ${theme.card} ${theme.hover} px-4 py-2 rounded-xl transition`}
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className={`flex-1 ${theme.accent} ${theme.accentHover} px-4 py-2 rounded-xl disabled:opacity-50 transition text-white`}
|
||||||
|
>
|
||||||
|
{loading ? 'Создание...' : 'Создать'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
44
frontend/src/components/ErrorBoundary.jsx
Normal file
44
frontend/src/components/ErrorBoundary.jsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Component } from 'react';
|
||||||
|
|
||||||
|
class ErrorBoundary extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false, error: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error) {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error, errorInfo) {
|
||||||
|
console.error('Error caught by boundary:', error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full bg-gray-900 text-white p-8">
|
||||||
|
<div className="max-w-md text-center">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Что-то пошло не так</h2>
|
||||||
|
<p className="text-gray-400 mb-4">
|
||||||
|
Произошла ошибка при загрузке компонента
|
||||||
|
</p>
|
||||||
|
<pre className="bg-black p-4 rounded text-left text-sm overflow-auto mb-4">
|
||||||
|
{this.state.error?.toString()}
|
||||||
|
</pre>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded"
|
||||||
|
>
|
||||||
|
Перезагрузить страницу
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary;
|
||||||
64
frontend/src/components/FileEditorModal.jsx
Normal file
64
frontend/src/components/FileEditorModal.jsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { X, Save } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function FileEditorModal({ file, onClose, onSave }) {
|
||||||
|
const [content, setContent] = useState(file.content);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setSaving(true);
|
||||||
|
await onSave(file.path, content);
|
||||||
|
setSaving(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
if (e.ctrlKey && e.key === 's') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSave();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [content]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-gray-800 rounded-lg w-full max-w-4xl h-[80vh] flex flex-col">
|
||||||
|
<div className="flex items-center justify-between p-4 border-b border-gray-700">
|
||||||
|
<h2 className="text-xl font-bold">Редактирование: {file.name}</h2>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={saving}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded flex items-center gap-2 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<Save className="w-4 h-4" />
|
||||||
|
{saving ? 'Сохранение...' : 'Сохранить'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-400 hover:text-white"
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden p-4 bg-gray-900">
|
||||||
|
<textarea
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => setContent(e.target.value)}
|
||||||
|
className="w-full h-full bg-black text-gray-300 font-mono text-sm p-4 rounded border border-gray-700 focus:outline-none focus:border-blue-500 resize-none"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 border-t border-gray-700 text-sm text-gray-400">
|
||||||
|
Используйте Ctrl+S для быстрого сохранения
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
303
frontend/src/components/FileManager.jsx
Normal file
303
frontend/src/components/FileManager.jsx
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Folder, File, Download, Trash2, Upload, Edit, Eye } from 'lucide-react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import FileEditorModal from './FileEditorModal';
|
||||||
|
import FileViewerModal from './FileViewerModal';
|
||||||
|
import { API_URL } from '../config';
|
||||||
|
|
||||||
|
export default function FileManager({ serverName, token }) {
|
||||||
|
const [files, setFiles] = useState([]);
|
||||||
|
const [currentPath, setCurrentPath] = useState('');
|
||||||
|
const [editingFile, setEditingFile] = useState(null);
|
||||||
|
const [viewingFile, setViewingFile] = useState(null);
|
||||||
|
const [renamingFile, setRenamingFile] = useState(null);
|
||||||
|
const [newFileName, setNewFileName] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadFiles();
|
||||||
|
}, [serverName, currentPath]);
|
||||||
|
|
||||||
|
const loadFiles = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${API_URL}/api/servers/${serverName}/files`, {
|
||||||
|
params: { path: currentPath },
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
setFiles(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки файлов:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openFolder = (folderName) => {
|
||||||
|
setCurrentPath(currentPath ? `${currentPath}/${folderName}` : folderName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
const parts = currentPath.split('/');
|
||||||
|
parts.pop();
|
||||||
|
setCurrentPath(parts.join('/'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadFile = async (fileName) => {
|
||||||
|
const filePath = currentPath ? `${currentPath}/${fileName}` : fileName;
|
||||||
|
window.open(`${API_URL}/api/servers/${serverName}/files/download?path=${filePath}`, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteFile = async (fileName) => {
|
||||||
|
if (!confirm(`Удалить ${fileName}?`)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filePath = currentPath ? `${currentPath}/${fileName}` : fileName;
|
||||||
|
await axios.delete(`${API_URL}/api/servers/${serverName}/files`, {
|
||||||
|
params: { path: filePath },
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
loadFiles();
|
||||||
|
} catch (error) {
|
||||||
|
alert('Ошибка удаления файла');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadFile = async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
`${API_URL}/api/servers/${serverName}/files/upload?path=${currentPath}`,
|
||||||
|
formData,
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
loadFiles();
|
||||||
|
} catch (error) {
|
||||||
|
alert('Ошибка загрузки файла');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewFile = async (fileName) => {
|
||||||
|
const filePath = currentPath ? `${currentPath}/${fileName}` : fileName;
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${API_URL}/api/servers/${serverName}/files/content`, {
|
||||||
|
params: { path: filePath },
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
setViewingFile({ name: fileName, path: filePath, content: data.content });
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.response?.data?.detail || 'Ошибка открытия файла');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const editFile = async (fileName) => {
|
||||||
|
const filePath = currentPath ? `${currentPath}/${fileName}` : fileName;
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${API_URL}/api/servers/${serverName}/files/content`, {
|
||||||
|
params: { path: filePath },
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
setEditingFile({ name: fileName, path: filePath, content: data.content });
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.response?.data?.detail || 'Ошибка открытия файла');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveFile = async (filePath, content) => {
|
||||||
|
try {
|
||||||
|
await axios.put(
|
||||||
|
`${API_URL}/api/servers/${serverName}/files/content`,
|
||||||
|
{ content },
|
||||||
|
{
|
||||||
|
params: { path: filePath },
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setEditingFile(null);
|
||||||
|
alert('Файл сохранен');
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.response?.data?.detail || 'Ошибка сохранения файла');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startRename = (fileName) => {
|
||||||
|
setRenamingFile(fileName);
|
||||||
|
setNewFileName(fileName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renameFile = async (oldName) => {
|
||||||
|
if (!newFileName.trim() || newFileName === oldName) {
|
||||||
|
setRenamingFile(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldPath = currentPath ? `${currentPath}/${oldName}` : oldName;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.put(
|
||||||
|
`${API_URL}/api/servers/${serverName}/files/rename`,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
params: { old_path: oldPath, new_name: newFileName },
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setRenamingFile(null);
|
||||||
|
loadFiles();
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.response?.data?.detail || 'Ошибка переименования файла');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatSize = (bytes) => {
|
||||||
|
if (bytes === 0) return '-';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Б', 'КБ', 'МБ', 'ГБ'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col bg-gray-900">
|
||||||
|
<div className="border-b border-gray-700 p-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{currentPath && (
|
||||||
|
<button
|
||||||
|
onClick={goBack}
|
||||||
|
className="bg-gray-700 hover:bg-gray-600 px-3 py-1 rounded"
|
||||||
|
>
|
||||||
|
← Назад
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<span className="text-gray-400">/{currentPath || 'root'}</span>
|
||||||
|
</div>
|
||||||
|
<label className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded cursor-pointer flex items-center gap-2">
|
||||||
|
<Upload className="w-4 h-4" />
|
||||||
|
Загрузить
|
||||||
|
<input type="file" onChange={uploadFile} className="hidden" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-gray-800 sticky top-0">
|
||||||
|
<tr>
|
||||||
|
<th className="text-left p-4">Имя</th>
|
||||||
|
<th className="text-left p-4">Размер</th>
|
||||||
|
<th className="text-right p-4">Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{files.map((file) => (
|
||||||
|
<tr
|
||||||
|
key={file.name}
|
||||||
|
className="border-b border-gray-800 hover:bg-gray-800"
|
||||||
|
>
|
||||||
|
<td className="p-4">
|
||||||
|
{renamingFile === file.name ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{file.type === 'directory' ? (
|
||||||
|
<Folder className="w-5 h-5 text-blue-400" />
|
||||||
|
) : (
|
||||||
|
<File className="w-5 h-5 text-gray-400" />
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newFileName}
|
||||||
|
onChange={(e) => setNewFileName(e.target.value)}
|
||||||
|
onBlur={() => renameFile(file.name)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') renameFile(file.name);
|
||||||
|
if (e.key === 'Escape') setRenamingFile(null);
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
className="bg-gray-700 border border-gray-600 rounded px-2 py-1 text-sm focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 cursor-pointer"
|
||||||
|
onClick={() => file.type === 'directory' && openFolder(file.name)}
|
||||||
|
onDoubleClick={() => file.type === 'file' && viewFile(file.name)}
|
||||||
|
>
|
||||||
|
{file.type === 'directory' ? (
|
||||||
|
<Folder className="w-5 h-5 text-blue-400" />
|
||||||
|
) : (
|
||||||
|
<File className="w-5 h-5 text-gray-400" />
|
||||||
|
)}
|
||||||
|
<span>{file.name}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="p-4 text-gray-400">{formatSize(file.size)}</td>
|
||||||
|
<td className="p-4">
|
||||||
|
<div className="flex gap-2 justify-end">
|
||||||
|
{file.type === 'file' && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => viewFile(file.name)}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 p-2 rounded"
|
||||||
|
title="Просмотр"
|
||||||
|
>
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => editFile(file.name)}
|
||||||
|
className="bg-purple-600 hover:bg-purple-700 p-2 rounded"
|
||||||
|
title="Редактировать"
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => downloadFile(file.name)}
|
||||||
|
className="bg-green-600 hover:bg-green-700 p-2 rounded"
|
||||||
|
title="Скачать"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => startRename(file.name)}
|
||||||
|
className="bg-yellow-600 hover:bg-yellow-700 p-2 rounded"
|
||||||
|
title="Переименовать"
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => deleteFile(file.name)}
|
||||||
|
className="bg-red-600 hover:bg-red-700 p-2 rounded"
|
||||||
|
title="Удалить"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{viewingFile && (
|
||||||
|
<FileViewerModal
|
||||||
|
file={viewingFile}
|
||||||
|
onClose={() => setViewingFile(null)}
|
||||||
|
onEdit={() => {
|
||||||
|
setEditingFile(viewingFile);
|
||||||
|
setViewingFile(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{editingFile && (
|
||||||
|
<FileEditorModal
|
||||||
|
file={editingFile}
|
||||||
|
onClose={() => setEditingFile(null)}
|
||||||
|
onSave={saveFile}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
34
frontend/src/components/FileViewerModal.jsx
Normal file
34
frontend/src/components/FileViewerModal.jsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { X, Edit } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function FileViewerModal({ file, onClose, onEdit }) {
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-gray-800 rounded-lg w-full max-w-4xl h-[80vh] flex flex-col">
|
||||||
|
<div className="flex items-center justify-between p-4 border-b border-gray-700">
|
||||||
|
<h2 className="text-xl font-bold">{file.name}</h2>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={onEdit}
|
||||||
|
className="bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
Редактировать
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-400 hover:text-white"
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-auto p-4 bg-gray-900">
|
||||||
|
<pre className="text-sm text-gray-300 font-mono whitespace-pre-wrap">
|
||||||
|
{file.content}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
106
frontend/src/components/Login.jsx
Normal file
106
frontend/src/components/Login.jsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Server } from 'lucide-react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { API_URL } from '../config';
|
||||||
|
|
||||||
|
export default function Login({ onLogin }) {
|
||||||
|
const [isRegister, setIsRegister] = useState(false);
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const endpoint = isRegister ? '/api/auth/register' : '/api/auth/login';
|
||||||
|
const { data } = await axios.post(`${API_URL}${endpoint}`, {
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
|
||||||
|
localStorage.setItem('token', data.access_token);
|
||||||
|
localStorage.setItem('username', data.username);
|
||||||
|
localStorage.setItem('role', data.role);
|
||||||
|
|
||||||
|
onLogin(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.detail || 'Ошибка авторизации');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
|
||||||
|
<div className="bg-gray-800 rounded-lg p-8 w-full max-w-md">
|
||||||
|
<div className="flex items-center justify-center mb-8">
|
||||||
|
<Server className="w-12 h-12 text-blue-500 mr-3" />
|
||||||
|
<h1 className="text-3xl font-bold text-white">MC Panel</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-xl font-semibold text-white mb-6 text-center">
|
||||||
|
{isRegister ? 'Регистрация' : 'Вход'}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-600 text-white p-3 rounded mb-4">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
Имя пользователя
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
minLength={3}
|
||||||
|
className="w-full bg-gray-700 border border-gray-600 rounded px-4 py-2 text-white focus:outline-none focus:border-blue-500"
|
||||||
|
placeholder="Введите имя пользователя"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
Пароль
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
minLength={6}
|
||||||
|
className="w-full bg-gray-700 border border-gray-600 rounded px-4 py-2 text-white focus:outline-none focus:border-blue-500"
|
||||||
|
placeholder="Введите пароль"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 rounded font-medium disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? 'Загрузка...' : (isRegister ? 'Зарегистрироваться' : 'Войти')}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsRegister(!isRegister)}
|
||||||
|
className="text-blue-400 hover:text-blue-300"
|
||||||
|
>
|
||||||
|
{isRegister ? 'Уже есть аккаунт? Войти' : 'Нет аккаунта? Зарегистрироваться'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
132
frontend/src/components/ServerAccessModal.jsx
Normal file
132
frontend/src/components/ServerAccessModal.jsx
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { X, UserPlus, Trash2 } from 'lucide-react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { API_URL } from '../config';
|
||||||
|
|
||||||
|
export default function ServerAccessModal({ serverName, onClose }) {
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
const [newUsername, setNewUsername] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadUsers();
|
||||||
|
}, [serverName]);
|
||||||
|
|
||||||
|
const loadUsers = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const { data } = await axios.get(
|
||||||
|
`${API_URL}/api/servers/${serverName}/access`,
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
setUsers(data.users);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки пользователей:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addUser = async () => {
|
||||||
|
if (!newUsername.trim()) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
await axios.post(
|
||||||
|
`${API_URL}/api/servers/${serverName}/access`,
|
||||||
|
{ username: newUsername, server_name: serverName },
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
setNewUsername('');
|
||||||
|
loadUsers();
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.response?.data?.detail || 'Ошибка добавления пользователя');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeUser = async (username) => {
|
||||||
|
if (!confirm(`Удалить доступ для ${username}?`)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
await axios.delete(
|
||||||
|
`${API_URL}/api/servers/${serverName}/access`,
|
||||||
|
{
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
params: { username, server_name: serverName }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
loadUsers();
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.response?.data?.detail || 'Ошибка удаления доступа');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-gray-800 rounded-lg p-6 w-full max-w-md">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-xl font-bold">Управление доступом</h2>
|
||||||
|
<button onClick={onClose} className="text-gray-400 hover:text-white">
|
||||||
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-sm font-medium mb-2">
|
||||||
|
Добавить пользователя
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newUsername}
|
||||||
|
onChange={(e) => setNewUsername(e.target.value)}
|
||||||
|
placeholder="Имя пользователя"
|
||||||
|
className="flex-1 bg-gray-700 border border-gray-600 rounded px-4 py-2 focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={addUser}
|
||||||
|
disabled={loading}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded flex items-center gap-2 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<UserPlus className="w-4 h-4" />
|
||||||
|
Добавить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium mb-2">Пользователи с доступом:</h3>
|
||||||
|
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||||
|
{users.length === 0 ? (
|
||||||
|
<p className="text-gray-400 text-sm">Нет пользователей</p>
|
||||||
|
) : (
|
||||||
|
users.map((user) => (
|
||||||
|
<div
|
||||||
|
key={user.username}
|
||||||
|
className="bg-gray-700 p-3 rounded flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">{user.username}</span>
|
||||||
|
{user.role === 'admin' && (
|
||||||
|
<span className="ml-2 text-xs bg-blue-600 px-2 py-1 rounded">
|
||||||
|
Админ
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => removeUser(user.username)}
|
||||||
|
className="text-red-400 hover:text-red-300"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
268
frontend/src/components/ServerSettings.jsx
Normal file
268
frontend/src/components/ServerSettings.jsx
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Save, Trash2, Users, UserPlus } from 'lucide-react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { API_URL } from '../config';
|
||||||
|
|
||||||
|
export default function ServerSettings({ serverName, token, user, onDeleted }) {
|
||||||
|
const [config, setConfig] = useState({
|
||||||
|
name: '',
|
||||||
|
displayName: '',
|
||||||
|
startCommand: '',
|
||||||
|
owner: ''
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [allUsers, setAllUsers] = useState([]);
|
||||||
|
const [serverUsers, setServerUsers] = useState([]);
|
||||||
|
const [showUserManagement, setShowUserManagement] = useState(false);
|
||||||
|
|
||||||
|
const isAdmin = user?.role === 'admin';
|
||||||
|
const isOwner = config.owner === user?.username;
|
||||||
|
const canManageAccess = isAdmin || isOwner;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadConfig();
|
||||||
|
if (canManageAccess) {
|
||||||
|
loadUsers();
|
||||||
|
}
|
||||||
|
}, [serverName, canManageAccess]);
|
||||||
|
|
||||||
|
const loadConfig = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${API_URL}/api/servers/${serverName}/config`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
setConfig(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки настроек:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadUsers = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${API_URL}/api/users`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
setAllUsers(data);
|
||||||
|
|
||||||
|
// Находим пользователей с доступом к этому серверу
|
||||||
|
const usersWithAccess = data.filter(u =>
|
||||||
|
u.role === 'admin' || u.servers?.includes(serverName)
|
||||||
|
);
|
||||||
|
setServerUsers(usersWithAccess);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки пользователей:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleUserAccess = async (username) => {
|
||||||
|
const userHasAccess = serverUsers.some(u => u.username === username);
|
||||||
|
const targetUser = allUsers.find(u => u.username === username);
|
||||||
|
|
||||||
|
if (!targetUser) return;
|
||||||
|
|
||||||
|
const newServers = userHasAccess
|
||||||
|
? targetUser.servers.filter(s => s !== serverName)
|
||||||
|
: [...(targetUser.servers || []), serverName];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.put(
|
||||||
|
`${API_URL}/api/users/${username}/servers`,
|
||||||
|
{ servers: newServers },
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
loadUsers();
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.response?.data?.detail || 'Ошибка изменения доступа');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveConfig = async () => {
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
await axios.put(
|
||||||
|
`${API_URL}/api/servers/${serverName}/config`,
|
||||||
|
config,
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
alert('Настройки сохранены');
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.response?.data?.detail || 'Ошибка сохранения настроек');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteServer = async () => {
|
||||||
|
if (!confirm(`Вы уверены, что хотите удалить сервер "${config.displayName}"? Все файлы будут удалены!`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.delete(`${API_URL}/api/servers/${serverName}`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
onDeleted();
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.response?.data?.detail || 'Ошибка удаления сервера');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="text-gray-400">Загрузка...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-8 bg-gray-900 h-full overflow-y-auto">
|
||||||
|
<h2 className="text-2xl font-bold mb-6">Настройки сервера</h2>
|
||||||
|
|
||||||
|
<div className="space-y-6 max-w-2xl">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">
|
||||||
|
Имя папки (нельзя изменить)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={config.name}
|
||||||
|
disabled
|
||||||
|
className="w-full bg-gray-800 border border-gray-700 rounded px-4 py-2 text-gray-500 cursor-not-allowed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">
|
||||||
|
Отображаемое имя
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={config.displayName}
|
||||||
|
onChange={(e) => setConfig({ ...config, displayName: e.target.value })}
|
||||||
|
className="w-full bg-gray-800 border border-gray-700 rounded px-4 py-2 focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">
|
||||||
|
Команда запуска
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={config.startCommand}
|
||||||
|
onChange={(e) => setConfig({ ...config, startCommand: e.target.value })}
|
||||||
|
className="w-full bg-gray-800 border border-gray-700 rounded px-4 py-2 focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-gray-400 mt-2">
|
||||||
|
Пример: java -Xmx2G -Xms1G -jar server.jar nogui
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{config.owner && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">
|
||||||
|
Владелец
|
||||||
|
</label>
|
||||||
|
<div className="text-gray-300">{config.owner}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-4 pt-4">
|
||||||
|
<button
|
||||||
|
onClick={saveConfig}
|
||||||
|
disabled={saving}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded flex items-center gap-2 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<Save className="w-4 h-4" />
|
||||||
|
{saving ? 'Сохранение...' : 'Сохранить'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{canManageAccess && (
|
||||||
|
<div className="border-t border-gray-700 pt-6 mt-8">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold">Управление доступом</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowUserManagement(!showUserManagement)}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Users className="w-4 h-4" />
|
||||||
|
{showUserManagement ? 'Скрыть' : 'Показать пользователей'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showUserManagement && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm text-gray-400 mb-4">
|
||||||
|
Выберите пользователей, которые могут управлять этим сервером:
|
||||||
|
</p>
|
||||||
|
{allUsers.filter(u => u.role !== 'admin').map((targetUser) => {
|
||||||
|
const hasAccess = serverUsers.some(u => u.username === targetUser.username);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={targetUser.username}
|
||||||
|
className="flex items-center justify-between bg-gray-800 p-3 rounded"
|
||||||
|
>
|
||||||
|
<span>{targetUser.username}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleUserAccess(targetUser.username)}
|
||||||
|
className={`px-4 py-1 rounded text-sm ${
|
||||||
|
hasAccess
|
||||||
|
? 'bg-green-600 hover:bg-green-700'
|
||||||
|
: 'bg-gray-600 hover:bg-gray-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{hasAccess ? 'Есть доступ' : 'Нет доступа'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{allUsers.filter(u => u.role !== 'admin').length === 0 && (
|
||||||
|
<p className="text-gray-500 text-sm">Нет обычных пользователей</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<h4 className="text-sm font-medium mb-2 text-gray-400">
|
||||||
|
Пользователи с доступом:
|
||||||
|
</h4>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{serverUsers.map((u) => (
|
||||||
|
<span
|
||||||
|
key={u.username}
|
||||||
|
className={`px-3 py-1 rounded text-sm ${
|
||||||
|
u.role === 'admin'
|
||||||
|
? 'bg-blue-600'
|
||||||
|
: 'bg-green-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{u.username} {u.role === 'admin' && '(Админ)'}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="border-t border-gray-700 pt-6 mt-8">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 text-red-400">Опасная зона</h3>
|
||||||
|
<button
|
||||||
|
onClick={deleteServer}
|
||||||
|
className="bg-red-600 hover:bg-red-700 px-6 py-2 rounded flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
Удалить сервер
|
||||||
|
</button>
|
||||||
|
<p className="text-sm text-gray-400 mt-2">
|
||||||
|
Это действие нельзя отменить. Все файлы сервера будут удалены.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
91
frontend/src/components/Stats.jsx
Normal file
91
frontend/src/components/Stats.jsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Cpu, HardDrive, Activity } from 'lucide-react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { API_URL } from '../config';
|
||||||
|
|
||||||
|
export default function Stats({ serverName, token }) {
|
||||||
|
const [stats, setStats] = useState({
|
||||||
|
status: 'stopped',
|
||||||
|
cpu: 0,
|
||||||
|
memory: 0,
|
||||||
|
disk: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadStats();
|
||||||
|
const interval = setInterval(loadStats, 2000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [serverName]);
|
||||||
|
|
||||||
|
const loadStats = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${API_URL}/api/servers/${serverName}/stats`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
setStats(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки статистики:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-8 bg-gray-900">
|
||||||
|
<h2 className="text-2xl font-bold mb-6">Статистика сервера</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold">CPU</h3>
|
||||||
|
<Cpu className="w-6 h-6 text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-3xl font-bold mb-2">{stats.cpu}%</div>
|
||||||
|
<div className="w-full bg-gray-700 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="bg-blue-500 h-2 rounded-full transition-all"
|
||||||
|
style={{ width: `${Math.min(stats.cpu, 100)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold">ОЗУ</h3>
|
||||||
|
<Activity className="w-6 h-6 text-green-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-3xl font-bold mb-2">{stats.memory} МБ</div>
|
||||||
|
<div className="w-full bg-gray-700 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="bg-green-500 h-2 rounded-full transition-all"
|
||||||
|
style={{ width: `${Math.min((stats.memory / 2048) * 100, 100)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold">Диск</h3>
|
||||||
|
<HardDrive className="w-6 h-6 text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-3xl font-bold mb-2">{stats.disk} МБ</div>
|
||||||
|
<div className="text-sm text-gray-400 mt-2">
|
||||||
|
Использовано на диске
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Статус</h3>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
className={`w-4 h-4 rounded-full ${
|
||||||
|
stats.status === 'running' ? 'bg-green-500' : 'bg-red-500'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<span className="text-xl">
|
||||||
|
{stats.status === 'running' ? 'Запущен' : 'Остановлен'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
frontend/src/components/ThemeSelector.jsx
Normal file
43
frontend/src/components/ThemeSelector.jsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { Palette } from 'lucide-react';
|
||||||
|
import { themes, getTheme } from '../themes';
|
||||||
|
|
||||||
|
export default function ThemeSelector({ currentTheme, onThemeChange }) {
|
||||||
|
const theme = getTheme(currentTheme);
|
||||||
|
|
||||||
|
const themeColors = {
|
||||||
|
dark: 'bg-gray-800',
|
||||||
|
light: 'bg-gray-100',
|
||||||
|
purple: 'bg-purple-600',
|
||||||
|
blue: 'bg-blue-600',
|
||||||
|
green: 'bg-green-600',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative group">
|
||||||
|
<button className={`p-2 rounded-lg ${theme.hover} transition`}>
|
||||||
|
<Palette className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className={`absolute right-0 mt-2 w-48 ${theme.secondary} rounded-lg shadow-xl ${theme.border} border opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50`}>
|
||||||
|
<div className="p-2">
|
||||||
|
<div className={`text-xs ${theme.textSecondary} px-2 py-1 mb-1`}>Выберите тему</div>
|
||||||
|
{Object.entries(themes).map(([key, themeItem]) => (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
onClick={() => onThemeChange(key)}
|
||||||
|
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg ${theme.hover} transition ${
|
||||||
|
currentTheme === key ? theme.tertiary : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`w-4 h-4 rounded ${themeColors[key]}`} />
|
||||||
|
<span className="text-sm">{themeItem.name}</span>
|
||||||
|
{currentTheme === key && (
|
||||||
|
<span className="ml-auto text-xs text-green-500">✓</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
183
frontend/src/components/Users.jsx
Normal file
183
frontend/src/components/Users.jsx
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Users as UsersIcon, Trash2, Shield, User } from 'lucide-react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { API_URL } from '../config';
|
||||||
|
|
||||||
|
export default function Users({ token }) {
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
const [servers, setServers] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
try {
|
||||||
|
const [usersRes, serversRes] = await Promise.all([
|
||||||
|
axios.get(`${API_URL}/api/users`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
}),
|
||||||
|
axios.get(`${API_URL}/api/servers`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
setUsers(usersRes.data);
|
||||||
|
setServers(serversRes.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки данных:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleServerAccess = async (username, serverName) => {
|
||||||
|
const user = users.find(u => u.username === username);
|
||||||
|
const currentServers = user.servers || [];
|
||||||
|
const newServers = currentServers.includes(serverName)
|
||||||
|
? currentServers.filter(s => s !== serverName)
|
||||||
|
: [...currentServers, serverName];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.put(
|
||||||
|
`${API_URL}/api/users/${username}/servers`,
|
||||||
|
{ servers: newServers },
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
loadData();
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.response?.data?.detail || 'Ошибка обновления доступа');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleRole = async (username, currentRole) => {
|
||||||
|
const newRole = currentRole === 'admin' ? 'user' : 'admin';
|
||||||
|
|
||||||
|
if (!confirm(`Изменить роль пользователя ${username} на ${newRole}?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.put(
|
||||||
|
`${API_URL}/api/users/${username}/role`,
|
||||||
|
{ role: newRole },
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
loadData();
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.response?.data?.detail || 'Ошибка изменения роли');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUser = async (username) => {
|
||||||
|
if (!confirm(`Удалить пользователя ${username}?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.delete(`${API_URL}/api/users/${username}`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
loadData();
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.response?.data?.detail || 'Ошибка удаления пользователя');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="text-gray-400">Загрузка...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-8 bg-gray-900 h-full overflow-y-auto">
|
||||||
|
<h2 className="text-2xl font-bold mb-6 flex items-center gap-2">
|
||||||
|
<UsersIcon className="w-8 h-8" />
|
||||||
|
Управление пользователями
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{users.map((user) => (
|
||||||
|
<div
|
||||||
|
key={user.username}
|
||||||
|
className="bg-gray-800 rounded-lg p-6 border border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`p-2 rounded ${
|
||||||
|
user.role === 'admin' ? 'bg-blue-600' : 'bg-gray-700'
|
||||||
|
}`}>
|
||||||
|
{user.role === 'admin' ? (
|
||||||
|
<Shield className="w-6 h-6" />
|
||||||
|
) : (
|
||||||
|
<User className="w-6 h-6" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold">{user.username}</h3>
|
||||||
|
<p className="text-sm text-gray-400">
|
||||||
|
{user.role === 'admin' ? 'Администратор' : 'Пользователь'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleRole(user.username, user.role)}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded text-sm"
|
||||||
|
>
|
||||||
|
{user.role === 'admin' ? 'Сделать пользователем' : 'Сделать админом'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => deleteUser(user.username)}
|
||||||
|
className="bg-red-600 hover:bg-red-700 p-2 rounded"
|
||||||
|
title="Удалить"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{user.role !== 'admin' && (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium mb-2 text-gray-400">
|
||||||
|
Доступ к серверам:
|
||||||
|
</h4>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{servers.map((server) => {
|
||||||
|
const hasAccess = user.servers?.includes(server.name);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={server.name}
|
||||||
|
onClick={() => toggleServerAccess(user.username, server.name)}
|
||||||
|
className={`px-3 py-1 rounded text-sm transition ${
|
||||||
|
hasAccess
|
||||||
|
? 'bg-green-600 hover:bg-green-700'
|
||||||
|
: 'bg-gray-700 hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{server.displayName}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{servers.length === 0 && (
|
||||||
|
<p className="text-gray-500 text-sm">Нет доступных серверов</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{user.role === 'admin' && (
|
||||||
|
<p className="text-sm text-gray-400">
|
||||||
|
Администратор имеет доступ ко всем серверам
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
frontend/src/config.js
Normal file
25
frontend/src/config.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// Автоматически определяем API URL
|
||||||
|
const getApiUrl = () => {
|
||||||
|
// Если задана переменная окружения, используем её
|
||||||
|
if (import.meta.env.VITE_API_URL) {
|
||||||
|
return import.meta.env.VITE_API_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Иначе используем текущий хост с портом 8000
|
||||||
|
const protocol = window.location.protocol;
|
||||||
|
const hostname = window.location.hostname;
|
||||||
|
|
||||||
|
// Если localhost, используем localhost:8000
|
||||||
|
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||||
|
return `${protocol}//localhost:8000`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для удаленного доступа используем IP:8000
|
||||||
|
return `${protocol}//${hostname}:8000`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const API_URL = getApiUrl();
|
||||||
|
export const WS_URL = API_URL.replace('http', 'ws');
|
||||||
|
|
||||||
|
console.log('API URL:', API_URL);
|
||||||
|
console.log('WS URL:', WS_URL);
|
||||||
22
frontend/src/index.css
Normal file
22
frontend/src/index.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
10
frontend/src/main.jsx
Normal file
10
frontend/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
111
frontend/src/themes.js
Normal file
111
frontend/src/themes.js
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
export const themes = {
|
||||||
|
dark: {
|
||||||
|
name: 'Тёмная',
|
||||||
|
gradient: 'from-blue-400 to-purple-600',
|
||||||
|
primary: 'bg-gray-950',
|
||||||
|
secondary: 'bg-gray-900',
|
||||||
|
tertiary: 'bg-gray-800',
|
||||||
|
accent: 'bg-blue-600',
|
||||||
|
accentHover: 'hover:bg-blue-700',
|
||||||
|
text: 'text-white',
|
||||||
|
textSecondary: 'text-gray-400',
|
||||||
|
border: 'border-gray-800',
|
||||||
|
hover: 'hover:bg-gray-800',
|
||||||
|
input: 'bg-gray-900 border-gray-700',
|
||||||
|
card: 'bg-gray-900',
|
||||||
|
cardHover: 'hover:bg-gray-800',
|
||||||
|
success: 'bg-green-600',
|
||||||
|
successHover: 'hover:bg-green-700',
|
||||||
|
danger: 'bg-red-600',
|
||||||
|
dangerHover: 'hover:bg-red-700',
|
||||||
|
warning: 'bg-yellow-600',
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
name: 'Светлая',
|
||||||
|
gradient: 'from-blue-600 to-purple-600',
|
||||||
|
primary: 'bg-gray-50',
|
||||||
|
secondary: 'bg-white',
|
||||||
|
tertiary: 'bg-gray-100',
|
||||||
|
accent: 'bg-blue-600',
|
||||||
|
accentHover: 'hover:bg-blue-700',
|
||||||
|
text: 'text-gray-900',
|
||||||
|
textSecondary: 'text-gray-600',
|
||||||
|
border: 'border-gray-200',
|
||||||
|
hover: 'hover:bg-gray-100',
|
||||||
|
input: 'bg-white border-gray-300',
|
||||||
|
card: 'bg-white',
|
||||||
|
cardHover: 'hover:bg-gray-50',
|
||||||
|
success: 'bg-green-600',
|
||||||
|
successHover: 'hover:bg-green-700',
|
||||||
|
danger: 'bg-red-600',
|
||||||
|
dangerHover: 'hover:bg-red-700',
|
||||||
|
warning: 'bg-yellow-600',
|
||||||
|
},
|
||||||
|
purple: {
|
||||||
|
name: 'Фиолетовая',
|
||||||
|
gradient: 'from-purple-400 to-pink-600',
|
||||||
|
primary: 'bg-slate-950',
|
||||||
|
secondary: 'bg-slate-900',
|
||||||
|
tertiary: 'bg-purple-900/30',
|
||||||
|
accent: 'bg-purple-600',
|
||||||
|
accentHover: 'hover:bg-purple-700',
|
||||||
|
text: 'text-white',
|
||||||
|
textSecondary: 'text-purple-300',
|
||||||
|
border: 'border-purple-900/50',
|
||||||
|
hover: 'hover:bg-purple-900/30',
|
||||||
|
input: 'bg-slate-900 border-purple-900/50',
|
||||||
|
card: 'bg-slate-900',
|
||||||
|
cardHover: 'hover:bg-purple-900/30',
|
||||||
|
success: 'bg-green-600',
|
||||||
|
successHover: 'hover:bg-green-700',
|
||||||
|
danger: 'bg-red-600',
|
||||||
|
dangerHover: 'hover:bg-red-700',
|
||||||
|
warning: 'bg-yellow-600',
|
||||||
|
},
|
||||||
|
blue: {
|
||||||
|
name: 'Синяя',
|
||||||
|
gradient: 'from-cyan-400 to-blue-600',
|
||||||
|
primary: 'bg-slate-950',
|
||||||
|
secondary: 'bg-slate-900',
|
||||||
|
tertiary: 'bg-blue-900/30',
|
||||||
|
accent: 'bg-blue-500',
|
||||||
|
accentHover: 'hover:bg-blue-600',
|
||||||
|
text: 'text-white',
|
||||||
|
textSecondary: 'text-blue-300',
|
||||||
|
border: 'border-blue-900/50',
|
||||||
|
hover: 'hover:bg-blue-900/30',
|
||||||
|
input: 'bg-slate-900 border-blue-900/50',
|
||||||
|
card: 'bg-slate-900',
|
||||||
|
cardHover: 'hover:bg-blue-900/30',
|
||||||
|
success: 'bg-green-600',
|
||||||
|
successHover: 'hover:bg-green-700',
|
||||||
|
danger: 'bg-red-600',
|
||||||
|
dangerHover: 'hover:bg-red-700',
|
||||||
|
warning: 'bg-yellow-600',
|
||||||
|
},
|
||||||
|
green: {
|
||||||
|
name: 'Зелёная',
|
||||||
|
gradient: 'from-emerald-400 to-green-600',
|
||||||
|
primary: 'bg-slate-950',
|
||||||
|
secondary: 'bg-slate-900',
|
||||||
|
tertiary: 'bg-green-900/30',
|
||||||
|
accent: 'bg-green-600',
|
||||||
|
accentHover: 'hover:bg-green-700',
|
||||||
|
text: 'text-white',
|
||||||
|
textSecondary: 'text-green-300',
|
||||||
|
border: 'border-green-900/50',
|
||||||
|
hover: 'hover:bg-green-900/30',
|
||||||
|
input: 'bg-slate-900 border-green-900/50',
|
||||||
|
card: 'bg-slate-900',
|
||||||
|
cardHover: 'hover:bg-green-900/30',
|
||||||
|
success: 'bg-green-600',
|
||||||
|
successHover: 'hover:bg-green-700',
|
||||||
|
danger: 'bg-red-600',
|
||||||
|
dangerHover: 'hover:bg-red-700',
|
||||||
|
warning: 'bg-yellow-600',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTheme = (themeName) => {
|
||||||
|
return themes[themeName] || themes.dark;
|
||||||
|
};
|
||||||
5
frontend/start.bat
Normal file
5
frontend/start.bat
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@echo off
|
||||||
|
echo Starting MC Panel Frontend...
|
||||||
|
cd /d "%~dp0"
|
||||||
|
npm run dev
|
||||||
|
pause
|
||||||
11
frontend/tailwind.config.js
Normal file
11
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
10
frontend/vite.config.js
Normal file
10
frontend/vite.config.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
host: true
|
||||||
|
}
|
||||||
|
});
|
||||||
182
test_connection.html
Normal file
182
test_connection.html
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Тест подключения MC Panel</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 50px auto;
|
||||||
|
padding: 20px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.test {
|
||||||
|
background: #2a2a2a;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
border-left: 4px solid #666;
|
||||||
|
}
|
||||||
|
.test.success {
|
||||||
|
border-left-color: #4caf50;
|
||||||
|
}
|
||||||
|
.test.error {
|
||||||
|
border-left-color: #f44336;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: #2196F3;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background: #1976D2;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background: #000;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
background: #333;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>🔧 Тест подключения MC Panel</h1>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<h3>Информация о подключении:</h3>
|
||||||
|
<p><strong>Текущий URL:</strong> <span id="currentUrl"></span></p>
|
||||||
|
<p><strong>API URL:</strong> <span id="apiUrl"></span></p>
|
||||||
|
<p><strong>WebSocket URL:</strong> <span id="wsUrl"></span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onclick="runTests()">▶️ Запустить тесты</button>
|
||||||
|
|
||||||
|
<div id="results"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const protocol = window.location.protocol;
|
||||||
|
const hostname = window.location.hostname || 'localhost';
|
||||||
|
const apiUrl = `${protocol}//${hostname}:8000`;
|
||||||
|
const wsUrl = apiUrl.replace('http', 'ws');
|
||||||
|
|
||||||
|
document.getElementById('currentUrl').textContent = window.location.href;
|
||||||
|
document.getElementById('apiUrl').textContent = apiUrl;
|
||||||
|
document.getElementById('wsUrl').textContent = wsUrl;
|
||||||
|
|
||||||
|
async function runTests() {
|
||||||
|
const results = document.getElementById('results');
|
||||||
|
results.innerHTML = '<h2>Результаты тестов:</h2>';
|
||||||
|
|
||||||
|
// Тест 1: Проверка доступности API
|
||||||
|
await testEndpoint('GET', '/api/servers', 'Получение списка серверов');
|
||||||
|
|
||||||
|
// Тест 2: Создание тестового сервера
|
||||||
|
const serverName = 'test_' + Date.now();
|
||||||
|
const created = await testEndpoint('POST', '/api/servers/create', 'Создание тестового сервера', {
|
||||||
|
name: serverName,
|
||||||
|
displayName: 'Тестовый сервер',
|
||||||
|
startCommand: 'java -Xmx1G -Xms1G -jar server.jar nogui'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (created) {
|
||||||
|
// Тест 3: Получение конфигурации
|
||||||
|
await testEndpoint('GET', `/api/servers/${serverName}/config`, 'Получение конфигурации сервера');
|
||||||
|
|
||||||
|
// Тест 4: Получение файлов
|
||||||
|
await testEndpoint('GET', `/api/servers/${serverName}/files`, 'Получение списка файлов');
|
||||||
|
|
||||||
|
// Тест 5: Удаление тестового сервера
|
||||||
|
await testEndpoint('DELETE', `/api/servers/${serverName}`, 'Удаление тестового сервера');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Тест 6: WebSocket
|
||||||
|
testWebSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testEndpoint(method, path, description, body = null) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'test';
|
||||||
|
div.innerHTML = `<strong>${description}</strong><br>`;
|
||||||
|
document.getElementById('results').appendChild(div);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (body) {
|
||||||
|
options.body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${apiUrl}${path}`, options);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
div.className = 'test success';
|
||||||
|
div.innerHTML += `✅ Успешно (${response.status})<br>`;
|
||||||
|
div.innerHTML += `<pre>${JSON.stringify(data, null, 2)}</pre>`;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
div.className = 'test error';
|
||||||
|
div.innerHTML += `❌ Ошибка (${response.status})<br>`;
|
||||||
|
div.innerHTML += `<pre>${JSON.stringify(data, null, 2)}</pre>`;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
div.className = 'test error';
|
||||||
|
div.innerHTML += `❌ Ошибка подключения: ${error.message}`;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testWebSocket() {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'test';
|
||||||
|
div.innerHTML = `<strong>Тест WebSocket подключения</strong><br>`;
|
||||||
|
document.getElementById('results').appendChild(div);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ws = new WebSocket(`${wsUrl}/ws/servers/test/console`);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
div.className = 'test success';
|
||||||
|
div.innerHTML += `✅ WebSocket подключен успешно`;
|
||||||
|
ws.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
div.className = 'test error';
|
||||||
|
div.innerHTML += `❌ Ошибка WebSocket: ${error}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (ws.readyState !== WebSocket.OPEN) {
|
||||||
|
div.className = 'test error';
|
||||||
|
div.innerHTML += `❌ WebSocket не подключился за 5 секунд`;
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
} catch (error) {
|
||||||
|
div.className = 'test error';
|
||||||
|
div.innerHTML += `❌ Ошибка создания WebSocket: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
78
БЫСТРЫЙ_СТАРТ.md
Normal file
78
БЫСТРЫЙ_СТАРТ.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# 🚀 Быстрый старт MC Panel
|
||||||
|
|
||||||
|
## Запуск панели
|
||||||
|
|
||||||
|
### 1️⃣ Запустите бэкенд
|
||||||
|
Откройте первый терминал:
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python main_new.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2️⃣ Запустите фронтенд
|
||||||
|
Откройте второй терминал:
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3️⃣ Откройте в браузере
|
||||||
|
```
|
||||||
|
http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4️⃣ Войдите в систему
|
||||||
|
- **Логин**: `admin`
|
||||||
|
- **Пароль**: `admin`
|
||||||
|
|
||||||
|
## 🎨 Смена темы
|
||||||
|
|
||||||
|
1. Нажмите на иконку палитры (🎨) в правом верхнем углу
|
||||||
|
2. Выберите тему:
|
||||||
|
- 🌑 Тёмная
|
||||||
|
- ☀️ Светлая
|
||||||
|
- 💜 Фиолетовая
|
||||||
|
- 💙 Синяя
|
||||||
|
- 💚 Зелёная
|
||||||
|
|
||||||
|
## 📋 Основные функции
|
||||||
|
|
||||||
|
### Создание сервера
|
||||||
|
1. Нажмите кнопку "+" в левой панели
|
||||||
|
2. Заполните форму:
|
||||||
|
- Имя папки (латиница, цифры, _ и -)
|
||||||
|
- Отображаемое имя
|
||||||
|
- Команда запуска
|
||||||
|
3. Нажмите "Создать"
|
||||||
|
|
||||||
|
### Управление сервером
|
||||||
|
- **Запуск**: кнопка "Запустить" на карточке сервера
|
||||||
|
- **Остановка**: кнопка "Остановить" на карточке сервера
|
||||||
|
- **Консоль**: вкладка "Консоль" для просмотра логов
|
||||||
|
- **Файлы**: вкладка "Файлы" для управления файлами
|
||||||
|
- **Статистика**: вкладка "Статистика" для мониторинга ресурсов
|
||||||
|
- **Настройки**: вкладка "Настройки" для изменения параметров
|
||||||
|
|
||||||
|
### Управление пользователями (только для админов)
|
||||||
|
1. Нажмите кнопку "Пользователи" в header
|
||||||
|
2. Создайте новых пользователей
|
||||||
|
3. Выдайте доступ к серверам
|
||||||
|
|
||||||
|
### Выдача доступа к серверу
|
||||||
|
1. Выберите сервер
|
||||||
|
2. Перейдите в "Настройки"
|
||||||
|
3. В разделе "Управление доступом" выберите пользователя
|
||||||
|
4. Нажмите "Выдать доступ"
|
||||||
|
|
||||||
|
## 🌐 Доступ через сеть
|
||||||
|
|
||||||
|
Панель автоматически определяет ваш IP-адрес и работает через:
|
||||||
|
- Локальную сеть
|
||||||
|
- Radmin VPN
|
||||||
|
- Другие VPN-сети
|
||||||
|
|
||||||
|
Друзья могут подключиться по вашему IP-адресу на порту 3000.
|
||||||
|
|
||||||
|
## ✅ Готово!
|
||||||
|
|
||||||
|
Панель готова к использованию. Создавайте серверы, управляйте ими и наслаждайтесь современным интерфейсом! 🎉
|
||||||
Reference in New Issue
Block a user