Initial commit

This commit is contained in:
2026-01-14 20:23:10 +06:00
commit 954dd473d1
57 changed files with 8854 additions and 0 deletions

36
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
VITE_API_URL=http://26.62.117.104:8000

3
frontend/.env.example Normal file
View File

@@ -0,0 +1,3 @@
# API URL (необязательно, по умолчанию определяется автоматически)
# Раскомментируйте и укажите ваш IP для удаленного доступа
# VITE_API_URL=http://26.123.45.67:8000

View 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
View 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

File diff suppressed because it is too large Load Diff

28
frontend/package.json Normal file
View 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"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

392
frontend/src/App.jsx Normal file
View 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
View 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;

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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;

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,5 @@
@echo off
echo Starting MC Panel Frontend...
cd /d "%~dp0"
npm run dev
pause

View 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
View 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
View 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>

View 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.
## ✅ Готово!
Панель готова к использованию. Создавайте серверы, управляйте ими и наслаждайтесь современным интерфейсом! 🎉