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)}")