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:
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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user