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

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,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)

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": []
}
}
}