Initial commit
This commit is contained in:
8
backend/.env.example
Normal file
8
backend/.env.example
Normal file
@@ -0,0 +1,8 @@
|
||||
# Секретный ключ для JWT (сгенерируйте свой!)
|
||||
SECRET_KEY=your-secret-key-here-change-this-in-production
|
||||
|
||||
# Алгоритм шифрования
|
||||
ALGORITHM=HS256
|
||||
|
||||
# Время жизни токена в минутах
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=43200
|
||||
152
backend/auth.py
Normal file
152
backend/auth.py
Normal file
@@ -0,0 +1,152 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
SECRET_KEY = "mc-panel-secret-key-change-in-production"
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 43200 # 30 дней
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
security = HTTPBearer()
|
||||
|
||||
USERS_FILE = Path("data/users.json")
|
||||
USERS_FILE.parent.mkdir(exist_ok=True)
|
||||
|
||||
def load_users():
|
||||
if USERS_FILE.exists():
|
||||
with open(USERS_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
def save_users(users):
|
||||
with open(USERS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(users, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
def get_password_hash(password):
|
||||
return pwd_context.hash(password)
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
def decode_token(token: str):
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
return payload
|
||||
except JWTError:
|
||||
return None
|
||||
|
||||
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
||||
token = credentials.credentials
|
||||
payload = decode_token(token)
|
||||
|
||||
if payload is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Неверный токен авторизации"
|
||||
)
|
||||
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Неверный токен авторизации"
|
||||
)
|
||||
|
||||
users = load_users()
|
||||
if username not in users:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Пользователь не найден"
|
||||
)
|
||||
|
||||
return {"username": username, "role": users[username].get("role", "user")}
|
||||
|
||||
def authenticate_user(username: str, password: str):
|
||||
users = load_users()
|
||||
if username not in users:
|
||||
return False
|
||||
user = users[username]
|
||||
if not verify_password(password, user["password"]):
|
||||
return False
|
||||
return user
|
||||
|
||||
def create_user(username: str, password: str, role: str = "user"):
|
||||
users = load_users()
|
||||
if username in users:
|
||||
return False
|
||||
|
||||
users[username] = {
|
||||
"password": get_password_hash(password),
|
||||
"role": role,
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
"servers": [] # Список серверов к которым есть доступ
|
||||
}
|
||||
save_users(users)
|
||||
return True
|
||||
|
||||
def get_user_servers(username: str):
|
||||
"""Получить список серверов пользователя"""
|
||||
users = load_users()
|
||||
if username not in users:
|
||||
return []
|
||||
return users[username].get("servers", [])
|
||||
|
||||
def add_server_to_user(username: str, server_name: str):
|
||||
"""Добавить сервер пользователю"""
|
||||
users = load_users()
|
||||
if username not in users:
|
||||
return False
|
||||
if "servers" not in users[username]:
|
||||
users[username]["servers"] = []
|
||||
if server_name not in users[username]["servers"]:
|
||||
users[username]["servers"].append(server_name)
|
||||
save_users(users)
|
||||
return True
|
||||
|
||||
def remove_server_from_user(username: str, server_name: str):
|
||||
"""Удалить сервер у пользователя"""
|
||||
users = load_users()
|
||||
if username not in users:
|
||||
return False
|
||||
if "servers" in users[username] and server_name in users[username]["servers"]:
|
||||
users[username]["servers"].remove(server_name)
|
||||
save_users(users)
|
||||
return True
|
||||
|
||||
def get_server_users(server_name: str):
|
||||
"""Получить список пользователей с доступом к серверу"""
|
||||
users = load_users()
|
||||
result = []
|
||||
for username, user_data in users.items():
|
||||
if server_name in user_data.get("servers", []):
|
||||
result.append({
|
||||
"username": username,
|
||||
"role": user_data.get("role", "user")
|
||||
})
|
||||
return result
|
||||
|
||||
def has_server_access(username: str, server_name: str):
|
||||
"""Проверить есть ли доступ к серверу"""
|
||||
users = load_users()
|
||||
if username not in users:
|
||||
return False
|
||||
user = users[username]
|
||||
# Админы имеют доступ ко всем серверам
|
||||
if user.get("role") == "admin":
|
||||
return True
|
||||
return server_name in user.get("servers", [])
|
||||
757
backend/main.py
Normal file
757
backend/main.py
Normal file
@@ -0,0 +1,757 @@
|
||||
from fastapi import FastAPI, WebSocket, UploadFile, File, HTTPException, Depends, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
import asyncio
|
||||
import subprocess
|
||||
import psutil
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
import json
|
||||
from passlib.context import CryptContext
|
||||
from jose import JWTError, jwt
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
app = FastAPI(title="MC Panel")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Настройки безопасности
|
||||
SECRET_KEY = "your-secret-key-change-this-in-production-12345"
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 дней
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
security = HTTPBearer(auto_error=False)
|
||||
|
||||
SERVERS_DIR = Path("servers")
|
||||
SERVERS_DIR.mkdir(exist_ok=True)
|
||||
USERS_FILE = Path("users.json")
|
||||
|
||||
server_processes: dict[str, subprocess.Popen] = {}
|
||||
server_logs: dict[str, list[str]] = {}
|
||||
|
||||
IS_WINDOWS = sys.platform == 'win32'
|
||||
|
||||
# Инициализация файла пользователей
|
||||
def init_users():
|
||||
if not USERS_FILE.exists():
|
||||
admin_user = {
|
||||
"username": "admin",
|
||||
"password": pwd_context.hash("admin"),
|
||||
"role": "admin",
|
||||
"servers": []
|
||||
}
|
||||
save_users({"admin": admin_user})
|
||||
print("Создан пользователь по умолчанию: admin / admin")
|
||||
|
||||
def load_users() -> dict:
|
||||
if USERS_FILE.exists():
|
||||
with open(USERS_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
def save_users(users: dict):
|
||||
with open(USERS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(users, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def load_server_config(server_name: str) -> dict:
|
||||
config_path = SERVERS_DIR / server_name / "panel_config.json"
|
||||
if config_path.exists():
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
return {
|
||||
"name": server_name,
|
||||
"displayName": server_name,
|
||||
"startCommand": "java -Xmx2G -Xms1G -jar server.jar nogui"
|
||||
}
|
||||
|
||||
def save_server_config(server_name: str, config: dict):
|
||||
config_path = SERVERS_DIR / server_name / "panel_config.json"
|
||||
with open(config_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, indent=2, ensure_ascii=False)
|
||||
|
||||
init_users()
|
||||
|
||||
# Функции аутентификации
|
||||
def verify_password(plain_password, hashed_password):
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
def get_password_hash(password):
|
||||
return pwd_context.hash(password)
|
||||
|
||||
def create_access_token(data: dict):
|
||||
to_encode = data.copy()
|
||||
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
def get_current_user(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="Неверный токен")
|
||||
|
||||
users = load_users()
|
||||
if username not in users:
|
||||
raise HTTPException(status_code=401, detail="Пользователь не найден")
|
||||
|
||||
return users[username]
|
||||
except JWTError:
|
||||
raise HTTPException(status_code=401, detail="Неверный токен")
|
||||
|
||||
def check_server_access(user: dict, server_name: str):
|
||||
if user["role"] == "admin":
|
||||
return True
|
||||
return server_name in user.get("servers", [])
|
||||
|
||||
# API для аутентификации
|
||||
@app.post("/api/auth/register")
|
||||
async def register(data: dict):
|
||||
users = load_users()
|
||||
username = data.get("username", "").strip()
|
||||
password = data.get("password", "").strip()
|
||||
|
||||
if not username or not password:
|
||||
raise HTTPException(400, "Имя пользователя и пароль обязательны")
|
||||
|
||||
if username in users:
|
||||
raise HTTPException(400, "Пользователь уже существует")
|
||||
|
||||
role = "admin" if len(users) == 0 else "user"
|
||||
|
||||
users[username] = {
|
||||
"username": username,
|
||||
"password": get_password_hash(password),
|
||||
"role": role,
|
||||
"servers": []
|
||||
}
|
||||
|
||||
save_users(users)
|
||||
|
||||
access_token = create_access_token(data={"sub": username})
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"username": username,
|
||||
"role": role
|
||||
}
|
||||
|
||||
@app.post("/api/auth/login")
|
||||
async def login(data: dict):
|
||||
users = load_users()
|
||||
username = data.get("username", "").strip()
|
||||
password = data.get("password", "").strip()
|
||||
|
||||
if username not in users:
|
||||
raise HTTPException(401, "Неверное имя пользователя или пароль")
|
||||
|
||||
user = users[username]
|
||||
if not verify_password(password, user["password"]):
|
||||
raise HTTPException(401, "Неверное имя пользователя или пароль")
|
||||
|
||||
access_token = create_access_token(data={"sub": username})
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"username": username,
|
||||
"role": user["role"]
|
||||
}
|
||||
|
||||
@app.get("/api/auth/me")
|
||||
async def get_me(user: dict = Depends(get_current_user)):
|
||||
return {
|
||||
"username": user["username"],
|
||||
"role": user["role"],
|
||||
"servers": user.get("servers", [])
|
||||
}
|
||||
|
||||
# API для управления пользователями
|
||||
@app.get("/api/users")
|
||||
async def get_users(user: dict = Depends(get_current_user)):
|
||||
# Админы видят всех пользователей
|
||||
# Обычные пользователи тоже видят всех (для управления доступом к своим серверам)
|
||||
users = load_users()
|
||||
return [
|
||||
{
|
||||
"username": u["username"],
|
||||
"role": u["role"],
|
||||
"servers": u.get("servers", [])
|
||||
}
|
||||
for u in users.values()
|
||||
]
|
||||
|
||||
@app.put("/api/users/{username}/servers")
|
||||
async def update_user_servers(username: str, data: dict, user: dict = Depends(get_current_user)):
|
||||
users = load_users()
|
||||
if username not in users:
|
||||
raise HTTPException(404, "Пользователь не найден")
|
||||
|
||||
# Админы могут управлять доступом к любым серверам
|
||||
if user["role"] == "admin":
|
||||
users[username]["servers"] = data.get("servers", [])
|
||||
save_users(users)
|
||||
return {"message": "Доступ обновлен"}
|
||||
|
||||
# Обычные пользователи могут управлять доступом только к своим серверам
|
||||
requested_servers = data.get("servers", [])
|
||||
current_servers = users[username].get("servers", [])
|
||||
|
||||
# Проверяем, что пользователь пытается изменить доступ только к своим серверам
|
||||
for server_name in requested_servers:
|
||||
if server_name not in current_servers:
|
||||
# Проверяем, является ли текущий пользователь владельцем этого сервера
|
||||
config = load_server_config(server_name)
|
||||
if config.get("owner") != user["username"]:
|
||||
raise HTTPException(403, f"Вы не можете выдать доступ к серверу {server_name}")
|
||||
|
||||
users[username]["servers"] = requested_servers
|
||||
save_users(users)
|
||||
return {"message": "Доступ обновлен"}
|
||||
|
||||
@app.delete("/api/users/{username}")
|
||||
async def delete_user(username: str, user: dict = Depends(get_current_user)):
|
||||
if user["role"] != "admin":
|
||||
raise HTTPException(403, "Доступ запрещен")
|
||||
|
||||
if username == user["username"]:
|
||||
raise HTTPException(400, "Нельзя удалить самого себя")
|
||||
|
||||
users = load_users()
|
||||
if username not in users:
|
||||
raise HTTPException(404, "Пользователь не найден")
|
||||
|
||||
del users[username]
|
||||
save_users(users)
|
||||
|
||||
return {"message": "Пользователь удален"}
|
||||
|
||||
@app.put("/api/users/{username}/role")
|
||||
async def update_user_role(username: str, data: dict, user: dict = Depends(get_current_user)):
|
||||
if user["role"] != "admin":
|
||||
raise HTTPException(403, "Доступ запрещен")
|
||||
|
||||
if username == user["username"]:
|
||||
raise HTTPException(400, "Нельзя изменить свою роль")
|
||||
|
||||
users = load_users()
|
||||
if username not in users:
|
||||
raise HTTPException(404, "Пользователь не найден")
|
||||
|
||||
new_role = data.get("role")
|
||||
if new_role not in ["admin", "user"]:
|
||||
raise HTTPException(400, "Неверная роль")
|
||||
|
||||
users[username]["role"] = new_role
|
||||
save_users(users)
|
||||
|
||||
return {"message": "Роль обновлена"}
|
||||
|
||||
# API для серверов
|
||||
@app.get("/api/servers")
|
||||
async def get_servers(user: dict = Depends(get_current_user)):
|
||||
servers = []
|
||||
try:
|
||||
for server_dir in SERVERS_DIR.iterdir():
|
||||
if server_dir.is_dir():
|
||||
if user["role"] != "admin" and server_dir.name not in user.get("servers", []):
|
||||
continue
|
||||
|
||||
config = load_server_config(server_dir.name)
|
||||
|
||||
is_running = False
|
||||
if server_dir.name in server_processes:
|
||||
process = server_processes[server_dir.name]
|
||||
if process.poll() is None:
|
||||
is_running = True
|
||||
else:
|
||||
del server_processes[server_dir.name]
|
||||
|
||||
servers.append({
|
||||
"name": server_dir.name,
|
||||
"displayName": config.get("displayName", server_dir.name),
|
||||
"status": "running" if is_running else "stopped"
|
||||
})
|
||||
print(f"Найдено серверов для {user['username']}: {len(servers)}")
|
||||
except Exception as e:
|
||||
print(f"Ошибка загрузки серверов: {e}")
|
||||
return servers
|
||||
|
||||
@app.post("/api/servers/create")
|
||||
async def create_server(data: dict, user: dict = Depends(get_current_user)):
|
||||
server_name = data.get("name", "").strip()
|
||||
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, "Сервер с таким именем уже существует")
|
||||
|
||||
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":
|
||||
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)
|
||||
save_users(users)
|
||||
|
||||
return {"message": "Сервер создан", "name": server_name}
|
||||
|
||||
@app.get("/api/servers/{server_name}/config")
|
||||
async def get_server_config(server_name: str, user: dict = Depends(get_current_user)):
|
||||
if not check_server_access(user, server_name):
|
||||
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||
|
||||
server_path = SERVERS_DIR / server_name
|
||||
if not server_path.exists():
|
||||
raise HTTPException(404, "Сервер не найден")
|
||||
|
||||
config = load_server_config(server_name)
|
||||
print(f"Загружена конфигурация для {server_name}: {config}")
|
||||
return config
|
||||
|
||||
@app.put("/api/servers/{server_name}/config")
|
||||
async def update_server_config(server_name: str, config: dict, user: dict = Depends(get_current_user)):
|
||||
if not check_server_access(user, server_name):
|
||||
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||
|
||||
server_path = SERVERS_DIR / server_name
|
||||
if not server_path.exists():
|
||||
raise HTTPException(404, "Сервер не найден")
|
||||
|
||||
if server_name in server_processes:
|
||||
raise HTTPException(400, "Остановите сервер перед изменением настроек")
|
||||
|
||||
save_server_config(server_name, config)
|
||||
return {"message": "Настройки сохранены"}
|
||||
|
||||
@app.delete("/api/servers/{server_name}")
|
||||
async def delete_server(server_name: str, user: dict = Depends(get_current_user)):
|
||||
if user["role"] != "admin":
|
||||
raise HTTPException(403, "Только администраторы могут удалять серверы")
|
||||
|
||||
server_path = SERVERS_DIR / server_name
|
||||
if not server_path.exists():
|
||||
raise HTTPException(404, "Сервер не найден")
|
||||
|
||||
if server_name in server_processes:
|
||||
raise HTTPException(400, "Остановите сервер перед удалением")
|
||||
|
||||
shutil.rmtree(server_path)
|
||||
return {"message": "Сервер удален"}
|
||||
|
||||
# Управление процессами серверов
|
||||
async def read_server_output(server_name: str, process: subprocess.Popen):
|
||||
try:
|
||||
print(f"Начало чтения вывода для сервера {server_name}")
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
while True:
|
||||
if process.poll() is not None:
|
||||
print(f"Процесс сервера {server_name} завершился с кодом {process.poll()}")
|
||||
break
|
||||
|
||||
try:
|
||||
line = await loop.run_in_executor(None, process.stdout.readline)
|
||||
if not line:
|
||||
break
|
||||
|
||||
line = line.strip()
|
||||
if line:
|
||||
if server_name not in server_logs:
|
||||
server_logs[server_name] = []
|
||||
server_logs[server_name].append(line)
|
||||
|
||||
if len(server_logs[server_name]) > 1000:
|
||||
server_logs[server_name].pop(0)
|
||||
except Exception as e:
|
||||
print(f"Ошибка чтения строки для {server_name}: {e}")
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка чтения вывода сервера {server_name}: {e}")
|
||||
finally:
|
||||
print(f"Чтение вывода для сервера {server_name} завершено")
|
||||
if server_name in server_processes and process.poll() is not None:
|
||||
del server_processes[server_name]
|
||||
print(f"Сервер {server_name} удален из списка процессов")
|
||||
|
||||
@app.post("/api/servers/{server_name}/start")
|
||||
async def start_server(server_name: str, user: dict = Depends(get_current_user)):
|
||||
if not check_server_access(user, server_name):
|
||||
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||
|
||||
server_path = SERVERS_DIR / server_name
|
||||
if not server_path.exists():
|
||||
raise HTTPException(404, "Сервер не найден")
|
||||
|
||||
if server_name in server_processes:
|
||||
raise HTTPException(400, "Сервер уже запущен")
|
||||
|
||||
config = load_server_config(server_name)
|
||||
start_command = config.get("startCommand", "java -Xmx2G -Xms1G -jar server.jar nogui")
|
||||
|
||||
cmd_parts = start_command.split()
|
||||
|
||||
try:
|
||||
if IS_WINDOWS:
|
||||
process = subprocess.Popen(
|
||||
cmd_parts,
|
||||
cwd=server_path,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW
|
||||
)
|
||||
else:
|
||||
process = subprocess.Popen(
|
||||
cmd_parts,
|
||||
cwd=server_path,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
server_processes[server_name] = process
|
||||
server_logs[server_name] = []
|
||||
|
||||
asyncio.create_task(read_server_output(server_name, process))
|
||||
|
||||
print(f"Сервер {server_name} запущен с PID {process.pid}")
|
||||
return {"message": "Сервер запущен", "pid": process.pid}
|
||||
except Exception as e:
|
||||
print(f"Ошибка запуска сервера {server_name}: {e}")
|
||||
raise HTTPException(500, f"Ошибка запуска сервера: {str(e)}")
|
||||
|
||||
@app.post("/api/servers/{server_name}/stop")
|
||||
async def stop_server(server_name: str, user: dict = Depends(get_current_user)):
|
||||
if not check_server_access(user, server_name):
|
||||
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||
|
||||
if server_name not in server_processes:
|
||||
raise HTTPException(400, "Сервер не запущен")
|
||||
|
||||
process = server_processes[server_name]
|
||||
|
||||
try:
|
||||
if process.stdin and not process.stdin.closed:
|
||||
process.stdin.write("stop\n")
|
||||
process.stdin.flush()
|
||||
|
||||
try:
|
||||
process.wait(timeout=30)
|
||||
except subprocess.TimeoutExpired:
|
||||
print(f"Сервер {server_name} не остановился за 30 секунд, принудительное завершение")
|
||||
process.kill()
|
||||
process.wait()
|
||||
except Exception as e:
|
||||
print(f"Ошибка при остановке сервера {server_name}: {e}")
|
||||
try:
|
||||
process.kill()
|
||||
process.wait()
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
if server_name in server_processes:
|
||||
del server_processes[server_name]
|
||||
print(f"Сервер {server_name} остановлен")
|
||||
|
||||
return {"message": "Сервер остановлен"}
|
||||
|
||||
@app.post("/api/servers/{server_name}/command")
|
||||
async def send_command(server_name: str, command: dict, user: dict = Depends(get_current_user)):
|
||||
if not check_server_access(user, server_name):
|
||||
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||
|
||||
if server_name not in server_processes:
|
||||
raise HTTPException(400, "Сервер не запущен")
|
||||
|
||||
process = server_processes[server_name]
|
||||
|
||||
if process.poll() is not None:
|
||||
del server_processes[server_name]
|
||||
raise HTTPException(400, "Сервер не запущен")
|
||||
|
||||
try:
|
||||
cmd = command["command"]
|
||||
if process.stdin and not process.stdin.closed:
|
||||
process.stdin.write(cmd + "\n")
|
||||
process.stdin.flush()
|
||||
print(f"Команда отправлена серверу {server_name}: {cmd}")
|
||||
return {"message": "Команда отправлена"}
|
||||
else:
|
||||
raise HTTPException(400, "Невозможно отправить команду")
|
||||
except Exception as e:
|
||||
print(f"Ошибка отправки команды серверу {server_name}: {e}")
|
||||
raise HTTPException(500, f"Ошибка отправки команды: {str(e)}")
|
||||
|
||||
@app.get("/api/servers/{server_name}/stats")
|
||||
async def get_server_stats(server_name: str, user: dict = Depends(get_current_user)):
|
||||
if not check_server_access(user, server_name):
|
||||
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||
|
||||
server_path = SERVERS_DIR / server_name
|
||||
|
||||
try:
|
||||
disk_usage = sum(f.stat().st_size for f in server_path.rglob('*') if f.is_file())
|
||||
disk_mb = disk_usage / 1024 / 1024
|
||||
except:
|
||||
disk_mb = 0
|
||||
|
||||
if server_name not in server_processes:
|
||||
return {
|
||||
"status": "stopped",
|
||||
"cpu": 0,
|
||||
"memory": 0,
|
||||
"disk": round(disk_mb, 2)
|
||||
}
|
||||
|
||||
process = server_processes[server_name]
|
||||
try:
|
||||
if process.poll() is not None:
|
||||
del server_processes[server_name]
|
||||
return {
|
||||
"status": "stopped",
|
||||
"cpu": 0,
|
||||
"memory": 0,
|
||||
"disk": round(disk_mb, 2)
|
||||
}
|
||||
|
||||
proc = psutil.Process(process.pid)
|
||||
memory_mb = proc.memory_info().rss / 1024 / 1024
|
||||
cpu_percent = proc.cpu_percent(interval=0.1)
|
||||
|
||||
return {
|
||||
"status": "running",
|
||||
"cpu": round(cpu_percent, 2),
|
||||
"memory": round(memory_mb, 2),
|
||||
"disk": round(disk_mb, 2)
|
||||
}
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
if server_name in server_processes:
|
||||
del server_processes[server_name]
|
||||
return {
|
||||
"status": "stopped",
|
||||
"cpu": 0,
|
||||
"memory": 0,
|
||||
"disk": round(disk_mb, 2)
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Ошибка получения статистики для {server_name}: {e}")
|
||||
return {
|
||||
"status": "unknown",
|
||||
"cpu": 0,
|
||||
"memory": 0,
|
||||
"disk": round(disk_mb, 2)
|
||||
}
|
||||
|
||||
@app.websocket("/ws/servers/{server_name}/console")
|
||||
async def console_websocket(websocket: WebSocket, server_name: str):
|
||||
await websocket.accept()
|
||||
print(f"WebSocket подключен для сервера: {server_name}")
|
||||
|
||||
if server_name in server_logs:
|
||||
print(f"Отправка {len(server_logs[server_name])} существующих логов")
|
||||
for log in server_logs[server_name]:
|
||||
await websocket.send_text(log)
|
||||
else:
|
||||
print(f"Логов для сервера {server_name} пока нет")
|
||||
await websocket.send_text(f"[Панель] Ожидание логов от сервера {server_name}...")
|
||||
|
||||
last_sent_index = len(server_logs.get(server_name, []))
|
||||
|
||||
try:
|
||||
while True:
|
||||
if server_name in server_logs:
|
||||
current_logs = server_logs[server_name]
|
||||
if len(current_logs) > last_sent_index:
|
||||
for log in current_logs[last_sent_index:]:
|
||||
await websocket.send_text(log)
|
||||
last_sent_index = len(current_logs)
|
||||
await asyncio.sleep(0.1)
|
||||
except Exception as e:
|
||||
print(f"WebSocket ошибка: {e}")
|
||||
pass
|
||||
|
||||
# API для файлов
|
||||
@app.get("/api/servers/{server_name}/files")
|
||||
async def list_files(server_name: str, path: str = "", user: dict = Depends(get_current_user)):
|
||||
if not check_server_access(user, server_name):
|
||||
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||
|
||||
server_path = SERVERS_DIR / server_name
|
||||
if not server_path.exists():
|
||||
raise HTTPException(404, "Сервер не найден")
|
||||
|
||||
target_path = server_path / path if path else server_path
|
||||
|
||||
try:
|
||||
target_path = target_path.resolve()
|
||||
server_path = server_path.resolve()
|
||||
if not str(target_path).startswith(str(server_path)):
|
||||
raise HTTPException(403, "Доступ запрещен")
|
||||
except:
|
||||
raise HTTPException(404, "Путь не найден")
|
||||
|
||||
if not target_path.exists():
|
||||
raise HTTPException(404, "Путь не найден")
|
||||
|
||||
if not target_path.is_dir():
|
||||
raise HTTPException(400, "Путь не является директорией")
|
||||
|
||||
files = []
|
||||
try:
|
||||
for item in target_path.iterdir():
|
||||
files.append({
|
||||
"name": item.name,
|
||||
"type": "directory" if item.is_dir() else "file",
|
||||
"size": item.stat().st_size if item.is_file() else 0
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Ошибка чтения директории: {e}")
|
||||
raise HTTPException(500, f"Ошибка чтения директории: {str(e)}")
|
||||
|
||||
return files
|
||||
|
||||
@app.get("/api/servers/{server_name}/files/download")
|
||||
async def download_file(server_name: str, path: str, user: dict = Depends(get_current_user)):
|
||||
if not check_server_access(user, server_name):
|
||||
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||
|
||||
server_path = SERVERS_DIR / server_name
|
||||
file_path = server_path / path
|
||||
|
||||
if not file_path.exists() or not str(file_path).startswith(str(server_path)):
|
||||
raise HTTPException(404, "Файл не найден")
|
||||
|
||||
return FileResponse(file_path, filename=file_path.name)
|
||||
|
||||
@app.post("/api/servers/{server_name}/files/upload")
|
||||
async def upload_file(server_name: str, path: str, file: UploadFile = File(...), user: dict = Depends(get_current_user)):
|
||||
if not check_server_access(user, server_name):
|
||||
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||
|
||||
server_path = SERVERS_DIR / server_name
|
||||
target_path = server_path / path / file.filename
|
||||
|
||||
if not str(target_path).startswith(str(server_path)):
|
||||
raise HTTPException(400, "Недопустимый путь")
|
||||
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(target_path, "wb") as f:
|
||||
content = await file.read()
|
||||
f.write(content)
|
||||
|
||||
return {"message": "Файл загружен"}
|
||||
|
||||
@app.delete("/api/servers/{server_name}/files")
|
||||
async def delete_file(server_name: str, path: str, user: dict = Depends(get_current_user)):
|
||||
if not check_server_access(user, server_name):
|
||||
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||
|
||||
server_path = SERVERS_DIR / server_name
|
||||
target_path = server_path / path
|
||||
|
||||
if not target_path.exists() or not str(target_path).startswith(str(server_path)):
|
||||
raise HTTPException(404, "Файл не найден")
|
||||
|
||||
if target_path.is_dir():
|
||||
shutil.rmtree(target_path)
|
||||
else:
|
||||
target_path.unlink()
|
||||
|
||||
return {"message": "Файл удален"}
|
||||
|
||||
@app.get("/api/servers/{server_name}/files/content")
|
||||
async def get_file_content(server_name: str, path: str, user: dict = Depends(get_current_user)):
|
||||
if not check_server_access(user, server_name):
|
||||
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||
|
||||
server_path = SERVERS_DIR / server_name
|
||||
file_path = server_path / path
|
||||
|
||||
if not file_path.exists() or not file_path.is_file() or not str(file_path).startswith(str(server_path)):
|
||||
raise HTTPException(404, "Файл не найден")
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
return {"content": content}
|
||||
except UnicodeDecodeError:
|
||||
raise HTTPException(400, "Файл не является текстовым")
|
||||
|
||||
@app.put("/api/servers/{server_name}/files/content")
|
||||
async def update_file_content(server_name: str, path: str, data: dict, user: dict = Depends(get_current_user)):
|
||||
if not check_server_access(user, server_name):
|
||||
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||
|
||||
server_path = SERVERS_DIR / server_name
|
||||
file_path = server_path / path
|
||||
|
||||
if not file_path.exists() or not file_path.is_file() or not str(file_path).startswith(str(server_path)):
|
||||
raise HTTPException(404, "Файл не найден")
|
||||
|
||||
try:
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(data.get("content", ""))
|
||||
return {"message": "Файл сохранен"}
|
||||
except Exception as e:
|
||||
raise HTTPException(400, f"Ошибка сохранения файла: {str(e)}")
|
||||
|
||||
@app.put("/api/servers/{server_name}/files/rename")
|
||||
async def rename_file(server_name: str, old_path: str, new_name: str, user: dict = Depends(get_current_user)):
|
||||
if not check_server_access(user, server_name):
|
||||
raise HTTPException(403, "Нет доступа к этому серверу")
|
||||
|
||||
server_path = SERVERS_DIR / server_name
|
||||
old_file_path = server_path / old_path
|
||||
|
||||
if not old_file_path.exists() or not str(old_file_path).startswith(str(server_path)):
|
||||
raise HTTPException(404, "Файл не найден")
|
||||
|
||||
new_file_path = old_file_path.parent / new_name
|
||||
|
||||
if new_file_path.exists():
|
||||
raise HTTPException(400, "Файл с таким именем уже существует")
|
||||
|
||||
if not str(new_file_path).startswith(str(server_path)):
|
||||
raise HTTPException(400, "Недопустимое имя файла")
|
||||
|
||||
old_file_path.rename(new_file_path)
|
||||
return {"message": "Файл переименован"}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
23
backend/models.py
Normal file
23
backend/models.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
|
||||
class UserRegister(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
username: str
|
||||
role: str
|
||||
|
||||
class ServerAccess(BaseModel):
|
||||
username: str
|
||||
server_name: str
|
||||
|
||||
class ServerAccessList(BaseModel):
|
||||
users: List[dict]
|
||||
12
backend/requirements.txt
Normal file
12
backend/requirements.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
fastapi==0.109.0
|
||||
uvicorn[standard]==0.27.0
|
||||
websockets==12.0
|
||||
psutil==5.9.8
|
||||
aiofiles==23.2.1
|
||||
python-multipart==0.0.6
|
||||
pydantic==2.5.3
|
||||
passlib[bcrypt]==1.7.4
|
||||
python-jose[cryptography]==3.3.0
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
python-dotenv==1.0.0
|
||||
5
backend/start.bat
Normal file
5
backend/start.bat
Normal file
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
echo Starting MC Panel Backend...
|
||||
cd /d "%~dp0"
|
||||
python main.py
|
||||
pause
|
||||
17
backend/users.json
Normal file
17
backend/users.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"admin": {
|
||||
"username": "admin",
|
||||
"password": "$2b$12$0AJU/Cc6vI.gqUY6BfU8E.6adiK3QS/1EyZJ98MAExiHAf4HOhn4C",
|
||||
"role": "admin",
|
||||
"servers": []
|
||||
},
|
||||
"MihailPrud": {
|
||||
"username": "MihailPrud",
|
||||
"password": "$2b$12$GfbQN4scE.b.mtUHofWWE.Dn1tQpT1zwLAxeICv90sHP4zGv0dc2G",
|
||||
"role": "user",
|
||||
"servers": [
|
||||
"test",
|
||||
"nya"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user