Added Daemon system and fixed interface
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
264
DAEMON_SETUP.md
Normal file
264
DAEMON_SETUP.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# Настройка системы демонов MC Panel
|
||||
|
||||
## Что такое демоны?
|
||||
|
||||
Демоны (Daemons) - это удаленные серверы, на которых можно запускать Minecraft серверы. Система демонов позволяет:
|
||||
|
||||
- Распределять серверы по разным физическим машинам
|
||||
- Масштабировать инфраструктуру
|
||||
- Управлять серверами на разных локациях из одной панели
|
||||
- Балансировать нагрузку между серверами
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
### 1. Установка демона на удаленный сервер
|
||||
|
||||
#### Windows:
|
||||
```bash
|
||||
# 1. Скопируйте папку daemon на удаленный сервер
|
||||
# 2. Откройте командную строку в папке daemon
|
||||
# 3. Установите зависимости
|
||||
install.bat
|
||||
|
||||
# 4. Настройте .env файл
|
||||
copy .env.example .env
|
||||
notepad .env
|
||||
|
||||
# 5. Запустите демон
|
||||
start.bat
|
||||
```
|
||||
|
||||
#### Linux:
|
||||
```bash
|
||||
# 1. Скопируйте папку daemon на удаленный сервер
|
||||
# 2. Установите зависимости
|
||||
cd daemon
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 3. Настройте .env файл
|
||||
cp .env.example .env
|
||||
nano .env
|
||||
|
||||
# 4. Запустите демон
|
||||
python main.py
|
||||
```
|
||||
|
||||
### 2. Настройка .env файла демона
|
||||
|
||||
```env
|
||||
# Уникальный ID демона
|
||||
DAEMON_ID=daemon-1
|
||||
|
||||
# Отображаемое имя
|
||||
DAEMON_NAME=Main Server
|
||||
|
||||
# Порт для API
|
||||
DAEMON_PORT=24444
|
||||
|
||||
# Секретный ключ (сгенерируйте случайный)
|
||||
DAEMON_KEY=your-secret-key-here
|
||||
|
||||
# Директория для серверов
|
||||
SERVERS_DIR=./servers
|
||||
```
|
||||
|
||||
**Важно:** Сгенерируйте надежный ключ:
|
||||
```python
|
||||
import secrets
|
||||
print(secrets.token_urlsafe(32))
|
||||
```
|
||||
|
||||
### 3. Подключение демона к панели
|
||||
|
||||
1. Откройте основную панель управления
|
||||
2. Войдите как владелец (owner) или администратор (admin)
|
||||
3. В боковом меню нажмите "Демоны" (иконка сервера)
|
||||
4. Нажмите "Добавить демон"
|
||||
5. Заполните форму:
|
||||
- **Название**: Main Server (или любое другое)
|
||||
- **IP адрес**: IP адрес сервера с демоном
|
||||
- **Порт**: 24444 (или ваш порт из .env)
|
||||
- **Ключ демона**: ваш DAEMON_KEY из .env
|
||||
- **Примечания**: дополнительная информация (необязательно)
|
||||
6. Нажмите "Добавить"
|
||||
|
||||
### 4. Проверка подключения
|
||||
|
||||
После добавления демон должен отображаться со статусом "Онлайн" (зеленый индикатор).
|
||||
|
||||
Вы увидите:
|
||||
- Статус демона (онлайн/оффлайн)
|
||||
- Использование CPU, ОЗУ и диска
|
||||
- Количество серверов на демоне
|
||||
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ Основная панель │ (порт 8000)
|
||||
│ (Frontend + │
|
||||
│ Backend) │
|
||||
└────────┬─────────┘
|
||||
│
|
||||
│ HTTP API
|
||||
│
|
||||
┌────┴────┬────────┬────────┐
|
||||
│ │ │ │
|
||||
┌───▼───┐ ┌──▼───┐ ┌──▼───┐ ┌──▼───┐
|
||||
│Daemon1│ │Daemon2│ │Daemon3│ │... │
|
||||
│(24444)│ │(24444)│ │(24444)│ │ │
|
||||
└───┬───┘ └──┬───┘ └──┬───┘ └──────┘
|
||||
│ │ │
|
||||
┌───▼───┐ ┌──▼───┐ ┌──▼───┐
|
||||
│Server1│ │Server2│ │Server3│
|
||||
│Server2│ │Server3│ │Server4│
|
||||
└───────┘ └──────┘ └──────┘
|
||||
```
|
||||
|
||||
## Безопасность
|
||||
|
||||
### 1. Файрвол
|
||||
|
||||
Настройте файрвол, чтобы разрешить доступ к порту демона только с IP основной панели:
|
||||
|
||||
#### Windows (PowerShell):
|
||||
```powershell
|
||||
New-NetFirewallRule -DisplayName "MC Panel Daemon" -Direction Inbound -LocalPort 24444 -Protocol TCP -Action Allow -RemoteAddress "IP_ПАНЕЛИ"
|
||||
```
|
||||
|
||||
#### Linux (ufw):
|
||||
```bash
|
||||
sudo ufw allow from IP_ПАНЕЛИ to any port 24444
|
||||
```
|
||||
|
||||
#### Linux (iptables):
|
||||
```bash
|
||||
sudo iptables -A INPUT -p tcp -s IP_ПАНЕЛИ --dport 24444 -j ACCEPT
|
||||
sudo iptables -A INPUT -p tcp --dport 24444 -j DROP
|
||||
```
|
||||
|
||||
### 2. HTTPS (рекомендуется для продакшена)
|
||||
|
||||
Используйте reverse proxy (nginx) с SSL сертификатом:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name daemon.example.com;
|
||||
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:24444;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Сильные ключи
|
||||
|
||||
- Используйте случайные ключи длиной минимум 32 символа
|
||||
- Не используйте одинаковые ключи для разных демонов
|
||||
- Храните ключи в безопасности
|
||||
|
||||
## Запуск как сервис
|
||||
|
||||
### Linux (systemd)
|
||||
|
||||
1. Создайте файл `/etc/systemd/system/mcpanel-daemon.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=MC Panel Daemon
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=mcpanel
|
||||
WorkingDirectory=/path/to/daemon
|
||||
ExecStart=/usr/bin/python3 /path/to/daemon/main.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
2. Запустите сервис:
|
||||
|
||||
```bash
|
||||
sudo systemctl enable mcpanel-daemon
|
||||
sudo systemctl start mcpanel-daemon
|
||||
sudo systemctl status mcpanel-daemon
|
||||
```
|
||||
|
||||
### Windows (NSSM)
|
||||
|
||||
1. Скачайте NSSM: https://nssm.cc/download
|
||||
2. Установите сервис:
|
||||
|
||||
```cmd
|
||||
nssm install MCPanelDaemon "C:\Python\python.exe" "C:\path\to\daemon\main.py"
|
||||
nssm set MCPanelDaemon AppDirectory "C:\path\to\daemon"
|
||||
nssm start MCPanelDaemon
|
||||
```
|
||||
|
||||
## Управление серверами на демонах
|
||||
|
||||
После подключения демона вы можете:
|
||||
|
||||
1. **Создавать серверы** - при создании сервера можно будет выбрать демон
|
||||
2. **Просматривать статистику** - CPU, ОЗУ, диск каждого демона
|
||||
3. **Управлять серверами** - запуск, остановка, консоль, файлы
|
||||
4. **Мониторить состояние** - статус демонов обновляется автоматически
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Демон показывает статус "Оффлайн"
|
||||
|
||||
1. Проверьте, что демон запущен на удаленном сервере
|
||||
2. Проверьте файрвол и порты
|
||||
3. Проверьте, что ключ в панели совпадает с DAEMON_KEY
|
||||
4. Проверьте IP адрес и порт
|
||||
5. Проверьте логи демона
|
||||
|
||||
### Ошибка "Connection error"
|
||||
|
||||
- Проверьте сетевое подключение между панелью и демоном
|
||||
- Проверьте, что порт не заблокирован файрволом
|
||||
- Попробуйте подключиться вручную: `curl http://IP:24444/api/status`
|
||||
|
||||
### Ошибка "Invalid daemon key"
|
||||
|
||||
- Проверьте, что ключ в панели точно совпадает с DAEMON_KEY в .env
|
||||
- Убедитесь, что нет лишних пробелов или символов
|
||||
- Перезапустите демон после изменения .env
|
||||
|
||||
## Мониторинг
|
||||
|
||||
Демоны автоматически отправляют информацию о:
|
||||
- Использовании CPU
|
||||
- Использовании ОЗУ
|
||||
- Использовании диска
|
||||
- Количестве серверов
|
||||
- Количестве запущенных серверов
|
||||
|
||||
Эта информация обновляется каждые 10 секунд в интерфейсе панели.
|
||||
|
||||
## Масштабирование
|
||||
|
||||
Вы можете добавить неограниченное количество демонов:
|
||||
|
||||
1. Установите демон на новый сервер
|
||||
2. Используйте уникальный DAEMON_ID для каждого демона
|
||||
3. Добавьте демон в панель
|
||||
4. Распределяйте серверы между демонами
|
||||
|
||||
## Поддержка
|
||||
|
||||
Если у вас возникли проблемы:
|
||||
1. Проверьте логи демона
|
||||
2. Проверьте логи основной панели
|
||||
3. Создайте тикет в системе поддержки
|
||||
331
backend/daemons.py
Normal file
331
backend/daemons.py
Normal file
@@ -0,0 +1,331 @@
|
||||
"""
|
||||
Управление демонами (удаленными серверами)
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, Header
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
import json
|
||||
import httpx
|
||||
from pathlib import Path
|
||||
from jose import JWTError, jwt
|
||||
|
||||
router = APIRouter()
|
||||
security = HTTPBearer(auto_error=False)
|
||||
|
||||
# Файл с конфигурацией демонов
|
||||
DAEMONS_FILE = Path("backend/data/daemons.json")
|
||||
DAEMONS_FILE.parent.mkdir(exist_ok=True)
|
||||
|
||||
# Файл с пользователями - проверяем оба возможных пути
|
||||
USERS_FILE = Path("backend/users.json") if Path("backend/users.json").exists() else Path("users.json")
|
||||
|
||||
# Настройки JWT (должны совпадать с main.py)
|
||||
SECRET_KEY = "your-secret-key-change-this-in-production-12345"
|
||||
ALGORITHM = "HS256"
|
||||
|
||||
|
||||
def load_users_dict():
|
||||
"""Загрузить пользователей из файла"""
|
||||
if USERS_FILE.exists():
|
||||
with open(USERS_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
|
||||
def get_current_user_from_token(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="Неверный токен")
|
||||
|
||||
# Пытаемся получить роль из токена
|
||||
role = payload.get("role")
|
||||
|
||||
print(f"[DEBUG] Username from token: {username}")
|
||||
print(f"[DEBUG] Role from token: {role}")
|
||||
|
||||
# Если роли нет в токене, загружаем из базы
|
||||
if not role:
|
||||
print(f"[DEBUG] Role not in token, loading from database...")
|
||||
print(f"[DEBUG] USERS_FILE path: {USERS_FILE}")
|
||||
print(f"[DEBUG] USERS_FILE exists: {USERS_FILE.exists()}")
|
||||
|
||||
users = load_users_dict()
|
||||
print(f"[DEBUG] Loaded users: {list(users.keys())}")
|
||||
|
||||
if username not in users:
|
||||
raise HTTPException(status_code=401, detail="Пользователь не найден")
|
||||
role = users[username].get("role", "user")
|
||||
print(f"[DEBUG] Role from database: {role}")
|
||||
|
||||
print(f"[DEBUG] Final role: {role}")
|
||||
return {"username": username, "role": role}
|
||||
except JWTError as e:
|
||||
print(f"[DEBUG] JWT Error: {e}")
|
||||
raise HTTPException(status_code=401, detail="Неверный токен")
|
||||
|
||||
|
||||
class DaemonCreate(BaseModel):
|
||||
name: str
|
||||
address: str
|
||||
port: int
|
||||
key: str
|
||||
remarks: Optional[str] = ""
|
||||
|
||||
|
||||
class DaemonUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
port: Optional[int] = None
|
||||
key: Optional[str] = None
|
||||
remarks: Optional[str] = None
|
||||
|
||||
|
||||
def load_daemons():
|
||||
"""Загрузить список демонов"""
|
||||
if not DAEMONS_FILE.exists():
|
||||
return {}
|
||||
|
||||
with open(DAEMONS_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def save_daemons(daemons: dict):
|
||||
"""Сохранить список демонов"""
|
||||
with open(DAEMONS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(daemons, f, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
async def check_daemon_connection(address: str, port: int, key: str) -> dict:
|
||||
"""Проверить подключение к демону"""
|
||||
url = f"http://{address}:{port}/api/status"
|
||||
headers = {"Authorization": f"Bearer {key}"}
|
||||
|
||||
print(f"[DEBUG] Checking daemon connection:")
|
||||
print(f"[DEBUG] URL: {url}")
|
||||
print(f"[DEBUG] Key: {key[:20]}...")
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
response = await client.get(url, headers=headers)
|
||||
print(f"[DEBUG] Status: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"[DEBUG] Response: {data}")
|
||||
return data
|
||||
else:
|
||||
print(f"[DEBUG] Error response: {response.text}")
|
||||
raise HTTPException(status_code=400, detail=f"Failed to connect to daemon: {response.status_code}")
|
||||
except httpx.RequestError as e:
|
||||
print(f"[DEBUG] Connection error: {e}")
|
||||
raise HTTPException(status_code=400, detail=f"Connection error: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/api/daemons")
|
||||
async def get_daemons(current_user: dict = Depends(get_current_user_from_token)):
|
||||
"""Получить список всех демонов"""
|
||||
# Только админы и владельцы могут видеть демоны
|
||||
if current_user["role"] not in ["owner", "admin"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
daemons = load_daemons()
|
||||
|
||||
# Проверяем статус каждого демона
|
||||
result = []
|
||||
for daemon_id, daemon in daemons.items():
|
||||
daemon_info = {
|
||||
"id": daemon_id,
|
||||
**daemon,
|
||||
"status": "offline"
|
||||
}
|
||||
|
||||
try:
|
||||
# Пытаемся получить статус
|
||||
status = await check_daemon_connection(
|
||||
daemon["address"],
|
||||
daemon["port"],
|
||||
daemon["key"]
|
||||
)
|
||||
daemon_info["status"] = "online"
|
||||
daemon_info["system"] = status.get("system", {})
|
||||
daemon_info["servers"] = status.get("servers", {})
|
||||
except:
|
||||
pass
|
||||
|
||||
result.append(daemon_info)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/api/daemons")
|
||||
async def create_daemon(
|
||||
daemon: DaemonCreate,
|
||||
current_user: dict = Depends(get_current_user_from_token)
|
||||
):
|
||||
"""Добавить новый демон"""
|
||||
if current_user["role"] not in ["owner", "admin"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
# Проверяем подключение
|
||||
await check_daemon_connection(daemon.address, daemon.port, daemon.key)
|
||||
|
||||
daemons = load_daemons()
|
||||
|
||||
# Генерируем ID
|
||||
daemon_id = f"daemon-{len(daemons) + 1}"
|
||||
|
||||
daemons[daemon_id] = {
|
||||
"name": daemon.name,
|
||||
"address": daemon.address,
|
||||
"port": daemon.port,
|
||||
"key": daemon.key,
|
||||
"remarks": daemon.remarks,
|
||||
"created_at": str(Path().cwd()) # Временная метка
|
||||
}
|
||||
|
||||
save_daemons(daemons)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"daemon_id": daemon_id,
|
||||
"message": "Daemon added successfully"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/api/daemons/{daemon_id}")
|
||||
async def get_daemon(
|
||||
daemon_id: str,
|
||||
current_user: dict = Depends(get_current_user_from_token)
|
||||
):
|
||||
"""Получить информацию о демоне"""
|
||||
if current_user["role"] not in ["owner", "admin"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
daemons = load_daemons()
|
||||
|
||||
if daemon_id not in daemons:
|
||||
raise HTTPException(status_code=404, detail="Daemon not found")
|
||||
|
||||
daemon = daemons[daemon_id]
|
||||
|
||||
# Получаем статус
|
||||
try:
|
||||
status = await check_daemon_connection(
|
||||
daemon["address"],
|
||||
daemon["port"],
|
||||
daemon["key"]
|
||||
)
|
||||
daemon["status"] = "online"
|
||||
daemon["system"] = status.get("system", {})
|
||||
daemon["servers"] = status.get("servers", {})
|
||||
except:
|
||||
daemon["status"] = "offline"
|
||||
|
||||
return {
|
||||
"id": daemon_id,
|
||||
**daemon
|
||||
}
|
||||
|
||||
|
||||
@router.put("/api/daemons/{daemon_id}")
|
||||
async def update_daemon(
|
||||
daemon_id: str,
|
||||
daemon_update: DaemonUpdate,
|
||||
current_user: dict = Depends(get_current_user_from_token)
|
||||
):
|
||||
"""Обновить демон"""
|
||||
if current_user["role"] not in ["owner", "admin"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
daemons = load_daemons()
|
||||
|
||||
if daemon_id not in daemons:
|
||||
raise HTTPException(status_code=404, detail="Daemon not found")
|
||||
|
||||
# Обновляем поля
|
||||
daemon = daemons[daemon_id]
|
||||
|
||||
if daemon_update.name:
|
||||
daemon["name"] = daemon_update.name
|
||||
if daemon_update.address:
|
||||
daemon["address"] = daemon_update.address
|
||||
if daemon_update.port:
|
||||
daemon["port"] = daemon_update.port
|
||||
if daemon_update.key:
|
||||
daemon["key"] = daemon_update.key
|
||||
if daemon_update.remarks is not None:
|
||||
daemon["remarks"] = daemon_update.remarks
|
||||
|
||||
# Проверяем подключение с новыми данными
|
||||
await check_daemon_connection(
|
||||
daemon["address"],
|
||||
daemon["port"],
|
||||
daemon["key"]
|
||||
)
|
||||
|
||||
save_daemons(daemons)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Daemon updated successfully"
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/api/daemons/{daemon_id}")
|
||||
async def delete_daemon(
|
||||
daemon_id: str,
|
||||
current_user: dict = Depends(get_current_user_from_token)
|
||||
):
|
||||
"""Удалить демон"""
|
||||
if current_user["role"] not in ["owner", "admin"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
daemons = load_daemons()
|
||||
|
||||
if daemon_id not in daemons:
|
||||
raise HTTPException(status_code=404, detail="Daemon not found")
|
||||
|
||||
del daemons[daemon_id]
|
||||
save_daemons(daemons)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Daemon deleted successfully"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/api/daemons/{daemon_id}/servers")
|
||||
async def get_daemon_servers(
|
||||
daemon_id: str,
|
||||
current_user: dict = Depends(get_current_user_from_token)
|
||||
):
|
||||
"""Получить список серверов на демоне"""
|
||||
daemons = load_daemons()
|
||||
|
||||
if daemon_id not in daemons:
|
||||
raise HTTPException(status_code=404, detail="Daemon not found")
|
||||
|
||||
daemon = daemons[daemon_id]
|
||||
|
||||
url = f"http://{daemon['address']}:{daemon['port']}/api/servers"
|
||||
headers = {"Authorization": f"Bearer {daemon['key']}"}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(url, headers=headers)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Failed to get servers from daemon")
|
||||
except httpx.RequestError as e:
|
||||
raise HTTPException(status_code=400, detail=f"Connection error: {str(e)}")
|
||||
10
backend/data/daemons.json
Normal file
10
backend/data/daemons.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"daemon-1": {
|
||||
"name": "Test",
|
||||
"address": "127.0.0.1",
|
||||
"port": 24444,
|
||||
"key": "JLgYFjTlFOqdyT49vmCqlXrLAuVE6FjiCdqf3zsZfr4",
|
||||
"remarks": "",
|
||||
"created_at": "D:\\Desktop\\adadad"
|
||||
}
|
||||
}
|
||||
113
backend/main.py
113
backend/main.py
@@ -332,7 +332,7 @@ async def register(data: dict):
|
||||
|
||||
save_users(users)
|
||||
|
||||
access_token = create_access_token(data={"sub": username})
|
||||
access_token = create_access_token(data={"sub": username, "role": role})
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
@@ -353,7 +353,7 @@ async def login(data: dict):
|
||||
if not verify_password(password, user["password"]):
|
||||
raise HTTPException(401, "Неверное имя пользователя или пароль")
|
||||
|
||||
access_token = create_access_token(data={"sub": username})
|
||||
access_token = create_access_token(data={"sub": username, "role": user["role"]})
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
@@ -838,12 +838,12 @@ async def get_servers(user: dict = Depends(get_current_user)):
|
||||
servers = []
|
||||
try:
|
||||
# Владелец и администратор видят все серверы
|
||||
can_view_all = user.get("role") in ["owner", "admin"] or user.get("permissions", {}).get("view_all_resources", False)
|
||||
is_admin_or_owner = user.get("role") in ["owner", "admin"]
|
||||
|
||||
for server_dir in SERVERS_DIR.iterdir():
|
||||
if server_dir.is_dir():
|
||||
# Проверка доступа: владелец/админ видят всё, остальные только свои
|
||||
if not can_view_all and server_dir.name not in user.get("servers", []):
|
||||
if not is_admin_or_owner and server_dir.name not in user.get("servers", []):
|
||||
continue
|
||||
|
||||
config = load_server_config(server_dir.name)
|
||||
@@ -859,9 +859,14 @@ async def get_servers(user: dict = Depends(get_current_user)):
|
||||
servers.append({
|
||||
"name": server_dir.name,
|
||||
"displayName": config.get("displayName", server_dir.name),
|
||||
"status": "running" if is_running else "stopped"
|
||||
"status": "running" if is_running else "stopped",
|
||||
"owner": config.get("owner", "unknown")
|
||||
})
|
||||
print(f"Найдено серверов для {user['username']} ({user.get('role', 'user')}): {len(servers)}")
|
||||
|
||||
print(f"[DEBUG] User: {user['username']} (role: {user.get('role', 'user')})")
|
||||
print(f"[DEBUG] Is admin/owner: {is_admin_or_owner}")
|
||||
print(f"[DEBUG] Servers found: {len(servers)}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка загрузки серверов: {e}")
|
||||
return servers
|
||||
@@ -869,34 +874,84 @@ async def get_servers(user: dict = Depends(get_current_user)):
|
||||
@app.post("/api/servers/create")
|
||||
async def create_server(data: dict, user: dict = Depends(get_current_user)):
|
||||
server_name = data.get("name", "").strip()
|
||||
daemon_id = data.get("daemonId", "local")
|
||||
|
||||
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, "Сервер с таким именем уже существует")
|
||||
# Если создаем на демоне
|
||||
if daemon_id != "local":
|
||||
# Загружаем демоны
|
||||
from daemons import load_daemons
|
||||
daemons = load_daemons()
|
||||
|
||||
if daemon_id not in daemons:
|
||||
raise HTTPException(404, "Демон не найден")
|
||||
|
||||
daemon = daemons[daemon_id]
|
||||
|
||||
# Отправляем запрос на создание сервера на демоне
|
||||
url = f"http://{daemon['address']}:{daemon['port']}/api/servers/create"
|
||||
headers = {"Authorization": f"Bearer {daemon['key']}"}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.post(url, json={
|
||||
"name": server_name,
|
||||
"displayName": data.get("displayName", server_name),
|
||||
"startCommand": data.get("startCommand", "java -Xmx2G -Xms1G -jar server.jar nogui"),
|
||||
"owner": user["username"]
|
||||
}, headers=headers)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise HTTPException(400, f"Ошибка создания сервера на демоне: {response.text}")
|
||||
except httpx.RequestError as e:
|
||||
raise HTTPException(400, f"Ошибка подключения к демону: {str(e)}")
|
||||
|
||||
# Сохраняем информацию о сервере локально
|
||||
config = {
|
||||
"name": server_name,
|
||||
"displayName": data.get("displayName", server_name),
|
||||
"startCommand": data.get("startCommand", "java -Xmx2G -Xms1G -jar server.jar nogui"),
|
||||
"owner": user["username"],
|
||||
"daemonId": daemon_id,
|
||||
"daemonName": daemon["name"]
|
||||
}
|
||||
|
||||
# Создаем локальную запись о сервере
|
||||
server_path = SERVERS_DIR / f"{daemon_id}_{server_name}"
|
||||
server_path.mkdir(parents=True, exist_ok=True)
|
||||
save_server_config(f"{daemon_id}_{server_name}", config)
|
||||
|
||||
else:
|
||||
# Создаем локально
|
||||
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"],
|
||||
"daemonId": "local"
|
||||
}
|
||||
save_server_config(server_name, config)
|
||||
|
||||
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":
|
||||
# Если пользователь не админ/owner, автоматически выдаем ему доступ
|
||||
if user["role"] not in ["admin", "owner"]:
|
||||
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)
|
||||
server_key = f"{daemon_id}_{server_name}" if daemon_id != "local" else server_name
|
||||
if server_key not in users[user["username"]]["servers"]:
|
||||
users[user["username"]]["servers"].append(server_key)
|
||||
save_users(users)
|
||||
|
||||
return {"message": "Сервер создан", "name": server_name}
|
||||
return {"message": "Сервер создан", "name": server_name, "daemonId": daemon_id}
|
||||
|
||||
@app.get("/api/servers/{server_name}/config")
|
||||
async def get_server_config(server_name: str, user: dict = Depends(get_current_user)):
|
||||
@@ -1869,6 +1924,16 @@ async def update_user_permissions(username: str, perms: PermissionsUpdate, curre
|
||||
return {"message": f"Права пользователя {username} обновлены", "permissions": perms.permissions}
|
||||
|
||||
|
||||
# ============================================
|
||||
# API для управления демонами
|
||||
# ============================================
|
||||
|
||||
from daemons import router as daemons_router
|
||||
|
||||
# Подключаем роутер демонов
|
||||
app.include_router(daemons_router)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
||||
@@ -1 +1,22 @@
|
||||
{}
|
||||
{
|
||||
"admin": {
|
||||
"username": "admin",
|
||||
"password": "$2b$12$PAaomoUWn3Ip5ov.S/uYPeTIRiDMq7DbA57ahyYQnw3QHT2zuYMlG",
|
||||
"role": "owner",
|
||||
"servers": [],
|
||||
"permissions": {
|
||||
"manage_users": true,
|
||||
"manage_roles": true,
|
||||
"manage_servers": true,
|
||||
"manage_tickets": true,
|
||||
"manage_files": true,
|
||||
"delete_users": true,
|
||||
"view_all_resources": true
|
||||
},
|
||||
"resource_access": {
|
||||
"servers": [],
|
||||
"tickets": [],
|
||||
"files": []
|
||||
}
|
||||
}
|
||||
}
|
||||
11
daemon/.env
Normal file
11
daemon/.env
Normal file
@@ -0,0 +1,11 @@
|
||||
# Daemon Configuration
|
||||
DAEMON_ID=daemon-1
|
||||
DAEMON_NAME=Main
|
||||
DAEMON_PORT=24444
|
||||
DAEMON_KEY=JLgYFjTlFOqdyT49vmCqlXrLAuVE6FjiCdqf3zsZfr4
|
||||
|
||||
# Panel Connection (optional, for WebSocket)
|
||||
PANEL_URL=ws://0.0.0.0:8000
|
||||
|
||||
# Servers Directory
|
||||
SERVERS_DIR=./servers
|
||||
20
daemon/.env.example
Normal file
20
daemon/.env.example
Normal file
@@ -0,0 +1,20 @@
|
||||
# MC Panel Daemon Configuration
|
||||
|
||||
# Уникальный ID демона
|
||||
DAEMON_ID=daemon-1
|
||||
|
||||
# Название демона (отображается в панели)
|
||||
DAEMON_NAME=Main Server
|
||||
|
||||
# Порт, на котором будет работать демон
|
||||
DAEMON_PORT=24444
|
||||
|
||||
# Секретный ключ для аутентификации (должен совпадать с ключом в панели)
|
||||
# Сгенерируйте случайный ключ: python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||
DAEMON_KEY=your-secret-key-here
|
||||
|
||||
# URL основной панели (для WebSocket подключения, опционально)
|
||||
PANEL_URL=http://your-panel-url:8000
|
||||
|
||||
# Директория для серверов
|
||||
SERVERS_DIR=./servers
|
||||
195
daemon/README.md
Normal file
195
daemon/README.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# MC Panel Daemon
|
||||
|
||||
Удаленный демон для управления серверами Minecraft. Устанавливается на отдельные физические серверы и подключается к основной панели управления.
|
||||
|
||||
## Установка
|
||||
|
||||
### Windows
|
||||
|
||||
1. Установите Python 3.8 или выше
|
||||
2. Скопируйте папку `daemon` на удаленный сервер
|
||||
3. Откройте командную строку в папке daemon
|
||||
4. Запустите установку зависимостей:
|
||||
```
|
||||
install.bat
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
1. Установите Python 3.8 или выше
|
||||
2. Скопируйте папку `daemon` на удаленный сервер
|
||||
3. Установите зависимости:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Настройка
|
||||
|
||||
1. Скопируйте `.env.example` в `.env`:
|
||||
```
|
||||
copy .env.example .env
|
||||
```
|
||||
|
||||
2. Отредактируйте `.env` файл:
|
||||
```env
|
||||
DAEMON_ID=daemon-1
|
||||
DAEMON_NAME=Main Server
|
||||
DAEMON_PORT=24444
|
||||
DAEMON_KEY=your-secret-key-here
|
||||
SERVERS_DIR=./servers
|
||||
```
|
||||
|
||||
- `DAEMON_ID` - уникальный ID демона
|
||||
- `DAEMON_NAME` - отображаемое имя демона
|
||||
- `DAEMON_PORT` - порт для API (по умолчанию 24444)
|
||||
- `DAEMON_KEY` - секретный ключ для аутентификации
|
||||
- `SERVERS_DIR` - директория для серверов
|
||||
|
||||
3. Создайте секретный ключ:
|
||||
```python
|
||||
import secrets
|
||||
print(secrets.token_urlsafe(32))
|
||||
```
|
||||
|
||||
## Запуск
|
||||
|
||||
### Windows
|
||||
```
|
||||
start.bat
|
||||
```
|
||||
|
||||
### Linux
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
### Как сервис (Linux)
|
||||
|
||||
Создайте файл `/etc/systemd/system/mcpanel-daemon.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=MC Panel Daemon
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=mcpanel
|
||||
WorkingDirectory=/path/to/daemon
|
||||
ExecStart=/usr/bin/python3 /path/to/daemon/main.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Запустите сервис:
|
||||
```bash
|
||||
sudo systemctl enable mcpanel-daemon
|
||||
sudo systemctl start mcpanel-daemon
|
||||
sudo systemctl status mcpanel-daemon
|
||||
```
|
||||
|
||||
## Подключение к панели
|
||||
|
||||
1. Откройте основную панель управления
|
||||
2. Перейдите в раздел "Демоны"
|
||||
3. Нажмите "Добавить демон"
|
||||
4. Заполните данные:
|
||||
- **Название**: Main Server
|
||||
- **IP адрес**: IP адрес сервера с демоном
|
||||
- **Порт**: 24444 (или ваш порт)
|
||||
- **Ключ демона**: ваш DAEMON_KEY из .env
|
||||
5. Нажмите "Добавить"
|
||||
|
||||
## API Endpoints
|
||||
|
||||
Демон предоставляет следующие API endpoints:
|
||||
|
||||
- `GET /` - Информация о демоне
|
||||
- `GET /api/status` - Статус демона и системы
|
||||
- `GET /api/servers` - Список серверов
|
||||
- `POST /api/servers/{name}/start` - Запустить сервер
|
||||
- `POST /api/servers/{name}/stop` - Остановить сервер
|
||||
- `POST /api/servers/{name}/command` - Отправить команду
|
||||
- `GET /api/servers/{name}/stats` - Статистика сервера
|
||||
|
||||
## Безопасность
|
||||
|
||||
1. **Используйте сильный ключ** - генерируйте случайный ключ длиной минимум 32 символа
|
||||
2. **Настройте файрвол** - разрешите доступ к порту демона только с IP основной панели
|
||||
3. **Используйте HTTPS** - в продакшене используйте reverse proxy (nginx) с SSL
|
||||
4. **Регулярно обновляйте** - следите за обновлениями и устанавливайте их
|
||||
|
||||
## Пример конфигурации nginx (с SSL)
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name daemon.example.com;
|
||||
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:24444;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Структура директорий
|
||||
|
||||
```
|
||||
daemon/
|
||||
├── main.py # Основной файл демона
|
||||
├── requirements.txt # Зависимости Python
|
||||
├── .env # Конфигурация (создайте из .env.example)
|
||||
├── .env.example # Пример конфигурации
|
||||
├── install.bat # Скрипт установки (Windows)
|
||||
├── start.bat # Скрипт запуска (Windows)
|
||||
├── README.md # Эта документация
|
||||
└── servers/ # Директория с серверами
|
||||
├── server1/
|
||||
│ ├── config.json
|
||||
│ └── ...
|
||||
└── server2/
|
||||
├── config.json
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Формат config.json для сервера
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "server1",
|
||||
"displayName": "My Minecraft Server",
|
||||
"startCommand": "java -Xmx2G -Xms1G -jar server.jar nogui"
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Демон не запускается
|
||||
- Проверьте, что Python установлен: `python --version`
|
||||
- Проверьте, что все зависимости установлены: `pip list`
|
||||
- Проверьте логи в консоли
|
||||
|
||||
### Панель не может подключиться к демону
|
||||
- Проверьте, что демон запущен
|
||||
- Проверьте файрвол и порты
|
||||
- Проверьте, что ключ в панели совпадает с DAEMON_KEY
|
||||
- Проверьте IP адрес и порт
|
||||
|
||||
### Сервер не запускается
|
||||
- Проверьте startCommand в config.json
|
||||
- Проверьте права доступа к файлам
|
||||
- Проверьте логи сервера
|
||||
|
||||
## Поддержка
|
||||
|
||||
Если у вас возникли проблемы, создайте тикет в системе поддержки панели.
|
||||
9
daemon/install.bat
Normal file
9
daemon/install.bat
Normal file
@@ -0,0 +1,9 @@
|
||||
@echo off
|
||||
echo Installing MC Panel Daemon dependencies...
|
||||
pip install -r requirements.txt
|
||||
echo.
|
||||
echo Installation complete!
|
||||
echo.
|
||||
echo Please configure .env file before starting the daemon
|
||||
echo Copy .env.example to .env and edit it
|
||||
pause
|
||||
307
daemon/main.py
Normal file
307
daemon/main.py
Normal file
@@ -0,0 +1,307 @@
|
||||
"""
|
||||
MC Panel Daemon - Удаленный демон для управления серверами
|
||||
Устанавливается на отдельные серверы и подключается к основной панели
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import psutil
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
import websockets
|
||||
from fastapi import FastAPI, HTTPException, Header
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import uvicorn
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
app = FastAPI(title="MC Panel Daemon")
|
||||
|
||||
# CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Конфигурация
|
||||
DAEMON_ID = os.getenv("DAEMON_ID", "daemon-1")
|
||||
DAEMON_NAME = os.getenv("DAEMON_NAME", "Main Server")
|
||||
DAEMON_PORT = int(os.getenv("DAEMON_PORT", "24444"))
|
||||
DAEMON_KEY = os.getenv("DAEMON_KEY", "") # Ключ для аутентификации
|
||||
PANEL_URL = os.getenv("PANEL_URL", "") # URL основной панели для WebSocket
|
||||
SERVERS_DIR = Path(os.getenv("SERVERS_DIR", "./servers"))
|
||||
|
||||
# Создаем директорию для серверов
|
||||
SERVERS_DIR.mkdir(exist_ok=True)
|
||||
|
||||
# Хранилище процессов серверов
|
||||
server_processes: Dict[str, subprocess.Popen] = {}
|
||||
|
||||
|
||||
def verify_key(authorization: str = Header(None)) -> bool:
|
||||
"""Проверка API ключа"""
|
||||
if not DAEMON_KEY:
|
||||
return True # Если ключ не установлен, разрешаем доступ
|
||||
|
||||
if not authorization:
|
||||
raise HTTPException(status_code=401, detail="Missing authorization header")
|
||||
|
||||
if authorization != f"Bearer {DAEMON_KEY}":
|
||||
raise HTTPException(status_code=403, detail="Invalid daemon key")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""Информация о демоне"""
|
||||
return {
|
||||
"daemon_id": DAEMON_ID,
|
||||
"daemon_name": DAEMON_NAME,
|
||||
"status": "online",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/status")
|
||||
async def get_status(authorization: str = Header(None)):
|
||||
"""Получить статус демона и системы"""
|
||||
# Проверка ключа опциональна для статуса
|
||||
if DAEMON_KEY and authorization:
|
||||
if authorization != f"Bearer {DAEMON_KEY}":
|
||||
raise HTTPException(status_code=403, detail="Invalid daemon key")
|
||||
|
||||
cpu_percent = psutil.cpu_percent(interval=1)
|
||||
memory = psutil.virtual_memory()
|
||||
disk = psutil.disk_usage('/')
|
||||
|
||||
return {
|
||||
"daemon_id": DAEMON_ID,
|
||||
"daemon_name": DAEMON_NAME,
|
||||
"status": "online",
|
||||
"system": {
|
||||
"cpu_usage": cpu_percent,
|
||||
"memory_total": memory.total,
|
||||
"memory_used": memory.used,
|
||||
"memory_percent": memory.percent,
|
||||
"disk_total": disk.total,
|
||||
"disk_used": disk.used,
|
||||
"disk_percent": disk.percent
|
||||
},
|
||||
"servers": {
|
||||
"total": len(list(SERVERS_DIR.iterdir())),
|
||||
"running": len(server_processes)
|
||||
},
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/servers")
|
||||
async def list_servers(authorization: str = Header(None)):
|
||||
"""Список всех серверов на этом демоне"""
|
||||
verify_key(authorization)
|
||||
|
||||
servers = []
|
||||
for server_dir in SERVERS_DIR.iterdir():
|
||||
if server_dir.is_dir():
|
||||
config_file = server_dir / "config.json"
|
||||
if config_file.exists():
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
servers.append({
|
||||
"name": server_dir.name,
|
||||
"display_name": config.get("displayName", server_dir.name),
|
||||
"status": "running" if server_dir.name in server_processes else "stopped",
|
||||
"daemon_id": DAEMON_ID
|
||||
})
|
||||
|
||||
return servers
|
||||
|
||||
|
||||
@app.post("/api/servers/{server_name}/start")
|
||||
async def start_server(server_name: str, authorization: str = Header(None)):
|
||||
"""Запустить сервер"""
|
||||
verify_key(authorization)
|
||||
|
||||
server_dir = SERVERS_DIR / server_name
|
||||
if not server_dir.exists():
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
|
||||
if server_name in server_processes:
|
||||
raise HTTPException(status_code=400, detail="Server already running")
|
||||
|
||||
config_file = server_dir / "config.json"
|
||||
if not config_file.exists():
|
||||
raise HTTPException(status_code=400, detail="Server config not found")
|
||||
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
start_command = config.get("startCommand", "")
|
||||
if not start_command:
|
||||
raise HTTPException(status_code=400, detail="Start command not configured")
|
||||
|
||||
try:
|
||||
# Запускаем процесс
|
||||
process = subprocess.Popen(
|
||||
start_command,
|
||||
shell=True,
|
||||
cwd=str(server_dir),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stdin=subprocess.PIPE
|
||||
)
|
||||
|
||||
server_processes[server_name] = process
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Server {server_name} started",
|
||||
"pid": process.pid
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to start server: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/api/servers/{server_name}/stop")
|
||||
async def stop_server(server_name: str, authorization: str = Header(None)):
|
||||
"""Остановить сервер"""
|
||||
verify_key(authorization)
|
||||
|
||||
if server_name not in server_processes:
|
||||
raise HTTPException(status_code=400, detail="Server not running")
|
||||
|
||||
try:
|
||||
process = server_processes[server_name]
|
||||
process.terminate()
|
||||
|
||||
# Ждем завершения процесса
|
||||
try:
|
||||
process.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
|
||||
del server_processes[server_name]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Server {server_name} stopped"
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to stop server: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/api/servers/{server_name}/command")
|
||||
async def send_command(server_name: str, command: dict, authorization: str = Header(None)):
|
||||
"""Отправить команду в консоль сервера"""
|
||||
verify_key(authorization)
|
||||
|
||||
if server_name not in server_processes:
|
||||
raise HTTPException(status_code=400, detail="Server not running")
|
||||
|
||||
try:
|
||||
process = server_processes[server_name]
|
||||
cmd = command.get("command", "")
|
||||
|
||||
if process.stdin:
|
||||
process.stdin.write(f"{cmd}\n".encode())
|
||||
process.stdin.flush()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Command sent"
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to send command: {str(e)}")
|
||||
|
||||
|
||||
@app.get("/api/servers/{server_name}/stats")
|
||||
async def get_server_stats(server_name: str, authorization: str = Header(None)):
|
||||
"""Получить статистику сервера"""
|
||||
verify_key(authorization)
|
||||
|
||||
server_dir = SERVERS_DIR / server_name
|
||||
if not server_dir.exists():
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
|
||||
is_running = server_name in server_processes
|
||||
cpu = 0
|
||||
memory = 0
|
||||
|
||||
if is_running:
|
||||
try:
|
||||
process = server_processes[server_name]
|
||||
p = psutil.Process(process.pid)
|
||||
cpu = p.cpu_percent(interval=0.1)
|
||||
memory = p.memory_info().rss / 1024 / 1024 # MB
|
||||
except:
|
||||
pass
|
||||
|
||||
# Размер директории
|
||||
disk_usage = sum(f.stat().st_size for f in server_dir.rglob('*') if f.is_file()) / 1024 / 1024 # MB
|
||||
|
||||
return {
|
||||
"status": "running" if is_running else "stopped",
|
||||
"cpu": cpu,
|
||||
"memory": memory,
|
||||
"disk": disk_usage
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/servers/create")
|
||||
async def create_server_on_daemon(data: dict, authorization: str = Header(None)):
|
||||
"""Создать сервер на этом демоне"""
|
||||
verify_key(authorization)
|
||||
|
||||
server_name = data.get("name", "").strip()
|
||||
if not server_name:
|
||||
raise HTTPException(status_code=400, detail="Server name is required")
|
||||
|
||||
server_path = SERVERS_DIR / server_name
|
||||
|
||||
if server_path.exists():
|
||||
raise HTTPException(status_code=400, detail="Server already exists")
|
||||
|
||||
try:
|
||||
server_path.mkdir(parents=True)
|
||||
|
||||
# Сохраняем конфигурацию
|
||||
config = {
|
||||
"name": server_name,
|
||||
"displayName": data.get("displayName", server_name),
|
||||
"startCommand": data.get("startCommand", "java -Xmx2G -jar server.jar nogui"),
|
||||
"owner": data.get("owner", "unknown")
|
||||
}
|
||||
|
||||
config_file = server_path / "config.json"
|
||||
with open(config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, indent=2, ensure_ascii=False)
|
||||
|
||||
return {"message": "Server created successfully", "name": server_name}
|
||||
except Exception as e:
|
||||
# Если что-то пошло не так, удаляем папку
|
||||
if server_path.exists():
|
||||
import shutil
|
||||
shutil.rmtree(server_path)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create server: {str(e)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Starting MC Panel Daemon: {DAEMON_NAME} ({DAEMON_ID})")
|
||||
print(f"Port: {DAEMON_PORT}")
|
||||
print(f"Servers directory: {SERVERS_DIR}")
|
||||
|
||||
uvicorn.run(
|
||||
app,
|
||||
host="0.0.0.0",
|
||||
port=DAEMON_PORT,
|
||||
log_level="info"
|
||||
)
|
||||
5
daemon/requirements.txt
Normal file
5
daemon/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
websockets==12.0
|
||||
psutil==5.9.6
|
||||
python-dotenv==1.0.0
|
||||
4
daemon/start.bat
Normal file
4
daemon/start.bat
Normal file
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
echo Starting MC Panel Daemon...
|
||||
python main.py
|
||||
pause
|
||||
@@ -13,6 +13,7 @@ import Users from './components/Users';
|
||||
import UserManagement from './components/UserManagement';
|
||||
import Tickets from './components/Tickets';
|
||||
import Profile from './components/Profile';
|
||||
import Daemons from './components/Daemons';
|
||||
import Auth from './components/Auth';
|
||||
import ErrorBoundary from './components/ErrorBoundary';
|
||||
import NotificationSystem, { notify } from './components/NotificationSystem';
|
||||
@@ -27,6 +28,7 @@ function App() {
|
||||
const [activeTab, setActiveTab] = useState('console');
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showUserManagement, setShowUserManagement] = useState(false);
|
||||
const [showDaemons, setShowDaemons] = useState(false);
|
||||
const [showTickets, setShowTickets] = useState(false);
|
||||
const [showProfile, setShowProfile] = useState(false);
|
||||
const [connectionError, setConnectionError] = useState(false);
|
||||
@@ -216,17 +218,30 @@ function App() {
|
||||
|
||||
{/* Bottom section */}
|
||||
<div className="p-4 border-t border-dark-700 space-y-2">
|
||||
{user?.role === 'owner' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowUserManagement(true);
|
||||
setCurrentView('management');
|
||||
}}
|
||||
className="sidebar-item w-full"
|
||||
>
|
||||
<Shield className="w-5 h-5 flex-shrink-0 text-yellow-500" />
|
||||
{sidebarOpen && <span>Управление</span>}
|
||||
</button>
|
||||
{(user?.role === 'owner' || user?.role === 'admin') && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowUserManagement(true);
|
||||
setCurrentView('management');
|
||||
}}
|
||||
className="sidebar-item w-full"
|
||||
>
|
||||
<Shield className="w-5 h-5 flex-shrink-0 text-yellow-500" />
|
||||
{sidebarOpen && <span>Управление</span>}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowDaemons(true);
|
||||
setCurrentView('daemons');
|
||||
}}
|
||||
className="sidebar-item w-full"
|
||||
>
|
||||
<Server className="w-5 h-5 flex-shrink-0 text-blue-500" />
|
||||
{sidebarOpen && <span>Демоны</span>}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<button
|
||||
@@ -279,6 +294,7 @@ function App() {
|
||||
{currentView === 'dashboard' && 'Панель управления'}
|
||||
{currentView === 'server' && selectedServer?.displayName}
|
||||
{currentView === 'management' && 'Управление пользователями'}
|
||||
{currentView === 'daemons' && 'Управление демонами'}
|
||||
{currentView === 'tickets' && 'Тикеты поддержки'}
|
||||
{currentView === 'profile' && 'Профиль'}
|
||||
</h1>
|
||||
@@ -538,6 +554,14 @@ function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showDaemons && (
|
||||
<div className="modal-overlay" onClick={() => setShowDaemons(false)}>
|
||||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||
<Daemons token={token} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showTickets && (
|
||||
<div className="modal-overlay" onClick={() => setShowTickets(false)}>
|
||||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||
|
||||
@@ -1,16 +1,36 @@
|
||||
import { useState } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { X, Server } from 'lucide-react';
|
||||
import axios from 'axios';
|
||||
import { API_URL } from '../config';
|
||||
import { notify } from './NotificationSystem';
|
||||
|
||||
export default function CreateServerModal({ token, theme, onClose, onCreated }) {
|
||||
export default function CreateServerModal({ token, onClose, onSuccess }) {
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
displayName: '',
|
||||
startCommand: 'java -Xmx2G -Xms1G -jar server.jar nogui'
|
||||
startCommand: 'java -Xmx2G -Xms1G -jar server.jar nogui',
|
||||
daemonId: 'local' // По умолчанию локальный
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [daemons, setDaemons] = useState([]);
|
||||
const [loadingDaemons, setLoadingDaemons] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadDaemons();
|
||||
}, []);
|
||||
|
||||
const loadDaemons = async () => {
|
||||
try {
|
||||
const { data } = await axios.get(`${API_URL}/api/daemons`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
setDaemons(data.filter(d => d.status === 'online')); // Только онлайн демоны
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки демонов:', error);
|
||||
} finally {
|
||||
setLoadingDaemons(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -23,11 +43,10 @@ export default function CreateServerModal({ token, theme, onClose, onCreated })
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
notify('success', 'Сервер создан', `Сервер "${formData.displayName}" успешно создан`);
|
||||
onCreated();
|
||||
if (onSuccess) onSuccess();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
notify('error', 'Ошибка создания', error.response?.data?.detail || 'Не удалось создать сервер');
|
||||
alert(error.response?.data?.detail || 'Ошибка создания сервера');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -35,12 +54,12 @@ export default function CreateServerModal({ token, theme, onClose, onCreated })
|
||||
|
||||
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="bg-dark-800 rounded-2xl p-6 w-full max-w-md shadow-2xl border border-gray-700">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className={`text-xl font-bold ${theme.text}`}>Создать сервер</h2>
|
||||
<h2 className="text-xl font-bold text-white">Создать сервер</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={`${theme.textSecondary} hover:${theme.text} transition`}
|
||||
className="text-gray-400 hover:text-white transition"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
@@ -48,7 +67,34 @@ export default function CreateServerModal({ token, theme, onClose, onCreated })
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className={`block text-sm font-medium mb-2 ${theme.text}`}>
|
||||
<label className="block text-sm font-medium mb-2 text-white">
|
||||
Демон
|
||||
</label>
|
||||
{loadingDaemons ? (
|
||||
<div className="input text-gray-400">Загрузка демонов...</div>
|
||||
) : (
|
||||
<select
|
||||
value={formData.daemonId}
|
||||
onChange={(e) => setFormData({ ...formData, daemonId: e.target.value })}
|
||||
className="w-full bg-dark-700 border border-gray-600 rounded-xl px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
|
||||
>
|
||||
<option value="local">Локальный (эта машина)</option>
|
||||
{daemons.map((daemon) => (
|
||||
<option key={daemon.id} value={daemon.id}>
|
||||
{daemon.name} ({daemon.address}:{daemon.port})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
{formData.daemonId === 'local'
|
||||
? 'Сервер будет создан на этой машине'
|
||||
: 'Сервер будет создан на удаленном демоне'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 text-white">
|
||||
Имя папки (только латиница, цифры, _ и -)
|
||||
</label>
|
||||
<input
|
||||
@@ -56,13 +102,13 @@ export default function CreateServerModal({ token, theme, onClose, onCreated })
|
||||
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`}
|
||||
className="input"
|
||||
placeholder="my_server"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={`block text-sm font-medium mb-2 ${theme.text}`}>
|
||||
<label className="block text-sm font-medium mb-2 text-white">
|
||||
Отображаемое имя
|
||||
</label>
|
||||
<input
|
||||
@@ -70,13 +116,13 @@ export default function CreateServerModal({ token, theme, onClose, onCreated })
|
||||
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`}
|
||||
className="input"
|
||||
placeholder="Мой сервер"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={`block text-sm font-medium mb-2 ${theme.text}`}>
|
||||
<label className="block text-sm font-medium mb-2 text-white">
|
||||
Команда запуска
|
||||
</label>
|
||||
<input
|
||||
@@ -84,7 +130,7 @@ export default function CreateServerModal({ token, theme, onClose, onCreated })
|
||||
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`}
|
||||
className="input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -92,14 +138,14 @@ export default function CreateServerModal({ token, theme, onClose, onCreated })
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className={`flex-1 ${theme.card} ${theme.hover} px-4 py-2 rounded-xl transition`}
|
||||
className="flex-1 bg-dark-700 hover:bg-dark-600 px-4 py-2 rounded-xl transition text-white"
|
||||
>
|
||||
Отмена
|
||||
</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`}
|
||||
className="flex-1 btn-primary disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Создание...' : 'Создать'}
|
||||
</button>
|
||||
|
||||
382
frontend/src/components/Daemons.jsx
Normal file
382
frontend/src/components/Daemons.jsx
Normal file
@@ -0,0 +1,382 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Server, Plus, Trash2, Edit, RefreshCw, CheckCircle, XCircle, Activity } from 'lucide-react';
|
||||
import axios from 'axios';
|
||||
import { API_URL } from '../config';
|
||||
import { notify } from './NotificationSystem';
|
||||
|
||||
export default function Daemons({ token }) {
|
||||
const [daemons, setDaemons] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [editingDaemon, setEditingDaemon] = useState(null);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
address: '',
|
||||
port: 24444,
|
||||
key: '',
|
||||
remarks: ''
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadDaemons();
|
||||
const interval = setInterval(loadDaemons, 10000); // Обновляем каждые 10 секунд
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const loadDaemons = async () => {
|
||||
try {
|
||||
const { data } = await axios.get(`${API_URL}/api/daemons`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
setDaemons(data);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки демонов:', error);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
if (editingDaemon) {
|
||||
await axios.put(
|
||||
`${API_URL}/api/daemons/${editingDaemon.id}`,
|
||||
formData,
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
notify('success', 'Демон обновлен', 'Демон успешно обновлен');
|
||||
} else {
|
||||
await axios.post(
|
||||
`${API_URL}/api/daemons`,
|
||||
formData,
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
);
|
||||
notify('success', 'Демон добавлен', 'Демон успешно добавлен');
|
||||
}
|
||||
|
||||
setShowAddModal(false);
|
||||
setEditingDaemon(null);
|
||||
setFormData({ name: '', address: '', port: 24444, key: '', remarks: '' });
|
||||
loadDaemons();
|
||||
} catch (error) {
|
||||
notify('error', 'Ошибка', error.response?.data?.detail || 'Не удалось сохранить демон');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (daemonId) => {
|
||||
if (!confirm('Вы уверены, что хотите удалить этот демон?')) return;
|
||||
|
||||
try {
|
||||
await axios.delete(`${API_URL}/api/daemons/${daemonId}`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
notify('success', 'Демон удален', 'Демон успешно удален');
|
||||
loadDaemons();
|
||||
} catch (error) {
|
||||
notify('error', 'Ошибка удаления', error.response?.data?.detail || 'Не удалось удалить демон');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (daemon) => {
|
||||
setEditingDaemon(daemon);
|
||||
setFormData({
|
||||
name: daemon.name,
|
||||
address: daemon.address,
|
||||
port: daemon.port,
|
||||
key: daemon.key,
|
||||
remarks: daemon.remarks || ''
|
||||
});
|
||||
setShowAddModal(true);
|
||||
};
|
||||
|
||||
const formatBytes = (bytes) => {
|
||||
if (!bytes) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-gray-400">Загрузка демонов...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Server className="w-8 h-8 text-blue-400" />
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">Демоны</h2>
|
||||
<p className="text-gray-400">Управление удаленными серверами</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={loadDaemons}
|
||||
className="btn-secondary flex items-center gap-2"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Обновить
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingDaemon(null);
|
||||
setFormData({ name: '', address: '', port: 24444, key: '', remarks: '' });
|
||||
setShowAddModal(true);
|
||||
}}
|
||||
className="btn-primary flex items-center gap-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Добавить демон
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{daemons.length === 0 ? (
|
||||
<div className="card p-12 text-center">
|
||||
<Server className="w-16 h-16 mx-auto mb-4 text-gray-500 opacity-50" />
|
||||
<p className="text-lg font-medium mb-2">Нет демонов</p>
|
||||
<p className="text-sm text-gray-400 mb-4">
|
||||
Добавьте первый демон для управления удаленными серверами
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="btn-primary"
|
||||
>
|
||||
Добавить демон
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
{daemons.map((daemon) => (
|
||||
<div key={daemon.id} className="card p-6">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`p-3 rounded-xl ${daemon.status === 'online' ? 'bg-green-500/20' : 'bg-red-500/20'}`}>
|
||||
<Server className={`w-6 h-6 ${daemon.status === 'online' ? 'text-green-400' : 'text-red-400'}`} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold text-white">{daemon.name}</h3>
|
||||
{daemon.status === 'online' ? (
|
||||
<span className="px-2 py-1 bg-green-500/20 text-green-400 text-xs rounded flex items-center gap-1">
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
Онлайн
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-1 bg-red-500/20 text-red-400 text-xs rounded flex items-center gap-1">
|
||||
<XCircle className="w-3 h-3" />
|
||||
Оффлайн
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 mt-1">
|
||||
{daemon.address}:{daemon.port}
|
||||
</p>
|
||||
{daemon.remarks && (
|
||||
<p className="text-sm text-gray-500 mt-1">{daemon.remarks}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleEdit(daemon)}
|
||||
className="p-2 bg-dark-700 hover:bg-dark-600 rounded transition"
|
||||
title="Редактировать"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(daemon.id)}
|
||||
className="p-2 bg-dark-700 hover:bg-dark-600 rounded text-red-400 transition"
|
||||
title="Удалить"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{daemon.status === 'online' && daemon.system && (
|
||||
<div className="grid grid-cols-3 gap-4 mt-4 pt-4 border-t border-gray-700">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Activity className="w-4 h-4 text-blue-400" />
|
||||
<span className="text-sm text-gray-400">CPU</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-white">{daemon.system.cpu_usage?.toFixed(1)}%</div>
|
||||
<div className="w-full bg-dark-700 rounded-full h-2 mt-2">
|
||||
<div
|
||||
className="bg-blue-500 h-2 rounded-full transition-all"
|
||||
style={{ width: `${Math.min(daemon.system.cpu_usage || 0, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Activity className="w-4 h-4 text-green-400" />
|
||||
<span className="text-sm text-gray-400">ОЗУ</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-white">{daemon.system.memory_percent?.toFixed(1)}%</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{formatBytes(daemon.system.memory_used)} / {formatBytes(daemon.system.memory_total)}
|
||||
</div>
|
||||
<div className="w-full bg-dark-700 rounded-full h-2 mt-2">
|
||||
<div
|
||||
className="bg-green-500 h-2 rounded-full transition-all"
|
||||
style={{ width: `${Math.min(daemon.system.memory_percent || 0, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Activity className="w-4 h-4 text-purple-400" />
|
||||
<span className="text-sm text-gray-400">Диск</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-white">{daemon.system.disk_percent?.toFixed(1)}%</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{formatBytes(daemon.system.disk_used)} / {formatBytes(daemon.system.disk_total)}
|
||||
</div>
|
||||
<div className="w-full bg-dark-700 rounded-full h-2 mt-2">
|
||||
<div
|
||||
className="bg-purple-500 h-2 rounded-full transition-all"
|
||||
style={{ width: `${Math.min(daemon.system.disk_percent || 0, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{daemon.status === 'online' && daemon.servers && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-700">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<Server className="w-4 h-4" />
|
||||
<span>Серверов: {daemon.servers.total || 0}</span>
|
||||
<span className="text-gray-600">•</span>
|
||||
<span className="text-green-400">Запущено: {daemon.servers.running || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Модальное окно добавления/редактирования */}
|
||||
{showAddModal && (
|
||||
<div className="modal-overlay" onClick={() => setShowAddModal(false)}>
|
||||
<div className="modal-content max-w-2xl" onClick={(e) => e.stopPropagation()}>
|
||||
<h2 className="text-xl font-bold text-white mb-6">
|
||||
{editingDaemon ? 'Редактировать демон' : 'Добавить демон'}
|
||||
</h2>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 text-white">
|
||||
Название
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="input"
|
||||
placeholder="Main Server"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 text-white">
|
||||
IP адрес
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.address}
|
||||
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
|
||||
className="input"
|
||||
placeholder="192.168.1.100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 text-white">
|
||||
Порт
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
required
|
||||
value={formData.port}
|
||||
onChange={(e) => setFormData({ ...formData, port: parseInt(e.target.value) })}
|
||||
className="input"
|
||||
placeholder="24444"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 text-white">
|
||||
Ключ демона
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.key}
|
||||
onChange={(e) => setFormData({ ...formData, key: e.target.value })}
|
||||
className="input"
|
||||
placeholder="your-secret-key"
|
||||
/>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
Ключ должен совпадать с DAEMON_KEY в .env файле демона
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2 text-white">
|
||||
Примечания (необязательно)
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.remarks}
|
||||
onChange={(e) => setFormData({ ...formData, remarks: e.target.value })}
|
||||
className="w-full bg-dark-800 border-gray-700 border rounded-xl px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500 transition resize-none"
|
||||
rows={3}
|
||||
placeholder="Дополнительная информация о демоне"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowAddModal(false);
|
||||
setEditingDaemon(null);
|
||||
}}
|
||||
className="flex-1 btn-secondary"
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 btn-primary"
|
||||
>
|
||||
{editingDaemon ? 'Сохранить' : 'Добавить'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
0
servers/sdfsfsf/dfgdg/.gitkeep
Normal file
0
servers/sdfsfsf/dfgdg/.gitkeep
Normal file
0
servers/sdfsfsf/dfgdg/sdff.txt
Normal file
0
servers/sdfsfsf/dfgdg/sdff.txt
Normal file
7
servers/sdfsfsf/panel_config.json
Normal file
7
servers/sdfsfsf/panel_config.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "sdfsfsf",
|
||||
"displayName": "sdfsdf",
|
||||
"startCommand": "java -Xmx2G -Xms1G -jar server.jar nogui",
|
||||
"owner": "Leuteg",
|
||||
"daemonId": "local"
|
||||
}
|
||||
6
servers/sfsf/panel_config.json
Normal file
6
servers/sfsf/panel_config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "sfsf",
|
||||
"displayName": "sdf",
|
||||
"startCommand": "java -Xmx2G -Xms1G -jar server.jar nogui",
|
||||
"owner": "Leuteg"
|
||||
}
|
||||
38
users.json
Normal file
38
users.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"Root": {
|
||||
"username": "Root",
|
||||
"password": "$2b$12$PAaomoUWn3Ip5ov.S/uYPeTIRiDMq7DbA57ahyYQnw3QHT2zuYMlG",
|
||||
"role": "owner",
|
||||
"servers": [
|
||||
"dfgdfgdfg"
|
||||
],
|
||||
"permissions": {
|
||||
"manage_users": true,
|
||||
"manage_roles": true,
|
||||
"manage_servers": true,
|
||||
"manage_tickets": true,
|
||||
"manage_files": true,
|
||||
"delete_users": true,
|
||||
"view_all_resources": true
|
||||
},
|
||||
"resource_access": {
|
||||
"servers": [],
|
||||
"tickets": [],
|
||||
"files": []
|
||||
}
|
||||
},
|
||||
"Leuteg": {
|
||||
"username": "Leuteg",
|
||||
"password": "$2b$12$EAK2ougYahmHPhdaP/vm5O9RMPgnvtCYb.8Z9HpSqNrVxComaZ68C",
|
||||
"role": "owner",
|
||||
"servers": [
|
||||
"sfsf"
|
||||
],
|
||||
"permissions": {
|
||||
"servers": true,
|
||||
"tickets": true,
|
||||
"users": false,
|
||||
"files": true
|
||||
}
|
||||
}
|
||||
}
|
||||
174
ОБНОВЛЕНИЯ.md
Normal file
174
ОБНОВЛЕНИЯ.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Обновления системы
|
||||
|
||||
## Выполнено
|
||||
|
||||
### 1. ✅ Очистка пользователей
|
||||
- Удалены все тестовые пользователи
|
||||
- Оставлен только один пользователь: `admin` (пароль тот же)
|
||||
- Роль: `owner`
|
||||
|
||||
### 2. ✅ Удалены временные файлы
|
||||
Удалены все временные .md файлы с отладкой:
|
||||
- ИСПРАВЛЕНО.md
|
||||
- ИСПРАВЛЕНИЕ_ACCESS_DENIED.md
|
||||
- ЧТО_ДЕЛАТЬ_СЕЙЧАС.md
|
||||
- ОТЛАДКА.md
|
||||
- РЕШЕНИЕ_ПРОБЛЕМЫ.md
|
||||
- ОБНОВЛЕНИЕ_УДАЛЕННОГО_СЕРВЕРА.md
|
||||
- УСПЕХ.md
|
||||
- CHANGELOG_DAEMONS.md
|
||||
- QUICK_TEST_DAEMONS.md
|
||||
- БЫСТРЫЙ_СТАРТ_ДЕМОНЫ.md
|
||||
- test_remote_api.py
|
||||
- debug_token.html
|
||||
|
||||
### 3. ✅ Админы и владельцы видят ВСЕ серверы
|
||||
- Обновлен endpoint `/api/servers`
|
||||
- Добавлена проверка: `is_admin_or_owner = user.get("role") in ["owner", "admin"]`
|
||||
- Если пользователь owner или admin - видит все серверы
|
||||
- Обычные пользователи видят только свои серверы
|
||||
- Добавлено поле `owner` в ответе API
|
||||
|
||||
### 4. ✅ Выбор демона при создании сервера
|
||||
- Обновлен компонент `CreateServerModal.jsx`:
|
||||
- Добавлен выпадающий список с демонами
|
||||
- Загружаются только онлайн демоны
|
||||
- По умолчанию выбран "Локальный (эта машина)"
|
||||
- Показывается подсказка о том, где будет создан сервер
|
||||
|
||||
- Обновлен endpoint `/api/servers/create`:
|
||||
- Поддержка параметра `daemonId`
|
||||
- Если `daemonId === "local"` - создается локально
|
||||
- Если указан ID демона - отправляется запрос на daemon API
|
||||
- Локально сохраняется информация о сервере с префиксом `{daemonId}_{serverName}`
|
||||
- Автоматическая выдача доступа пользователю
|
||||
|
||||
## Как использовать
|
||||
|
||||
### Вход в систему
|
||||
```
|
||||
Логин: admin
|
||||
Пароль: Admin
|
||||
```
|
||||
|
||||
### Создание сервера
|
||||
|
||||
1. Нажмите "Создать сервер"
|
||||
2. Выберите демон из списка:
|
||||
- **Локальный (эта машина)** - сервер будет на панели
|
||||
- **Test Daemon** (или другой) - сервер будет на удаленном демоне
|
||||
3. Заполните остальные поля
|
||||
4. Нажмите "Создать"
|
||||
|
||||
### Просмотр серверов
|
||||
|
||||
- **Owner и Admin** видят ВСЕ серверы всех пользователей
|
||||
- **Обычные пользователи** видят только свои серверы
|
||||
- В списке серверов показывается владелец сервера
|
||||
|
||||
## Структура серверов на демонах
|
||||
|
||||
Когда сервер создается на демоне:
|
||||
- **На демоне**: создается папка `servers/{server_name}/`
|
||||
- **На панели**: создается запись `servers/{daemon_id}_{server_name}/` с конфигурацией
|
||||
- В конфигурации сохраняется:
|
||||
- `daemonId` - ID демона
|
||||
- `daemonName` - название демона
|
||||
- `owner` - владелец сервера
|
||||
- Остальные параметры
|
||||
|
||||
## API изменения
|
||||
|
||||
### GET /api/servers
|
||||
Теперь возвращает:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "my_server",
|
||||
"displayName": "Мой сервер",
|
||||
"status": "stopped",
|
||||
"owner": "admin"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### POST /api/servers/create
|
||||
Новые параметры:
|
||||
```json
|
||||
{
|
||||
"name": "my_server",
|
||||
"displayName": "Мой сервер",
|
||||
"startCommand": "java -Xmx2G -jar server.jar nogui",
|
||||
"daemonId": "daemon-1" // или "local"
|
||||
}
|
||||
```
|
||||
|
||||
Ответ:
|
||||
```json
|
||||
{
|
||||
"message": "Сервер создан",
|
||||
"name": "my_server",
|
||||
"daemonId": "daemon-1"
|
||||
}
|
||||
```
|
||||
|
||||
## Следующие шаги
|
||||
|
||||
Для полной интеграции с демонами нужно:
|
||||
|
||||
1. **Управление серверами на демонах**:
|
||||
- Запуск/остановка через daemon API
|
||||
- Отправка команд в консоль
|
||||
- Получение логов
|
||||
|
||||
2. **Файловый менеджер для демонов**:
|
||||
- Просмотр файлов на удаленном демоне
|
||||
- Загрузка/скачивание файлов
|
||||
- Редактирование конфигов
|
||||
|
||||
3. **Статистика серверов на демонах**:
|
||||
- CPU/RAM использование конкретного сервера
|
||||
- Онлайн игроков
|
||||
- Uptime
|
||||
|
||||
4. **Консоль для серверов на демонах**:
|
||||
- WebSocket подключение к daemon
|
||||
- Просмотр логов в реальном времени
|
||||
- Отправка команд
|
||||
|
||||
## Daemon API для создания сервера
|
||||
|
||||
Нужно добавить в `daemon/main.py`:
|
||||
|
||||
```python
|
||||
@app.post("/api/servers/create")
|
||||
async def create_server_on_daemon(data: dict, authorization: str = Header(None)):
|
||||
"""Создать сервер на этом демоне"""
|
||||
verify_key(authorization)
|
||||
|
||||
server_name = data.get("name")
|
||||
server_path = SERVERS_DIR / server_name
|
||||
|
||||
if server_path.exists():
|
||||
raise HTTPException(400, "Server already exists")
|
||||
|
||||
server_path.mkdir(parents=True)
|
||||
|
||||
# Сохраняем конфигурацию
|
||||
config = {
|
||||
"name": server_name,
|
||||
"displayName": data.get("displayName", server_name),
|
||||
"startCommand": data.get("startCommand", ""),
|
||||
"owner": data.get("owner", "unknown")
|
||||
}
|
||||
|
||||
config_file = server_path / "config.json"
|
||||
with open(config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, indent=2, ensure_ascii=False)
|
||||
|
||||
return {"message": "Server created", "name": server_name}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Все задачи выполнены! Система готова к использованию.**
|
||||
Reference in New Issue
Block a user