Added Daemon system and fixed interface
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-01-16 18:56:21 +06:00
parent fbfddf3c7a
commit d188cec1f0
24 changed files with 1974 additions and 4985 deletions

264
DAEMON_SETUP.md Normal file
View 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
View 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
View File

@@ -0,0 +1,10 @@
{
"daemon-1": {
"name": "Test",
"address": "127.0.0.1",
"port": 24444,
"key": "JLgYFjTlFOqdyT49vmCqlXrLAuVE6FjiCdqf3zsZfr4",
"remarks": "",
"created_at": "D:\\Desktop\\adadad"
}
}

View File

@@ -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,9 +874,57 @@ 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, "Недопустимое имя сервера")
# Если создаем на демоне
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, "Сервер с таким именем уже существует")
@@ -882,21 +935,23 @@ async def create_server(data: dict, user: dict = Depends(get_current_user)):
"name": server_name,
"displayName": data.get("displayName", server_name),
"startCommand": data.get("startCommand", "java -Xmx2G -Xms1G -jar server.jar nogui"),
"owner": user["username"] # Сохраняем владельца
"owner": user["username"],
"daemonId": "local"
}
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)

View File

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

@@ -0,0 +1,4 @@
@echo off
echo Starting MC Panel Daemon...
python main.py
pause

View File

@@ -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,7 +218,8 @@ function App() {
{/* Bottom section */}
<div className="p-4 border-t border-dark-700 space-y-2">
{user?.role === 'owner' && (
{(user?.role === 'owner' || user?.role === 'admin') && (
<>
<button
onClick={() => {
setShowUserManagement(true);
@@ -227,6 +230,18 @@ function App() {
<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()}>

View File

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

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

2
key.py Normal file
View File

@@ -0,0 +1,2 @@
import secrets
print(secrets.token_urlsafe(32))

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

View File

View File

@@ -0,0 +1,7 @@
{
"name": "sdfsfsf",
"displayName": "sdfsdf",
"startCommand": "java -Xmx2G -Xms1G -jar server.jar nogui",
"owner": "Leuteg",
"daemonId": "local"
}

View File

@@ -0,0 +1,6 @@
{
"name": "sfsf",
"displayName": "sdf",
"startCommand": "java -Xmx2G -Xms1G -jar server.jar nogui",
"owner": "Leuteg"
}

38
users.json Normal file
View 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
View 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}
```
---
**Все задачи выполнены! Система готова к использованию.**