Merge branch 'dev' into refactor/backups

This commit is contained in:
amcmanu3 2024-03-08 16:23:45 -05:00
commit 0c517868b4
65 changed files with 1202 additions and 554 deletions

View File

@ -0,0 +1,15 @@
#!/bin/bash
# Prompt the user for the directory path
read -p "Enter the directory path to set permissions (/var/opt/minecraft/crafty): " directory_path
# Check if the script is running within a Docker container
if [ -f "/.dockerenv" ]; then
echo "Script is running within a Docker container. Exiting with error."
exit 1 # Exit with an error code if running in Docker
else
echo "Script is not running within a Docker container. Executing permissions changes..."
# Run the commands to set permissions
sudo chmod 700 $(find "$directory_path" -type d)
sudo chmod 644 $(find "$directory_path" -type f)
fi

View File

@ -1,13 +1,26 @@
# Changelog
## --- [4.2.4] - 2023/TBD
### New features
TBD
## --- [4.3.0] - 2023/03/09
### Breaking Changes
- This release includes database migrations that are not revertable. Once you update to this version you will not be able to rollback to a previous version.
- In this release, we've implemented a breaking change to enhance server identification within Crafty: instead of relying on numerical integers (1, 2, 3, etc.), Servers are now uniquely identified by their UUIDs. Please adapt your API clients accordingly.
### Refactor
- Refactor remote file downloads ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/719))
### Bug fixes
TBD
- Fix Bedrock cert issues ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/719))
- Make sure default.json is read from correct location ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/714))
- Do not allow users at server limit to clone servers ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/718))
- Fix bug where you cannot get to config with unloaded server ([Commit](https://gitlab.com/crafty-controller/crafty-4/-/commit/9de08973b6bb2ddf91283c5c6b0e189ff34f7e24))
- Fix forge install v1.20, 1.20.1 and 1.20.2 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/710))
- Fix Sanitisation on Passwords ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/715) | [Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/725))
- Fix `Upload Imports` on unix systems, that have a space in the root dir name ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/722))
- Fix Bedrock downloads, add `www` to download URL ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/723))
- Fire backup webhook 'after' backup has finished ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/727))
### Tweaks
TBD
- Bump pyOpenSSL & cryptography for CVE-2024-0727, CVE-2023-50782 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/716))
- Bump cryptography for CVE-2024-26130 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/724))
### Lang
TBD
- Update `de_DE, en_EN, es_ES, fr_FR, he_IL, lol_EN, lv_LV, nl_BE pl_PL, th_TH, tr_TR, uk_UA, zh_CN` translations for `4.3.0` ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/715))
<br><br>
## --- [4.2.3] - 2023/02/02

View File

@ -1,5 +1,5 @@
[![Crafty Logo](app/frontend/static/assets/images/logo_long.svg)](https://craftycontrol.com)
# Crafty Controller 4.2.4
# Crafty Controller 4.3.0
> Python based Control Panel for your Minecraft Server
## What is Crafty Controller?

View File

@ -79,8 +79,8 @@ class ServersController(metaclass=Singleton):
PeeweeException: If the server already exists
"""
return HelperServers.create_server(
name,
server_uuid,
name,
server_dir,
server_command,
server_file,
@ -159,9 +159,9 @@ class ServersController(metaclass=Singleton):
# Servers Methods
# **********************************************************************************
def get_server_instance_by_id(self, server_id: t.Union[str, int]) -> ServerInstance:
def get_server_instance_by_id(self, server_id: t.Union[str, str]) -> ServerInstance:
for server in self.servers_list:
if int(server["server_id"]) == int(server_id):
if server["server_id"] == server_id:
return server["server_obj"]
logger.warning(f"Unable to find server object for server id {server_id}")

View File

@ -52,7 +52,7 @@ class UsersController:
},
"password": {
"type": "string",
"minLength": 8,
"minLength": self.helper.minimum_password_length,
"examples": ["crafty"],
"title": "Password",
},

View File

@ -1,13 +1,14 @@
import os
import json
import threading
import time
import shutil
import logging
from datetime import datetime
import requests
from app.classes.controllers.servers_controller import ServersController
from app.classes.models.server_permissions import PermissionsServers
from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.websocket_manager import WebSocketManager
logger = logging.getLogger(__name__)
@ -24,6 +25,113 @@ class ServerJars:
def get_paper_jars():
return PAPERJARS
def get_paper_versions(self, project):
"""
Retrieves a list of versions for a specified project from the PaperMC API.
Parameters:
project (str): The project name to query for available versions.
Returns:
list: A list of version strings available for the project. Returns an empty
list if the API call fails or if no versions are found.
This function makes a GET request to the PaperMC API to fetch available project
versions, The versions are returned in reverse order, with the most recent
version first.
"""
try:
response = requests.get(
f"{self.paper_base}/v2/projects/{project}/", timeout=2
)
response.raise_for_status()
api_data = response.json()
except Exception as e:
logger.error(f"Error loading project versions for {project}: {e}")
return []
versions = api_data.get("versions", [])
versions.reverse() # Ensure the most recent version comes first
return versions
def get_paper_build(self, project, version):
"""
Fetches the latest build for a specified project and version from PaperMC API.
Parameters:
project (str): Project name, typically a server software like 'paper'.
version (str): Project version to fetch the build number for.
Returns:
int or None: Latest build number if successful, None if not or on error.
This method attempts to query the PaperMC API for the latest build and
handles exceptions by logging errors and returning None.
"""
try:
response = requests.get(
f"{self.paper_base}/v2/projects/{project}/versions/{version}/builds/",
timeout=2,
)
response.raise_for_status()
api_data = response.json()
except Exception as e:
logger.error(f"Error fetching build for {project} {version}: {e}")
return None
builds = api_data.get("builds", [])
return builds[-1] if builds else None
def get_fetch_url(self, jar, server, version):
"""
Constructs the URL for downloading a server JAR file based on the server type.
Supports two main types of server JAR sources:
- ServerJars API for servers not in PAPERJARS.
- Paper API for servers available through the Paper project.
Parameters:
jar (str): Name of the JAR file.
server (str): Server software name (e.g., "paper").
version (str): Server version.
Returns:
str or None: URL for downloading the JAR file, or None if URL cannot be
constructed or an error occurs.
"""
try:
# Check if the server type is not specifically handled by Paper.
if server not in PAPERJARS:
return f"{self.base_url}/api/fetchJar/{jar}/{server}/{version}"
# For Paper servers, attempt to get the build for the specified version.
paper_build_info = self.get_paper_build(server, version)
if paper_build_info is None:
# Log an error or handle the case where paper_build_info is None
logger.error(
"Error: Unable to get build information for server:"
f" {server}, version: {version}"
)
return None
build = paper_build_info.get("build")
if not build:
# Log an error or handle the case where build is None or not found
logger.error(
f"Error: Build number not found for server:"
f" {server}, version: {version}"
)
return None
# Construct and return the URL for downloading the Paper server JAR.
return (
f"{self.paper_base}/v2/projects/{server}/versions/{version}/"
f"builds/{build}/downloads/{server}-{version}-{build}.jar"
)
except Exception as e:
logger.error(f"An error occurred while constructing fetch URL: {e}")
return None
def _get_api_result(self, call_url: str):
full_url = f"{self.base_url}{call_url}"
@ -44,40 +152,6 @@ class ServerJars:
return api_response
def get_paper_versions(self, project):
try:
response = requests.get(
f"{self.paper_base}/v2/projects/{project}/", timeout=2
)
response.raise_for_status()
api_data = json.loads(response.content)
except Exception as e:
logger.error(
f"Unable to load https://api.papermc.io/v2/projects/{project}/"
f"api due to error: {e}"
)
return {}
versions = api_data.get("versions", [])
versions.reverse()
return versions
def get_paper_build(self, project, version):
try:
response = requests.get(
f"{self.paper_base}/v2/projects/{project}/versions/{version}/builds/",
timeout=2,
)
response.raise_for_status()
api_data = json.loads(response.content)
except Exception as e:
logger.error(
f"Unable to load https://api.papermc.io/v2/projects/{project}/"
f"api due to error: {e}"
)
return {}
build = api_data.get("builds", [])[-1]
return build
def _read_cache(self):
cache_file = self.helper.serverjar_cache
cache = {}
@ -213,55 +287,75 @@ class ServerJars:
update_thread.start()
def a_download_jar(self, jar, server, version, path, server_id):
"""
Downloads a server JAR file and performs post-download actions including
notifying users and setting import status.
This method waits for the server registration to complete, retrieves the
download URL for the specified server JAR file.
Upon successful download, it either runs the installer for
Forge servers or simply finishes the import process for other types. It
notifies server users about the completion of the download.
Parameters:
- jar (str): The name of the JAR file to download.
- server (str): The type of server software (e.g., 'forge', 'paper').
- version (str): The version of the server software.
- path (str): The local filesystem path where the JAR file will be saved.
- server_id (str): The unique identifier for the server being updated or
imported, used for notifying users and setting the import status.
Returns:
- bool: True if the JAR file was successfully downloaded and saved;
False otherwise.
The method ensures that the server is properly registered before proceeding
with the download and handles exceptions by logging errors and reverting
the import status if necessary.
"""
# delaying download for server register to finish
time.sleep(3)
if server not in PAPERJARS:
fetch_url = f"{self.base_url}/api/fetchJar/{jar}/{server}/{version}"
else:
build = self.get_paper_build(server, version).get("build", None)
if not build:
return
fetch_url = (
f"{self.paper_base}/v2/projects"
f"/{server}/versions/{version}/builds/{build}/downloads/"
f"{server}-{version}-{build}.jar"
)
fetch_url = self.get_fetch_url(jar, server, version)
if not fetch_url:
return False
server_users = PermissionsServers.get_server_user_list(server_id)
# We need to make sure the server is registered before
# we submit a db update for it's stats.
# Make sure the server is registered before updating its stats
while True:
try:
ServersController.set_import(server_id)
for user in server_users:
WebSocketManager().broadcast_user(user, "send_start_reload", {})
break
except Exception as ex:
logger.debug(f"server not registered yet. Delaying download - {ex}")
logger.debug(f"Server not registered yet. Delaying download - {ex}")
# open a file stream
with requests.get(fetch_url, timeout=2, stream=True) as r:
success = False
try:
with open(path, "wb") as output:
shutil.copyfileobj(r.raw, output)
# If this is the newer forge version we will run the installer
if server == "forge":
ServersController.finish_import(server_id, True)
else:
ServersController.finish_import(server_id)
# Initiate Download
jar_dir = os.path.dirname(path)
jar_name = os.path.basename(path)
logger.info(fetch_url)
success = FileHelpers.ssl_get_file(fetch_url, jar_dir, jar_name)
success = True
except Exception as e:
logger.error(f"Unable to save jar to {path} due to error:{e}")
# Post-download actions
if success:
if server == "forge":
# If this is the newer Forge version, run the installer
ServersController.finish_import(server_id, True)
else:
ServersController.finish_import(server_id)
server_users = PermissionsServers.get_server_user_list(server_id)
# Notify users
for user in server_users:
WebSocketManager().broadcast_user(
user, "notification", "Executable download finished"
)
time.sleep(3)
time.sleep(3) # Delay for user notification
WebSocketManager().broadcast_user(user, "send_start_reload", {})
return success
else:
logger.error(f"Unable to save jar to {path} due to download failure.")
ServersController.finish_import(server_id)
return success

View File

@ -33,9 +33,9 @@ class AuditLog(BaseModel):
user_name = CharField(default="")
user_id = IntegerField(default=0, index=True)
source_ip = CharField(default="127.0.0.1")
server_id = IntegerField(
default=None, index=True
) # When auditing global events, use server ID 0
server_id = ForeignKeyField(
Servers, backref="audit_server", null=True
) # When auditing global events, use server ID null
log_msg = TextField(default="")
class Meta:
@ -81,7 +81,7 @@ class HostStats(BaseModel):
# **********************************************************************************
class Webhooks(BaseModel):
id = AutoField()
server_id = IntegerField(null=True)
server_id = ForeignKeyField(Servers, backref="webhook_server", null=True)
name = CharField(default="Custom Webhook", max_length=64)
url = CharField(default="")
webhook_type = CharField(default="Custom")
@ -342,7 +342,7 @@ class HelpersManagement:
@staticmethod
def delete_scheduled_task_by_server(server_id):
Schedules.delete().where(Schedules.server_id == int(server_id)).execute()
Schedules.delete().where(Schedules.server_id == server_id).execute()
@staticmethod
def get_scheduled_task(schedule_id):

View File

@ -172,9 +172,9 @@ class PermissionsServers:
RoleServers.server_id, RoleServers.permissions
).where(RoleServers.role_id == role_id)
for role_server in role_servers:
permissions_dict[
role_server.server_id_id
] = PermissionsServers.get_permissions(role_server.permissions)
permissions_dict[role_server.server_id_id] = (
PermissionsServers.get_permissions(role_server.permissions)
)
return permissions_dict
@staticmethod

View File

@ -71,7 +71,7 @@ class HelperServerStats:
database = None
def __init__(self, server_id):
self.server_id = int(server_id)
self.server_id = server_id
self.init_database(self.server_id)
def init_database(self, server_id):

View File

@ -3,7 +3,6 @@ import datetime
import typing as t
from peewee import (
CharField,
AutoField,
DateTimeField,
BooleanField,
IntegerField,
@ -13,6 +12,9 @@ from playhouse.shortcuts import model_to_dict
from app.classes.shared.main_models import DatabaseShortcuts
from app.classes.models.base_model import BaseModel
# from app.classes.models.users import Users
from app.classes.shared.helpers import Helpers
logger = logging.getLogger(__name__)
@ -20,9 +22,8 @@ logger = logging.getLogger(__name__)
# Servers Model
# **********************************************************************************
class Servers(BaseModel):
server_id = AutoField()
server_id = CharField(primary_key=True, default=Helpers.create_uuid())
created = DateTimeField(default=datetime.datetime.now)
server_uuid = CharField(default="", index=True)
server_name = CharField(default="Server", index=True)
path = CharField(default="")
backup_path = CharField(default="")
@ -40,6 +41,7 @@ class Servers(BaseModel):
type = CharField(default="minecraft-java")
show_status = BooleanField(default=1)
created_by = IntegerField(default=-100)
# created_by = ForeignKeyField(Users, backref="creator_server", null=True)
shutdown_timeout = IntegerField(default=60)
ignored_exits = CharField(default="0")
count_players = BooleanField(default=True)
@ -60,8 +62,8 @@ class HelperServers:
# **********************************************************************************
@staticmethod
def create_server(
server_id: str,
name: str,
server_uuid: str,
server_dir: str,
backup_path: str,
server_command: str,
@ -95,25 +97,24 @@ class HelperServers:
Raises:
PeeweeException: If the server already exists
"""
return Servers.insert(
{
Servers.server_name: name,
Servers.server_uuid: server_uuid,
Servers.path: server_dir,
Servers.executable: server_file,
Servers.execution_command: server_command,
Servers.auto_start: False,
Servers.auto_start_delay: 10,
Servers.crash_detection: False,
Servers.log_path: server_log_file,
Servers.server_port: server_port,
Servers.server_ip: server_host,
Servers.stop_command: server_stop,
Servers.backup_path: backup_path,
Servers.type: server_type,
Servers.created_by: created_by,
}
).execute()
return Servers.create(
server_id=server_id,
server_uuid=server_id,
server_name=name,
path=server_dir,
executable=server_file,
execution_command=server_command,
auto_start=False,
auto_start_delay=10,
crash_detection=False,
log_path=server_log_file,
server_port=server_port,
server_ip=server_host,
stop_command=server_stop,
backup_path=backup_path,
type=server_type,
created_by=created_by,
).server_id
@staticmethod
def get_server_obj(server_id):

View File

@ -18,7 +18,12 @@ logger = logging.getLogger(__name__)
class MainPrompt(cmd.Cmd):
def __init__(
self, helper, tasks_manager, migration_manager, main_controller, import3
self,
helper,
tasks_manager,
migration_manager,
main_controller,
import3,
):
super().__init__()
self.helper: Helpers = helper
@ -77,11 +82,11 @@ class MainPrompt(cmd.Cmd):
# get new password from user
new_pass = getpass.getpass(prompt=f"NEW password for: {username} > ")
# check to make sure it fits our requirements.
if len(new_pass) > 512:
Console.warning("Passwords must be greater than 6char long and under 512")
return False
if len(new_pass) < 6:
Console.warning("Passwords must be greater than 6char long and under 512")
if len(new_pass) < self.helper.minimum_password_length:
Console.warning(
"Passwords must be greater than"
f" {self.helper.minimum_password_length} char long"
)
return False
# grab repeated password input
new_pass_conf = getpass.getpass(prompt="Re-enter your password: > ")

View File

@ -5,6 +5,10 @@ import pathlib
import tempfile
import zipfile
from zipfile import ZipFile, ZIP_DEFLATED
import urllib.request
import ssl
import time
import certifi
from app.classes.shared.helpers import Helpers
from app.classes.shared.console import Console
@ -19,6 +23,92 @@ class FileHelpers:
def __init__(self, helper):
self.helper: Helpers = helper
@staticmethod
def ssl_get_file(
url, out_path, out_file, max_retries=3, backoff_factor=2, headers=None
):
"""
Downloads a file from a given URL using HTTPS with SSL context verification,
retries with exponential backoff and providing download progress feedback.
Parameters:
- url (str): The URL of the file to download. Must start with "https".
- out_path (str): The local path where the file will be saved.
- out_file (str): The name of the file to save the downloaded content as.
- max_retries (int, optional): The maximum number of retry attempts
in case of download failure. Defaults to 3.
- backoff_factor (int, optional): The factor by which the wait time
increases after each failed attempt. Defaults to 2.
- headers (dict, optional):
A dictionary of HTTP headers to send with the request.
Returns:
- bool: True if the download was successful, False otherwise.
Raises:
- urllib.error.URLError: If a URL error occurs during the download.
- ssl.SSLError: If an SSL error occurs during the download.
Exception: If an unexpected error occurs during the download.
Note:
This method logs critical errors and download progress information.
Ensure that the logger is properly configured to capture this information.
"""
if not url.lower().startswith("https"):
logger.error("SSL File Get - Error: URL must start with https.")
return False
ssl_context = ssl.create_default_context(cafile=certifi.where())
if not headers:
headers = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/58.0.3029.110 Safari/537.3"
)
}
req = urllib.request.Request(url, headers=headers)
write_path = os.path.join(out_path, out_file)
attempt = 0
logger.info(f"SSL File Get - Requesting remote: {url}")
file_path_full = os.path.join(out_path, out_file)
logger.info(f"SSL File Get - Download Destination: {file_path_full}")
while attempt < max_retries:
try:
with urllib.request.urlopen(req, context=ssl_context) as response:
total_size = response.getheader("Content-Length")
if total_size:
total_size = int(total_size)
downloaded = 0
with open(write_path, "wb") as file:
while True:
chunk = response.read(1024 * 1024) # 1 MB
if not chunk:
break
file.write(chunk)
downloaded += len(chunk)
if total_size:
progress = (downloaded / total_size) * 100
logger.info(
f"SSL File Get - Download progress: {progress:.2f}%"
)
return True
except (urllib.error.URLError, ssl.SSLError) as e:
logger.warning(f"SSL File Get - Attempt {attempt+1} failed: {e}")
time.sleep(backoff_factor**attempt)
except Exception as e:
logger.critical(f"SSL File Get - Unexpected error: {e}")
return False
finally:
attempt += 1
logger.error("SSL File Get - Maximum retries reached. Download failed.")
return False
@staticmethod
def del_dirs(path):
path = pathlib.Path(path)

View File

@ -81,6 +81,7 @@ class Helpers:
self.update_available = False
self.ignored_names = ["crafty_managed.txt", "db_stats"]
self.crafty_starting = False
self.minimum_password_length = 8
@staticmethod
def auto_installer_fix(ex):
@ -117,7 +118,7 @@ class Helpers:
Get latest bedrock executable url \n\n
returns url if successful, False if not
"""
url = "https://minecraft.net/en-us/download/server/bedrock/"
url = "https://www.minecraft.net/en-us/download/server/bedrock/"
headers = {
"Accept-Encoding": "identity",
"Accept-Language": "en",
@ -1112,7 +1113,7 @@ class Helpers:
return os.path.normpath(path)
def find_default_password(self):
default_file = os.path.join(self.root_dir, "default.json")
default_file = os.path.join(self.root_dir, "app", "config", "default.json")
data = {}
if Helpers.check_file_exists(default_file):
@ -1180,25 +1181,6 @@ class Helpers:
return temp_dir
return False
@staticmethod
def download_file(executable_url, jar_path):
try:
response = requests.get(executable_url, timeout=5)
except Exception as ex:
logger.error("Could not download executable: %s", ex)
return False
if response.status_code != 200:
logger.error("Unable to download file from %s", executable_url)
return False
try:
with open(jar_path, "wb") as jar_file:
jar_file.write(response.content)
except Exception as e:
logger.error("Unable to finish executable download. Error: %s", e)
return False
return True
@staticmethod
def remove_prefix(text, prefix):
if text.startswith(prefix):

View File

@ -3,7 +3,6 @@ import time
import shutil
import logging
import threading
import urllib
from app.classes.controllers.server_perms_controller import PermissionsServers
from app.classes.controllers.servers_controller import ServersController
@ -227,25 +226,39 @@ class ImportHelpers:
download_thread.start()
def download_threaded_bedrock_server(self, path, new_id):
# downloads zip from remote url
"""
Downloads the latest Bedrock server, unzips it, sets necessary permissions.
Parameters:
path (str): The directory path to download and unzip the Bedrock server.
new_id (str): The identifier for the new server import operation.
This method handles exceptions and logs errors for each step of the process.
"""
try:
bedrock_url = Helpers.get_latest_bedrock_url()
if bedrock_url.lower().startswith("https"):
urllib.request.urlretrieve(
bedrock_url,
os.path.join(path, "bedrock_server.zip"),
if bedrock_url:
file_path = os.path.join(path, "bedrock_server.zip")
success = FileHelpers.ssl_get_file(
bedrock_url, path, "bedrock_server.zip"
)
if not success:
logger.error("Failed to download the Bedrock server zip.")
return
unzip_path = os.path.join(path, "bedrock_server.zip")
unzip_path = self.helper.wtol_path(unzip_path)
# unzips archive that was downloaded.
FileHelpers.unzip_file(unzip_path)
# adjusts permissions for execution if os is not windows
if not self.helper.is_os_windows():
os.chmod(os.path.join(path, "bedrock_server"), 0o0744)
unzip_path = self.helper.wtol_path(file_path)
# unzips archive that was downloaded.
FileHelpers.unzip_file(unzip_path)
# adjusts permissions for execution if os is not windows
# we'll delete the zip we downloaded now
os.remove(os.path.join(path, "bedrock_server.zip"))
if not self.helper.is_os_windows():
os.chmod(os.path.join(path, "bedrock_server"), 0o0744)
# we'll delete the zip we downloaded now
os.remove(file_path)
else:
logger.error("Bedrock download URL issue!")
except Exception as e:
logger.critical(
f"Failed to download bedrock executable during server creation! \n{e}"

View File

@ -239,7 +239,7 @@ class Controller:
try:
os.mkdir(final_path)
except FileExistsError:
final_path += "_" + server["server_uuid"]
final_path += "_" + server["server_id"]
os.mkdir(final_path)
try:
FileHelpers.copy_file(
@ -632,11 +632,11 @@ class Controller:
# and add the user to it if he's not a superuser
if len(captured_roles) == 0:
if not exec_user["superuser"]:
new_server_uuid = self.servers.get_server_data_by_id(new_server_id).get(
"server_uuid"
new_server_id = self.servers.get_server_data_by_id(new_server_id).get(
"server_id"
)
role_id = self.roles.add_role(
f"Creator of Server with uuid={new_server_uuid}",
f"Creator of Server with id={new_server_id}",
exec_user["user_id"],
)
self.server_perms.add_role_server(new_server_id, role_id, "11111111")
@ -647,7 +647,7 @@ class Controller:
role_id = role
self.server_perms.add_role_server(new_server_id, role_id, "11111111")
return new_server_id, server_fs_uuid
return new_server_id
@staticmethod
def verify_jar_server(server_path: str, server_jar: str):
@ -1095,7 +1095,7 @@ class Controller:
for server in servers:
server_path = server.get("path")
new_local_server_path = os.path.join(
new_server_path, server.get("server_uuid")
new_server_path, server.get("server_id")
)
if os.path.isdir(server_path):
WebSocketManager().broadcast_page(

View File

@ -18,13 +18,22 @@ class DatabaseBuilder:
logger.info("Fresh Install Detected - Creating Default Settings")
Console.info("Fresh Install Detected - Creating Default Settings")
default_data = self.helper.find_default_password()
if password not in default_data:
if "password" not in default_data:
Console.help(
"No default password found. Using password created "
"by Crafty. Find it in app/config/default-creds.txt"
)
username = default_data.get("username", "admin")
password = default_data.get("password", password)
if self.helper.minimum_password_length > len(
default_data.get("password", password)
):
Console.critical(
"Default password too short"
" using Crafty's created default."
" Find it in app/config/default-creds.txt"
)
else:
password = default_data.get("password", password)
self.users_helper.add_user(
username=username,

View File

@ -200,6 +200,21 @@ class Migrator(object):
)
return model
@get_model
def alter_column_type(
self,
model: peewee.Model,
column_name: str,
field: peewee.Field,
) -> peewee.Model:
"""
Alter field data type in database.
"""
self.operations.append(
self.migrator.alter_column_type(model._meta.table_name, column_name, field)
)
return model
@get_model
def rename_table(self, model: peewee.Model, new_name: str) -> peewee.Model:
"""

View File

@ -10,7 +10,6 @@ import threading
import logging.config
import subprocess
import html
import urllib.request
import glob
import json
@ -209,7 +208,7 @@ class ServerInstance:
self.dir_scheduler.start()
self.start_dir_calc_task()
self.backup_thread = threading.Thread(
target=self.a_backup_server, daemon=True, name=f"backup_{self.name}"
target=self.backup_server, daemon=True, name=f"backup_{self.name}"
)
self.is_backingup = False
# Reset crash and update at initialization
@ -697,6 +696,10 @@ class ServerInstance:
version_param = version[0][0].split(".")
version_major = int(version_param[0])
version_minor = int(version_param[1])
if len(version_param) > 2:
version_sub = int(version_param[2])
else:
version_sub = 0
# Checking which version we are with
if version_major <= 1 and version_minor < 17:
@ -730,8 +733,8 @@ class ServerInstance:
server_obj.execution_command = execution_command
Console.debug(SUCCESSMSG)
elif version_major <= 1 and version_minor < 20:
# NEW VERSION >= 1.17 and <= 1.20
elif version_major <= 1 and version_minor <= 20 and version_sub < 3:
# NEW VERSION >= 1.17 and <= 1.20.2
# (no jar file in server dir, only run.bat and run.sh)
run_file_path = ""
@ -778,7 +781,7 @@ class ServerInstance:
server_obj.execution_command = execution_command
Console.debug(SUCCESSMSG)
else:
# NEW VERSION >= 1.20
# NEW VERSION >= 1.20.3
# (executable jar is back in server dir)
# Retrieving the executable jar filename
@ -1107,13 +1110,12 @@ class ServerInstance:
f.write("eula=true")
self.run_threaded_server(user_id)
@callback
def backup_server(self):
def a_backup_server(self):
if self.settings["backup_path"] == "":
logger.critical("Backup path is None. Canceling Backup!")
return
backup_thread = threading.Thread(
target=self.a_backup_server, daemon=True, name=f"backup_{self.name}"
target=self.backup_server, daemon=True, name=f"backup_{self.name}"
)
logger.info(
f"Starting Backup Thread for server {self.settings['server_name']}."
@ -1140,7 +1142,8 @@ class ServerInstance:
return False
logger.info(f"Backup Thread started for server {self.settings['server_name']}.")
def a_backup_server(self):
@callback
def backup_server(self):
was_server_running = None
logger.info(f"Starting server {self.name} (ID {self.server_id}) backup")
server_users = PermissionsServers.get_server_user_list(self.server_id)
@ -1370,7 +1373,7 @@ class ServerInstance:
def a_jar_update(self):
server_users = PermissionsServers.get_server_user_list(self.server_id)
was_started = "-1"
self.backup_server()
self.a_backup_server()
# checks if server is running. Calls shutdown if it is running.
if self.check_running():
was_started = True
@ -1450,33 +1453,45 @@ class ServerInstance:
# lets download the files
if HelperServers.get_server_type_by_id(self.server_id) != "minecraft-bedrock":
# boolean returns true for false for success
downloaded = Helpers.download_file(
self.settings["executable_update_url"], current_executable
jar_dir = os.path.dirname(current_executable)
jar_file_name = os.path.basename(current_executable)
downloaded = FileHelpers.ssl_get_file(
self.settings["executable_update_url"], jar_dir, jar_file_name
)
else:
# downloads zip from remote url
try:
bedrock_url = Helpers.get_latest_bedrock_url()
if bedrock_url.lower().startswith("https"):
urllib.request.urlretrieve(
bedrock_url,
os.path.join(self.settings["path"], "bedrock_server.zip"),
if bedrock_url:
# Use the new method for secure download
download_path = os.path.join(
self.settings["path"], "bedrock_server.zip"
)
downloaded = FileHelpers.ssl_get_file(
bedrock_url, self.settings["path"], "bedrock_server.zip"
)
unzip_path = os.path.join(self.settings["path"], "bedrock_server.zip")
unzip_path = self.helper.wtol_path(unzip_path)
# unzips archive that was downloaded.
FileHelpers.unzip_file(unzip_path, server_update=True)
# adjusts permissions for execution if os is not windows
if not self.helper.is_os_windows():
os.chmod(
os.path.join(self.settings["path"], "bedrock_server"), 0o0744
)
if downloaded:
unzip_path = download_path
unzip_path = self.helper.wtol_path(unzip_path)
# we'll delete the zip we downloaded now
os.remove(os.path.join(self.settings["path"], "bedrock_server.zip"))
downloaded = True
# unzips archive that was downloaded.
FileHelpers.unzip_file(unzip_path, server_update=True)
# adjusts permissions for execution if os is not windows
if not self.helper.is_os_windows():
os.chmod(
os.path.join(self.settings["path"], "bedrock_server"),
0o0744,
)
# we'll delete the zip we downloaded now
os.remove(download_path)
else:
logger.error("Failed to download the Bedrock server zip.")
downloaded = False
except Exception as e:
logger.critical(
f"Failed to download bedrock executable for update \n{e}"

View File

@ -140,7 +140,7 @@ class TasksManager:
)
elif command == "backup_server":
svr.backup_server()
svr.a_backup_server()
elif command == "update_executable":
svr.jar_update()

View File

@ -174,7 +174,7 @@ class PanelHandler(BaseHandler):
self.redirect("/panel/error?error=Invalid Server ID")
return None
for server in self.controller.servers.failed_servers:
if int(server_id) == server["server_id"]:
if server_id == server["server_id"]:
self.failed_server = True
return server_id
# Does this server exist?
@ -345,15 +345,17 @@ class PanelHandler(BaseHandler):
self.controller.users.get_user_lang_by_id(exec_user["user_id"])
),
"super_user": superuser,
"api_key": {
"name": api_key.name,
"created": api_key.created,
"server_permissions": api_key.server_permissions,
"crafty_permissions": api_key.crafty_permissions,
"superuser": api_key.superuser,
}
if api_key is not None
else None,
"api_key": (
{
"name": api_key.name,
"created": api_key.created,
"server_permissions": api_key.server_permissions,
"crafty_permissions": api_key.crafty_permissions,
"superuser": api_key.superuser,
}
if api_key is not None
else None
),
"superuser": superuser,
}
try:
@ -417,14 +419,14 @@ class PanelHandler(BaseHandler):
self.controller.first_login = False
if superuser: # TODO: Figure out a better solution
try:
page_data[
"servers"
] = self.controller.servers.get_all_servers_stats()
page_data["servers"] = (
self.controller.servers.get_all_servers_stats()
)
except IndexError:
self.controller.servers.stats.record_stats()
page_data[
"servers"
] = self.controller.servers.get_all_servers_stats()
page_data["servers"] = (
self.controller.servers.get_all_servers_stats()
)
else:
try:
user_auth = self.controller.servers.get_authorized_servers_stats(
@ -454,19 +456,19 @@ class PanelHandler(BaseHandler):
for server_id in user_order[:]:
for server in un_used_servers[:]:
if flag == 0:
server["stats"][
"importing"
] = self.controller.servers.get_import_status(
str(server["stats"]["server_id"]["server_id"])
server["stats"]["importing"] = (
self.controller.servers.get_import_status(
str(server["stats"]["server_id"]["server_id"])
)
)
server["stats"]["crashed"] = self.controller.servers.is_crashed(
str(server["stats"]["server_id"]["server_id"])
)
try:
server["stats"][
"waiting_start"
] = self.controller.servers.get_waiting_start(
str(server["stats"]["server_id"]["server_id"])
server["stats"]["waiting_start"] = (
self.controller.servers.get_waiting_start(
str(server["stats"]["server_id"]["server_id"])
)
)
except Exception as e:
logger.error(f"Failed to get server waiting to start: {e}")
@ -543,9 +545,9 @@ class PanelHandler(BaseHandler):
server_id
)
if not self.failed_server:
page_data[
"server_stats"
] = self.controller.servers.get_server_stats_by_id(server_id)
page_data["server_stats"] = (
self.controller.servers.get_server_stats_by_id(server_id)
)
else:
server_temp_obj = self.controller.servers.get_server_data_by_id(
server_id
@ -554,7 +556,7 @@ class PanelHandler(BaseHandler):
"server_id": {
"server_id": server_id,
"server_name": server_temp_obj["server_name"],
"server_uuid": server_temp_obj["server_uuid"],
"server_uuid": server_temp_obj["server_id"],
"path": server_temp_obj["path"],
"log_path": server_temp_obj["log_path"],
"executable": server_temp_obj["executable"],
@ -572,6 +574,7 @@ class PanelHandler(BaseHandler):
"crash_detection": server_temp_obj["crash_detection"],
"show_status": server_temp_obj["show_status"],
"ignored_exits": server_temp_obj["ignored_exits"],
"count_players": server_temp_obj["count_players"],
},
"running": False,
"crashed": False,
@ -611,19 +614,19 @@ class PanelHandler(BaseHandler):
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
}
page_data[
"user_permissions"
] = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
page_data["user_permissions"] = (
self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
)
if not self.failed_server:
page_data["server_stats"][
"crashed"
] = self.controller.servers.is_crashed(server_id)
page_data["server_stats"]["crashed"] = (
self.controller.servers.is_crashed(server_id)
)
if not self.failed_server:
page_data["server_stats"][
"server_type"
] = self.controller.servers.get_server_type_by_id(server_id)
page_data["server_stats"]["server_type"] = (
self.controller.servers.get_server_type_by_id(server_id)
)
if not subpage:
for spage, perm in SUBPAGE_PERMS.items():
@ -674,23 +677,23 @@ class PanelHandler(BaseHandler):
page_data["java_versions"] = page_java
if subpage == "backup":
server_info = self.controller.servers.get_server_data_by_id(server_id)
page_data[
"backup_config"
] = self.controller.management.get_backup_config(server_id)
page_data["backup_config"] = (
self.controller.management.get_backup_config(server_id)
)
exclusions = []
page_data[
"exclusions"
] = self.controller.management.get_excluded_backup_dirs(server_id)
page_data[
"backing_up"
] = self.controller.servers.get_server_instance_by_id(
server_id
).is_backingup
page_data[
"backup_stats"
] = self.controller.servers.get_server_instance_by_id(
server_id
).send_backup_status()
page_data["exclusions"] = (
self.controller.management.get_excluded_backup_dirs(server_id)
)
page_data["backing_up"] = (
self.controller.servers.get_server_instance_by_id(
server_id
).is_backingup
)
page_data["backup_stats"] = (
self.controller.servers.get_server_instance_by_id(
server_id
).send_backup_status()
)
# makes it so relative path is the only thing shown
for file in page_data["exclusions"]:
if Helpers.is_os_windows():
@ -723,10 +726,10 @@ class PanelHandler(BaseHandler):
server_id, hours=(days * 24)
)
if subpage == "webhooks":
page_data[
"webhooks"
] = self.controller.management.get_webhooks_by_server(
server_id, model=True
page_data["webhooks"] = (
self.controller.management.get_webhooks_by_server(
server_id, model=True
)
)
page_data["triggers"] = WebhookFactory.get_monitored_events()
@ -758,9 +761,9 @@ class PanelHandler(BaseHandler):
if not superuser:
self.redirect("/panel/error?error=Unauthorized access")
page_data["banned_players_html"] = get_banned_players_html()
page_data[
"banned_players"
] = self.controller.servers.get_banned_players(server_id)
page_data["banned_players"] = (
self.controller.servers.get_banned_players(server_id)
)
server_instance = self.controller.servers.get_server_instance_by_id(
server_id
)
@ -925,9 +928,9 @@ class PanelHandler(BaseHandler):
if item not in page_data["backgrounds"]:
page_data["backgrounds"].append(item)
page_data["background"] = self.controller.cached_login
page_data[
"login_opacity"
] = self.controller.management.get_login_opacity()
page_data["login_opacity"] = (
self.controller.management.get_login_opacity()
)
page_data["active_link"] = "custom_login"
template = "panel/custom_login.html"
@ -959,13 +962,11 @@ class PanelHandler(BaseHandler):
page_data["servers"] = []
page_data["servers_all"] = self.controller.servers.get_all_defined_servers()
page_data["role-servers"] = []
page_data[
"permissions_all"
] = self.controller.crafty_perms.list_defined_crafty_permissions()
page_data["permissions_all"] = (
self.controller.crafty_perms.list_defined_crafty_permissions()
)
page_data["permissions_list"] = set()
page_data[
"quantity_server"
] = (
page_data["quantity_server"] = (
self.controller.crafty_perms.list_all_crafty_permissions_quantity_limits() # pylint: disable=line-too-long
)
page_data["languages"] = []
@ -1007,10 +1008,10 @@ class PanelHandler(BaseHandler):
page_data["server_data"] = self.controller.servers.get_server_data_by_id(
server_id
)
page_data[
"user_permissions"
] = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
page_data["user_permissions"] = (
self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
)
page_data["permissions"] = {
"Commands": EnumPermissionsServer.COMMANDS,
@ -1025,9 +1026,9 @@ class PanelHandler(BaseHandler):
page_data["server_stats"] = self.controller.servers.get_server_stats_by_id(
server_id
)
page_data["server_stats"][
"server_type"
] = self.controller.servers.get_server_type_by_id(server_id)
page_data["server_stats"]["server_type"] = (
self.controller.servers.get_server_type_by_id(server_id)
)
page_data["new_webhook"] = True
page_data["webhook"] = {}
page_data["webhook"]["webhook_type"] = "Custom"
@ -1061,10 +1062,10 @@ class PanelHandler(BaseHandler):
page_data["server_data"] = self.controller.servers.get_server_data_by_id(
server_id
)
page_data[
"user_permissions"
] = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
page_data["user_permissions"] = (
self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
)
page_data["permissions"] = {
"Commands": EnumPermissionsServer.COMMANDS,
@ -1079,9 +1080,9 @@ class PanelHandler(BaseHandler):
page_data["server_stats"] = self.controller.servers.get_server_stats_by_id(
server_id
)
page_data["server_stats"][
"server_type"
] = self.controller.servers.get_server_type_by_id(server_id)
page_data["server_stats"]["server_type"] = (
self.controller.servers.get_server_type_by_id(server_id)
)
page_data["new_webhook"] = False
page_data["webhook"] = self.controller.management.get_webhook_by_id(
webhook_id
@ -1121,10 +1122,10 @@ class PanelHandler(BaseHandler):
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
}
page_data[
"user_permissions"
] = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
page_data["user_permissions"] = (
self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
)
page_data["server_data"] = self.controller.servers.get_server_data_by_id(
server_id
@ -1132,9 +1133,9 @@ class PanelHandler(BaseHandler):
page_data["server_stats"] = self.controller.servers.get_server_stats_by_id(
server_id
)
page_data["server_stats"][
"server_type"
] = self.controller.servers.get_server_type_by_id(server_id)
page_data["server_stats"]["server_type"] = (
self.controller.servers.get_server_type_by_id(server_id)
)
page_data["new_schedule"] = True
page_data["schedule"] = {}
page_data["schedule"]["children"] = []
@ -1189,10 +1190,10 @@ class PanelHandler(BaseHandler):
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
}
page_data[
"user_permissions"
] = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
page_data["user_permissions"] = (
self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
)
page_data["server_data"] = self.controller.servers.get_server_data_by_id(
server_id
@ -1200,9 +1201,9 @@ class PanelHandler(BaseHandler):
page_data["server_stats"] = self.controller.servers.get_server_stats_by_id(
server_id
)
page_data["server_stats"][
"server_type"
] = self.controller.servers.get_server_type_by_id(server_id)
page_data["server_stats"]["server_type"] = (
self.controller.servers.get_server_type_by_id(server_id)
)
page_data["new_schedule"] = False
page_data["schedule"] = {}
page_data["schedule"]["server_id"] = server_id
@ -1212,9 +1213,9 @@ class PanelHandler(BaseHandler):
page_data["schedule"]["name"] = schedule.name
else:
page_data["schedule"]["name"] = ""
page_data["schedule"][
"children"
] = self.controller.management.get_child_schedules(sch_id)
page_data["schedule"]["children"] = (
self.controller.management.get_child_schedules(sch_id)
)
# We check here to see if the command is any of the default ones.
# We do not want a user changing to a custom command
# and seeing our command there.
@ -1280,16 +1281,16 @@ class PanelHandler(BaseHandler):
}
if exec_user["superuser"]:
page_data["users"] = self.controller.users.get_all_users()
page_data[
"permissions_all"
] = self.controller.crafty_perms.list_defined_crafty_permissions()
page_data[
"permissions_list"
] = self.controller.crafty_perms.get_crafty_permissions_list(user_id)
page_data[
"quantity_server"
] = self.controller.crafty_perms.list_crafty_permissions_quantity_limits(
user_id
page_data["permissions_all"] = (
self.controller.crafty_perms.list_defined_crafty_permissions()
)
page_data["permissions_list"] = (
self.controller.crafty_perms.get_crafty_permissions_list(user_id)
)
page_data["quantity_server"] = (
self.controller.crafty_perms.list_crafty_permissions_quantity_limits(
user_id
)
)
page_data["languages"] = []
page_data["languages"].append(
@ -1349,12 +1350,12 @@ class PanelHandler(BaseHandler):
page_data["user"] = self.controller.users.get_user_by_id(user_id)
page_data["api_keys"] = self.controller.users.get_user_api_keys(user_id)
# self.controller.crafty_perms.list_defined_crafty_permissions()
page_data[
"server_permissions_all"
] = self.controller.server_perms.list_defined_permissions()
page_data[
"crafty_permissions_all"
] = self.controller.crafty_perms.list_defined_crafty_permissions()
page_data["server_permissions_all"] = (
self.controller.server_perms.list_defined_permissions()
)
page_data["crafty_permissions_all"] = (
self.controller.crafty_perms.list_defined_crafty_permissions()
)
if user_id is None:
self.redirect("/panel/error?error=Invalid User ID")
@ -1442,9 +1443,9 @@ class PanelHandler(BaseHandler):
DatabaseShortcuts.get_data_obj(server.server_object)
)
page_data["servers_all"] = page_servers
page_data[
"permissions_all"
] = self.controller.server_perms.list_defined_permissions()
page_data["permissions_all"] = (
self.controller.server_perms.list_defined_permissions()
)
page_data["permissions_dict"] = {}
template = "panel/panel_edit_role.html"
@ -1467,12 +1468,12 @@ class PanelHandler(BaseHandler):
DatabaseShortcuts.get_data_obj(server.server_object)
)
page_data["servers_all"] = page_servers
page_data[
"permissions_all"
] = self.controller.server_perms.list_defined_permissions()
page_data[
"permissions_dict"
] = self.controller.server_perms.get_role_permissions_dict(role_id)
page_data["permissions_all"] = (
self.controller.server_perms.list_defined_permissions()
)
page_data["permissions_dict"] = (
self.controller.server_perms.get_role_permissions_dict(role_id)
)
page_data["user-roles"] = user_roles
page_data["users"] = self.controller.users.get_all_users()

View File

@ -1,5 +1,8 @@
import logging
import json
import nh3
from jsonschema import validate
from jsonschema.exceptions import ValidationError
from app.classes.shared.helpers import Helpers
from app.classes.models.users import HelperUsers
@ -45,7 +48,7 @@ class PublicHandler(BaseHandler):
}
if self.request.query:
page_data["query"] = self.request.query
page_data["query"] = self.request.query_arguments.get("next")[0].decode()
# sensible defaults
template = "public/404.html"
@ -75,11 +78,7 @@ class PublicHandler(BaseHandler):
# if we have no page, let's go to login
else:
if self.request.query:
self.redirect("/login?" + self.request.query)
else:
self.redirect("/login")
return
return self.redirect("/login")
self.render(
template,
@ -89,33 +88,61 @@ class PublicHandler(BaseHandler):
)
def post(self, page=None):
# pylint: disable=no-member
error = nh3.clean(self.get_argument("error", "Invalid Login!"))
error_msg = nh3.clean(self.get_argument("error_msg", ""))
# pylint: enable=no-member
login_schema = {
"type": "object",
"properties": {
"username": {
"type": "string",
},
"password": {"type": "string"},
},
"required": ["username", "password"],
"additionalProperties": False,
}
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
logger.error(
"Invalid JSON schema for API"
f" login attempt from {self.get_remote_ip()}"
)
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, login_schema)
except ValidationError as e:
logger.error(
"Invalid JSON schema for API"
f" login attempt from {self.get_remote_ip()}"
)
return self.finish_json(
400,
{
"status": "error",
"error": "VWggb2ghIFN0aW5reS 🪠",
"error_data": str(e),
},
)
page_data = {
"version": self.helper.get_version_string(),
"error": error,
"lang": self.helper.get_setting("language"),
"lang_page": self.helper.get_lang_page(self.helper.get_setting("language")),
"query": "",
}
if self.request.query:
page_data["query"] = self.request.query
page_data["query"] = self.request.query_arguments.get("next")[0].decode()
if page == "login":
data = json.loads(self.request.body)
auth_log.info(
f"User attempting to authenticate from {self.get_remote_ip()}"
)
next_page = "/login"
if self.request.query:
next_page = "/login?" + self.request.query
# pylint: disable=no-member
entered_username = nh3.clean(self.get_argument("username"))
entered_password = self.get_argument("password")
# pylint: enable=no-member
entered_username = nh3.clean(data["username"]) # pylint: disable=no-member
entered_password = data["password"]
try:
user_id = HelperUsers.get_user_id_by_name(entered_username.lower())
@ -127,16 +154,18 @@ class PublicHandler(BaseHandler):
f" Authentication failed from remote IP {self.get_remote_ip()}"
" Users does not exist."
)
error_msg = "Incorrect username or password. Please try again."
self.finish_json(
403,
{
"status": "error",
"error": self.helper.translation.translate(
"login", "incorrect", self.helper.get_setting("language")
),
},
)
# self.clear_cookie("user")
# self.clear_cookie("user_data")
self.clear_cookie("token")
if self.request.query:
self.redirect(f"/login?error_msg={error_msg}&{self.request.query}")
else:
self.redirect(f"/login?error_msg={error_msg}")
return
return self.clear_cookie("token")
# if we don't have a user
if not user_data:
auth_log.error(
@ -145,15 +174,18 @@ class PublicHandler(BaseHandler):
" User does not exist."
)
self.controller.log_attempt(self.get_remote_ip(), entered_username)
error_msg = "Incorrect username or password. Please try again."
self.finish_json(
403,
{
"status": "error",
"error": self.helper.translation.translate(
"login", "incorrect", self.helper.get_setting("language")
),
},
)
# self.clear_cookie("user")
# self.clear_cookie("user_data")
self.clear_cookie("token")
if self.request.query:
self.redirect(f"/login?error_msg={error_msg}&{self.request.query}")
else:
self.redirect(f"/login?error_msg={error_msg}")
return
return self.clear_cookie("token")
# if they are disabled
if not user_data.enabled:
@ -163,19 +195,18 @@ class PublicHandler(BaseHandler):
" User account disabled"
)
self.controller.log_attempt(self.get_remote_ip(), entered_username)
error_msg = (
"User account disabled. Please contact "
"your system administrator for more info."
self.finish_json(
403,
{
"status": "error",
"error": self.helper.translation.translate(
"login", "disabled", self.helper.get_setting("language")
),
},
)
# self.clear_cookie("user")
# self.clear_cookie("user_data")
self.clear_cookie("token")
if self.request.query:
self.redirect(f"/login?error_msg={error_msg}&{self.request.query}")
else:
self.redirect(f"/login?error_msg={error_msg}")
return
return self.clear_cookie("token")
login_result = self.helper.verify_pass(entered_password, user_data.password)
# Valid Login
@ -200,32 +231,34 @@ class PublicHandler(BaseHandler):
user_data.user_id, "Logged in", 0, self.get_remote_ip()
)
if self.request.query_arguments.get("next"):
next_page = self.request.query_arguments.get("next")[0].decode()
else:
next_page = "/panel/dashboard"
return self.finish_json(
200, {"status": "ok", "data": {"message": "login successful!"}}
)
self.redirect(next_page)
else:
auth_log.error(
f"User attempted to log into {entered_username}."
f" Authentication failed from remote IP {self.get_remote_ip()}"
# We'll continue on and handle unsuccessful logins
auth_log.error(
f"User attempted to log into {entered_username}."
f" Authentication failed from remote IP {self.get_remote_ip()}"
)
self.controller.log_attempt(self.get_remote_ip(), entered_username)
# self.clear_cookie("user")
# self.clear_cookie("user_data")
self.clear_cookie("token")
error_msg = self.helper.translation.translate(
"login", "incorrect", self.helper.get_setting("language")
)
if entered_password == "app/config/default-creds.txt":
error_msg += ". "
error_msg += self.helper.translation.translate(
"login", "defaultPath", self.helper.get_setting("language")
)
self.controller.log_attempt(self.get_remote_ip(), entered_username)
# self.clear_cookie("user")
# self.clear_cookie("user_data")
self.clear_cookie("token")
error_msg = "Incorrect username or password. Please try again."
# log this failed login attempt
self.controller.management.add_to_audit_log(
user_data.user_id, "Tried to log in", 0, self.get_remote_ip()
)
if self.request.query:
self.redirect(f"/login?error_msg={error_msg}&{self.request.query}")
else:
self.redirect(f"/login?error_msg={error_msg}")
# log this failed login attempt
self.controller.management.add_to_audit_log(
user_data.user_id, "Tried to log in", 0, self.get_remote_ip()
)
return self.finish_json(
403,
{"status": "error", "error": error_msg},
)
else:
if self.request.query:
self.redirect("/login?" + self.request.query)
else:
self.redirect("/login")
self.redirect("/login?")

View File

@ -208,92 +208,92 @@ def api_handlers(handler_args):
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/?",
r"/api/v2/servers/([a-z0-9-]+)/?",
ApiServersServerIndexHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/backups/?",
r"/api/v2/servers/([a-z0-9-]+)/backups/?",
ApiServersServerBackupsIndexHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/backups/backup/?",
r"/api/v2/servers/([a-z0-9-]+)/backups/backup/?",
ApiServersServerBackupsBackupIndexHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/files/?",
r"/api/v2/servers/([a-z0-9-]+)/files/?",
ApiServersServerFilesIndexHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/files/create/?",
r"/api/v2/servers/([a-z0-9-]+)/files/create/?",
ApiServersServerFilesCreateHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/files/zip/?",
r"/api/v2/servers/([a-z0-9-]+)/files/zip/?",
ApiServersServerFilesZipHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/tasks/?",
r"/api/v2/servers/([a-z0-9-]+)/tasks/?",
ApiServersServerTasksIndexHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/tasks/([0-9]+)/?",
r"/api/v2/servers/([a-z0-9-]+)/tasks/([0-9]+)/?",
ApiServersServerTasksTaskIndexHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/tasks/([0-9]+)/children/?",
r"/api/v2/servers/([a-z0-9-]+)/tasks/([0-9]+)/children/?",
ApiServersServerTasksTaskChildrenHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/stats/?",
r"/api/v2/servers/([a-z0-9-]+)/stats/?",
ApiServersServerStatsHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/history/?",
r"/api/v2/servers/([a-z0-9-]+)/history/?",
ApiServersServerHistoryHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/webhook/([0-9]+)/?",
r"/api/v2/servers/([a-z0-9-]+)/webhook/([0-9]+)/?",
ApiServersServerWebhooksManagementIndexHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/webhook/?",
r"/api/v2/servers/([a-z0-9-]+)/webhook/?",
ApiServersServerWebhooksIndexHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/action/([a-z_]+)/?",
r"/api/v2/servers/([a-z0-9-]+)/action/([a-z_]+)/?",
ApiServersServerActionHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/logs/?",
r"/api/v2/servers/([a-z0-9-]+)/logs/?",
ApiServersServerLogsHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/users/?",
r"/api/v2/servers/([a-z0-9-]+)/users/?",
ApiServersServerUsersHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/public/?",
r"/api/v2/servers/([a-z0-9-]+)/public/?",
ApiServersServerPublicHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/stdin/?",
r"/api/v2/servers/([a-z0-9-]+)/stdin/?",
ApiServersServerStdinHandler,
handler_args,
),

View File

@ -17,7 +17,7 @@ login_schema = {
"minLength": 4,
"pattern": "^[a-z0-9_]+$",
},
"password": {"type": "string", "maxLength": 20, "minLength": 4},
"password": {"type": "string", "minLength": 4},
},
"required": ["username", "password"],
"additionalProperties": False,

View File

@ -80,9 +80,13 @@ class ApiCraftyConfigIndexHandler(BaseApiHandler):
200,
{
"status": "ok",
"data": self.controller.roles.get_all_role_ids()
if get_only_ids
else [model_to_dict(r) for r in self.controller.roles.get_all_roles()],
"data": (
self.controller.roles.get_all_role_ids()
if get_only_ids
else [
model_to_dict(r) for r in self.controller.roles.get_all_roles()
]
),
},
)
@ -158,9 +162,13 @@ class ApiCraftyCustomizeIndexHandler(BaseApiHandler):
200,
{
"status": "ok",
"data": self.controller.roles.get_all_role_ids()
if get_only_ids
else [model_to_dict(r) for r in self.controller.roles.get_all_roles()],
"data": (
self.controller.roles.get_all_role_ids()
if get_only_ids
else [
model_to_dict(r) for r in self.controller.roles.get_all_roles()
]
),
},
)

View File

@ -36,9 +36,13 @@ class ApiCraftyConfigServerDirHandler(BaseApiHandler):
200,
{
"status": "ok",
"data": self.controller.roles.get_all_role_ids()
if get_only_ids
else [model_to_dict(r) for r in self.controller.roles.get_all_roles()],
"data": (
self.controller.roles.get_all_role_ids()
if get_only_ids
else [
model_to_dict(r) for r in self.controller.roles.get_all_roles()
]
),
},
)

View File

@ -87,9 +87,13 @@ class ApiRolesIndexHandler(BaseApiHandler):
200,
{
"status": "ok",
"data": self.controller.roles.get_all_role_ids()
if get_only_ids
else [model_to_dict(r) for r in self.controller.roles.get_all_roles()],
"data": (
self.controller.roles.get_all_role_ids()
if get_only_ids
else [
model_to_dict(r) for r in self.controller.roles.get_all_roles()
]
),
},
)

View File

@ -25,8 +25,12 @@ class ApiRolesRoleServersHandler(BaseApiHandler):
200,
{
"status": "ok",
"data": PermissionsServers.get_server_ids_from_role(role_id)
if get_only_ids
else self.controller.roles.get_server_ids_and_perms_from_role(role_id),
"data": (
PermissionsServers.get_server_ids_from_role(role_id)
if get_only_ids
else self.controller.roles.get_server_ids_and_perms_from_role(
role_id
)
),
},
)

View File

@ -723,9 +723,7 @@ class ApiServersIndexHandler(BaseApiHandler):
405, {"status": "error", "error": "DATA CONSTRAINT FAILED"}
)
return
new_server_id, new_server_uuid = self.controller.create_api_server(
data, user["user_id"]
)
new_server_id = self.controller.create_api_server(data, user["user_id"])
self.controller.servers.stats.record_stats()
@ -734,7 +732,7 @@ class ApiServersIndexHandler(BaseApiHandler):
(
f"created server {data['name']}"
f" (ID: {new_server_id})"
f" (UUID: {new_server_uuid})"
f" (UUID: {new_server_id})"
),
server_id=new_server_id,
source_ip=self.get_remote_ip(),
@ -746,7 +744,7 @@ class ApiServersIndexHandler(BaseApiHandler):
"status": "ok",
"data": {
"new_server_id": str(new_server_id),
"new_server_uuid": new_server_uuid,
"new_server_uuid": new_server_id,
},
},
)

View File

@ -3,7 +3,6 @@ import os
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.models.servers import Servers
from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.helpers import Helpers
from app.classes.web.base_api_handler import BaseApiHandler
@ -30,7 +29,15 @@ class ApiServersServerActionHandler(BaseApiHandler):
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if action == "clone_server":
return self._clone_server(server_id, auth_data[4]["user_id"])
if (
self.controller.crafty_perms.can_create_server(auth_data[4]["user_id"])
or auth_data[4]["superuser"]
):
self._clone_server(server_id, auth_data[4]["user_id"])
return self.finish_json(200, {"status": "ok"})
return self.finish_json(
200, {"status": "error", "error": "SERVER_LIMIT_REACHED"}
)
if action == "eula":
return self._agree_eula(server_id, auth_data[4]["user_id"])
@ -60,10 +67,20 @@ class ApiServersServerActionHandler(BaseApiHandler):
name_counter += 1
new_server_name = server_data.get("server_name") + f" (Copy {name_counter})"
new_server_uuid = Helpers.create_uuid()
while os.path.exists(os.path.join(self.helper.servers_dir, new_server_uuid)):
new_server_uuid = Helpers.create_uuid()
new_server_path = os.path.join(self.helper.servers_dir, new_server_uuid)
new_server_id = self.controller.servers.create_server(
new_server_name,
None,
"",
None,
server_data.get("executable"),
None,
server_data.get("stop_command"),
server_data.get("type"),
user_id,
server_data.get("server_port"),
)
new_server_path = os.path.join(self.helper.servers_dir, new_server_id)
self.controller.management.add_to_audit_log(
user_id,
@ -81,19 +98,19 @@ class ApiServersServerActionHandler(BaseApiHandler):
self.helper.get_os_understandable_path(server_data.get("log_path"))
)
new_server_id = self.controller.servers.create_server(
new_server_name,
new_server_uuid,
new_server_path,
"",
new_server_command,
server_data.get("executable"),
new_server_log_file,
server_data.get("stop_command"),
server_data.get("type"),
user_id,
server_data.get("server_port"),
)
server: Servers = self.controller.servers.get_server_obj(new_server_id)
server.path = new_server_path
server.log_path = new_server_log_file
server.execution_command = new_server_command
self.controller.servers.update_server(server)
for role in self.controller.server_perms.get_server_roles(server_id):
mask = self.controller.server_perms.get_permissions_mask(
role.role_id, server_id
)
self.controller.server_perms.add_role_server(
new_server_id, role.role_id, mask
)
self.controller.servers.init_all_servers()

View File

@ -145,7 +145,7 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
new_server_id = new_server
new_server = self.controller.servers.get_server_data(new_server)
self.controller.rename_backup_dir(
server_id, new_server_id, new_server["server_uuid"]
server_id, new_server_id, new_server["server_id"]
)
# preserve current schedules
for schedule in self.controller.management.get_schedules_by_server(

View File

@ -176,7 +176,7 @@ class ApiServersServerIndexHandler(BaseApiHandler):
self.tasks_manager.remove_all_server_tasks(server_id)
failed = False
for item in self.controller.servers.failed_servers[:]:
if item["server_id"] == int(server_id):
if item["server_id"] == server_id:
self.controller.servers.failed_servers.remove(item)
failed = True

View File

@ -17,7 +17,7 @@ def metrics_handlers(handler_args):
handler_args,
),
(
r"/metrics/servers/([0-9]+)/?",
r"/metrics/servers/([a-z0-9-]+)/?",
ApiOpenMetricsServersHandler,
handler_args,
),

View File

@ -118,15 +118,17 @@ class ServerHandler(BaseHandler):
"lang_page": Helpers.get_lang_page(
self.controller.users.get_user_lang_by_id(exec_user["user_id"])
),
"api_key": {
"name": api_key.name,
"created": api_key.created,
"server_permissions": api_key.server_permissions,
"crafty_permissions": api_key.crafty_permissions,
"superuser": api_key.superuser,
}
if api_key is not None
else None,
"api_key": (
{
"name": api_key.name,
"created": api_key.created,
"server_permissions": api_key.server_permissions,
"crafty_permissions": api_key.crafty_permissions,
"superuser": api_key.superuser,
}
if api_key is not None
else None
),
"superuser": superuser,
}

View File

@ -1,5 +1,5 @@
{
"major": 4,
"minor": 2,
"sub": 4
"minor": 3,
"sub": 0
}

View File

@ -598,26 +598,30 @@
</script>
<script>
function send_command(server_id, command) {
async function send_command(server_id, command) {
/* this getCookie function is in base.html */
const token = getCookie("_xsrf");
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: `/api/v2/servers/${server_id}/action/${command}`,
success: function (data) {
console.log("got response:");
console.log(data);
if (command === "clone_server" && data.status === "ok") {
window.location.reload();
}
/*setTimeout(function () {
if (command != 'start_server') {
location.reload();
}
}, 10000);*/
}
let res = await fetch(`/api/v2/servers/${server_id}/action/${command}`, {
method: 'POST',
headers: {
'token': token,
},
});
let responseData = await res.json();
if (responseData.status === "ok") {
if (command === "clone_server"){
window.location.reload()
}
console.log("Command received successfully")
} else {
setTimeout(function(){
$('.modal').modal('hide');
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}, 2000)
}
}

View File

@ -17,7 +17,7 @@
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
data['server_stats']['server_id']['server_name'] }}
<br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
<small>UUID: {{ data['server_stats']['server_id']['server_id'] }}</small>
</h4>
</div>
</div>

View File

@ -17,7 +17,7 @@
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
data['server_stats']['server_id']['server_name'] }}
<br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
<small>UUID: {{ data['server_stats']['server_id']['server_id'] }}</small>
</h4>
</div>
</div>
@ -454,7 +454,7 @@
});
}
});
try {
if ($('#backup_path').val() == '') {
console.log('true')

View File

@ -17,7 +17,7 @@
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
data['server_stats']['server_id']['server_name'] }}
<br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
<small>UUID: {{ data['server_stats']['server_id']['server_id'] }}</small>
</h4>
</div>
</div>

View File

@ -17,7 +17,7 @@
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
data['server_stats']['server_id']['server_name'] }}
<br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
<small>UUID: {{ data['server_stats']['server_id']['server_id'] }}</small>
</h4>
</div>
</div>

View File

@ -17,7 +17,7 @@
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
data['server_stats']['server_id']['server_name'] }}
<br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
<small>UUID: {{ data['server_stats']['server_id']['server_id'] }}</small>
</h4>
</div>
</div>

View File

@ -17,7 +17,7 @@
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
data['server_stats']['server_id']['server_name'] }}
<br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
<small>UUID: {{ data['server_stats']['server_id']['server_id'] }}</small>
</h4>
</div>
</div>

View File

@ -17,7 +17,7 @@
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
data['server_stats']['server_id']['server_name'] }}
<br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
<small>UUID: {{ data['server_stats']['server_id']['server_id'] }}</small>
</h4>
</div>
</div>
@ -235,7 +235,7 @@
if (key != "start_time" && key != "cron_string" && key != "interval_type") {
if (typeof value == "boolean") {
return value
}
}
console.log(key)
if (key === "interval" && value === ""){
return 0;
@ -298,7 +298,7 @@
title: responseData.status,
message: responseData.error
});
}
}
});
$("#schedule_form").on("submit", async function (e) {
@ -332,7 +332,7 @@
method: 'PATCH',
headers: {
'X-XSRFToken': token,
"Content-Type": "application/json",
"Content-Type": "application/json",
},
body: formDataJsonString,
});
@ -345,7 +345,7 @@
title: responseData.error,
message: responseData.error_data
});
}
}
});
});

View File

@ -17,7 +17,7 @@
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
data['server_stats']['server_id']['server_name'] }}
<br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
<small>UUID: {{ data['server_stats']['server_id']['server_id'] }}</small>
</h4>
</div>
</div>

View File

@ -17,7 +17,7 @@
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
data['server_stats']['server_id']['server_name'] }}
<br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
<small>UUID: {{ data['server_stats']['server_id']['server_id'] }}</small>
</h4>
</div>
</div>

View File

@ -17,7 +17,7 @@
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
data['server_stats']['server_id']['server_name'] }}
<br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
<small>UUID: {{ data['server_stats']['server_id']['server_id'] }}</small>
</h4>
</div>
</div>
@ -161,7 +161,7 @@
if (key != "start_time" && key != "cron_string" && key != "interval_type") {
if (typeof value == "boolean") {
return value
}
}
console.log(key)
if (key === "interval" && value === ""){
return 0;
@ -221,7 +221,7 @@
title: responseData.status,
message: responseData.error
});
}
}
});
$("#webhook_form").on("submit", async function (e) {
@ -249,7 +249,7 @@
method: 'PATCH',
headers: {
'X-XSRFToken': token,
"Content-Type": "application/json",
"Content-Type": "application/json",
},
body: formDataJsonString,
});
@ -262,15 +262,15 @@
title: responseData.status,
message: responseData.error
});
}
}
});
});
function hexToDiscordInt(hexColor) {
// Remove the hash at the start if it's there
const sanitizedHex = hexColor.startsWith('#') ? hexColor.slice(1) : hexColor;
// Convert the hex to an integer
return parseInt(sanitizedHex, 16);
}

View File

@ -17,7 +17,7 @@
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
data['server_stats']['server_id']['server_name'] }}
<br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
<small>UUID: {{ data['server_stats']['server_id']['server_id'] }}</small>
</h4>
</div>
</div>

View File

@ -77,55 +77,49 @@
box-shadow: 0 12px 16px 0 hsla(0, 0%, 0%, 0.4);
}
</style>
{% if data['query'] %}
<form action="/login?{{ data['query'] }}" method="post">
{% else %}
<form action="/login" method="post">
{% end %}
{% raw xsrf_form_html() %}
<div class="form-group">
<label class="label">{{ translate('login', 'username', data['lang']) }}</label>
<div class="input-group">
<input type="text" class="form-control login-text-input login-input"
placeholder="{{ translate('login', 'username', data['lang']) }}" name="username" id="username"
required="true">
</div>
<form id="login-form" data-query="{{ data.get('query', None) }}">
{% raw xsrf_form_html() %}
<div class="form-group">
<label class="label">{{ translate('login', 'username', data['lang']) }}</label>
<div class="input-group">
<input type="text" class="form-control login-text-input login-input"
placeholder="{{ translate('login', 'username', data['lang']) }}" name="username" id="username"
required="true">
</div>
<div class="form-group">
<label class="label">{{ translate('login', 'password', data['lang']) }}</label>
<div class="input-group">
<input type="password" class="form-control login-text-input login-input"
placeholder="{{ translate('login', 'password', data['lang']) }}" name="password" id="password"
required="true">
</div>
</div>
<div class="form-group">
<label class="label">{{ translate('login', 'password', data['lang']) }}</label>
<div class="input-group">
<input type="password" class="form-control login-text-input login-input"
placeholder="{{ translate('login', 'password', data['lang']) }}" name="password" id="password"
required="true">
</div>
<div class="form-group">
<button class="login-input btn btn-primary submit-btn btn-block">{{ translate('login', 'login',
data['lang']) }}</button>
</div>
{% if error_msg is not None %}
<fieldset style="color: red; text-align: center;">
<span>{{error_msg}}</span>
</fieldset>
{% end %}
<div class="form-group d-flex justify-content-between">
<div class="form-check form-check-flat mt-0">
&nbsp;
</div>
<button onclick="resetPass()" id="#resetPass" form="" class="btn btn-outline-primary btn-sm forgot-password ">{{ translate('login', 'forgotPassword',
data['lang']) }}</button>
</div>
<div class="form-group">
<button class="login-input btn btn-primary submit-btn btn-block">{{ translate('login', 'login',
data['lang']) }}</button>
</div>
<fieldset id="error-field" style="color: red; text-align: center;">
</fieldset>
<div class="form-group d-flex justify-content-between">
<div class="form-check form-check-flat mt-0">
&nbsp;
</div>
<button onclick="resetPass()" id="#resetPass" form=""
class="btn btn-outline-primary btn-sm forgot-password ">{{ translate('login', 'forgotPassword',
data['lang']) }}</button>
</div>
<div class="text-block text-center my-3">
<span class="text-small font-weight-semibold"><a href="https://craftycontrol.com/">Crafty Control
{{data['version'] }}</a> </span>
</div>
<div class="text-block text-center my-3">
<a href="/status" class="text-small forgot-password">{{ translate('login', 'viewStatus',
data['lang']) }}</a>
</div>
</form>
<div class="text-block text-center my-3">
<span class="text-small font-weight-semibold"><a href="https://craftycontrol.com/">Crafty Control
{{data['version'] }}</a> </span>
</div>
<div class="text-block text-center my-3">
<a href="/status" class="text-small forgot-password">{{ translate('login', 'viewStatus',
data['lang']) }}</a>
</div>
</form>
</div>
@ -155,13 +149,13 @@
document.getElementById('login-form-background').style.background = 'rgb(34, 36, 55, ' + (opacity / 100) + ')';
//Register Service worker for mobile app
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/static/assets/js/shared/service-worker.js', {scope: '/'})
navigator.serviceWorker.register('/static/assets/js/shared/service-worker.js', { scope: '/' })
.then(function (registration) {
console.log('Service Worker Registered');
});
}
});
async function resetPass(){
async function resetPass() {
let res = await fetch(`/api/v2/crafty/resetPass/`, {
method: 'GET',
});
@ -170,7 +164,38 @@
bootbox.alert(responseData.data)
}
$("#login-form").on("submit", async function (e) {
e.preventDefault();
let loginForm = document.getElementById("login-form");
let formData = new FormData(loginForm);
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
console.log(formDataObject)
let res = await fetch(`/login`, {
method: 'POST',
headers: {
'X-XSRFToken': formDataObject._xsrf,
"Content-Type": "application/json"
},
body: JSON.stringify({
"username": formDataObject.username,
"password": formDataObject.password
}),
});
let responseData = await res.json();
if (responseData.status === "ok") {
console.log("OK")
if ($("#login-form").data("query")) {
location.href = `${$("#login-form").data("query")}`;
} else {
location.href = `/panel/dashboard`
}
} else {
$("#error-field").html(responseData.error);
}
});
</script>
<style>
.modal-content {

View File

@ -556,7 +556,7 @@
xmlHttpRequest.addEventListener('load', (event) => {
if (event.target.responseText == 'success') {
console.log('Upload for file', file.name, 'was successful!')
document.getElementById("upload_input").innerHTML = `<div class="card-header header-sm d-flex justify-content-between align-items-center" style="width: 100%;"><input value=${fileName} type="text" id="file-uploaded" disabled></input> 🔒</div>`;
$("#upload_input").html(`<div class="card-header header-sm d-flex justify-content-between align-items-center" style="width: 100%;"><input value="${decodeURIComponent(fileName)}" type="text" id="file-uploaded" disabled></input> 🔒</div>`);
document.getElementById("lower_half").style.visibility = "visible";
}
else {

View File

@ -881,7 +881,7 @@
xmlHttpRequest.addEventListener('load', (event) => {
if (event.target.responseText == 'success') {
console.log('Upload for file', file.name, 'was successful!')
document.getElementById("upload_input").innerHTML = `<div class="card-header header-sm d-flex justify-content-between align-items-center" style="width: 100%;"><input value=${fileName} type="text" id="file-uploaded" disabled></input> 🔒</div>`;
$("#upload_input").html(`<div class="card-header header-sm d-flex justify-content-between align-items-center" style="width: 100%;"><input value="${fileName}" type="text" id="file-uploaded" disabled></input> 🔒</div>`);
document.getElementById("lower_half").style.visibility = "visible";
document.getElementById("lower_half").hidden = false;
}

View File

@ -0,0 +1,244 @@
import datetime
import uuid
import peewee
import logging
from app.classes.shared.console import Console
from app.classes.shared.migration import Migrator, MigrateHistory
from app.classes.models.management import (
AuditLog,
Webhooks,
Schedules,
Backups,
)
from app.classes.models.server_permissions import RoleServers
logger = logging.getLogger(__name__)
def migrate(migrator: Migrator, database, **kwargs):
"""
Write your migrations here.
"""
db = database
# **********************************************************************************
# Servers New Model from Old (easier to migrate without dunmping Database)
# **********************************************************************************
class Servers(peewee.Model):
server_id = peewee.CharField(primary_key=True, default=str(uuid.uuid4()))
created = peewee.DateTimeField(default=datetime.datetime.now)
server_uuid = peewee.CharField(default="", index=True)
server_name = peewee.CharField(default="Server", index=True)
path = peewee.CharField(default="")
backup_path = peewee.CharField(default="")
executable = peewee.CharField(default="")
log_path = peewee.CharField(default="")
execution_command = peewee.CharField(default="")
auto_start = peewee.BooleanField(default=0)
auto_start_delay = peewee.IntegerField(default=10)
crash_detection = peewee.BooleanField(default=0)
stop_command = peewee.CharField(default="stop")
executable_update_url = peewee.CharField(default="")
server_ip = peewee.CharField(default="127.0.0.1")
server_port = peewee.IntegerField(default=25565)
logs_delete_after = peewee.IntegerField(default=0)
type = peewee.CharField(default="minecraft-java")
show_status = peewee.BooleanField(default=1)
created_by = peewee.IntegerField(default=-100)
shutdown_timeout = peewee.IntegerField(default=60)
ignored_exits = peewee.CharField(default="0")
class Meta:
table_name = "servers"
database = db
try:
logger.info("Migrating Data from Int to UUID (Type Change)")
Console.info("Migrating Data from Int to UUID (Type Change)")
# Changes on Server Table
migrator.alter_column_type(
Servers,
"server_id",
peewee.CharField(primary_key=True, default=str(uuid.uuid4())),
)
# Changes on Audit Log Table
migrator.alter_column_type(
AuditLog,
"server_id",
peewee.ForeignKeyField(
Servers,
backref="audit_server",
null=True,
field=peewee.CharField(primary_key=True, default=str(uuid.uuid4())),
),
)
# Changes on Webhook Table
migrator.alter_column_type(
Webhooks,
"server_id",
peewee.ForeignKeyField(
Servers,
backref="webhook_server",
null=True,
field=peewee.CharField(primary_key=True, default=str(uuid.uuid4())),
),
)
migrator.run()
logger.info("Migrating Data from Int to UUID (Type Change) : SUCCESS")
Console.info("Migrating Data from Int to UUID (Type Change) : SUCCESS")
except Exception as ex:
logger.error("Error while migrating Data from Int to UUID (Type Change)")
logger.error(ex)
Console.error("Error while migrating Data from Int to UUID (Type Change)")
Console.error(ex)
last_migration = MigrateHistory.get_by_id(MigrateHistory.select().count())
last_migration.delete()
return
try:
logger.info("Migrating Data from Int to UUID (Foreign Keys)")
Console.info("Migrating Data from Int to UUID (Foreign Keys)")
# Changes on Audit Log Table
for audit_log in AuditLog.select():
old_server_id = audit_log.server_id_id
if old_server_id == "0" or old_server_id is None:
server_uuid = None
else:
try:
server = Servers.get_by_id(old_server_id)
server_uuid = server.server_uuid
except:
server_uuid = old_server_id
AuditLog.update(server_id=server_uuid).where(
AuditLog.audit_id == audit_log.audit_id
).execute()
# Changes on Webhooks Log Table
for webhook in Webhooks.select():
old_server_id = webhook.server_id_id
try:
server = Servers.get_by_id(old_server_id)
server_uuid = server.server_uuid
except:
server_uuid = old_server_id
Webhooks.update(server_id=server_uuid).where(
Webhooks.id == webhook.id
).execute()
# Changes on Schedules Log Table
for schedule in Schedules.select():
old_server_id = schedule.server_id_id
try:
server = Servers.get_by_id(old_server_id)
server_uuid = server.server_uuid
except:
server_uuid = old_server_id
Schedules.update(server_id=server_uuid).where(
Schedules.schedule_id == schedule.schedule_id
).execute()
# Changes on Backups Log Table
for backup in Backups.select():
old_server_id = backup.server_id_id
try:
server = Servers.get_by_id(old_server_id)
server_uuid = server.server_uuid
except:
server_uuid = old_server_id
Backups.update(server_id=server_uuid).where(
Backups.server_id == old_server_id
).execute()
# Changes on RoleServers Log Table
for role_servers in RoleServers.select():
old_server_id = role_servers.server_id_id
try:
server = Servers.get_by_id(old_server_id)
server_uuid = server.server_uuid
except:
server_uuid = old_server_id
RoleServers.update(server_id=server_uuid).where(
RoleServers.role_id == role_servers.id
and RoleServers.server_id == old_server_id
).execute()
logger.info("Migrating Data from Int to UUID (Foreign Keys) : SUCCESS")
Console.info("Migrating Data from Int to UUID (Foreign Keys) : SUCCESS")
except Exception as ex:
logger.error("Error while migrating Data from Int to UUID (Foreign Keys)")
logger.error(ex)
Console.error("Error while migrating Data from Int to UUID (Foreign Keys)")
Console.error(ex)
last_migration = MigrateHistory.get_by_id(MigrateHistory.select().count())
last_migration.delete()
return
try:
logger.info("Migrating Data from Int to UUID (Primary Keys)")
Console.info("Migrating Data from Int to UUID (Primary Keys)")
# Migrating servers from the old id type to the new one
for server in Servers.select():
Servers.update(server_id=server.server_uuid).where(
Servers.server_id == server.server_id
).execute()
logger.info("Migrating Data from Int to UUID (Primary Keys) : SUCCESS")
Console.info("Migrating Data from Int to UUID (Primary Keys) : SUCCESS")
except Exception as ex:
logger.error("Error while migrating Data from Int to UUID (Primary Keys)")
logger.error(ex)
Console.error("Error while migrating Data from Int to UUID (Primary Keys)")
Console.error(ex)
last_migration = MigrateHistory.get_by_id(MigrateHistory.select().count())
last_migration.delete()
return
# Changes on Server Table
logger.info("Migrating Data from Int to UUID (Removing UUID Field from Servers)")
Console.info("Migrating Data from Int to UUID (Removing UUID Field from Servers)")
migrator.drop_columns("servers", ["server_uuid"])
migrator.run()
logger.info(
"Migrating Data from Int to UUID (Removing UUID Field from Servers) : SUCCESS"
)
Console.info(
"Migrating Data from Int to UUID (Removing UUID Field from Servers) : SUCCESS"
)
return
def rollback(migrator: Migrator, database, **kwargs):
"""
Write your rollback migrations here.
"""
db = database
# Changes on Server Table
migrator.alter_column_type(
"servers",
"server_id",
peewee.AutoField(),
)
# Changes on Audit Log Table
migrator.alter_column_type(
AuditLog,
"server_id",
peewee.IntegerField(default=None, index=True),
)
# Changes on Webhook Table
migrator.alter_column_type(
Webhooks,
"server_id",
peewee.IntegerField(null=True),
)

View File

@ -215,7 +215,10 @@
"version": "Version"
},
"login": {
"defaultPath": "Der eingegebene Text ist der Pfad zum Passwort, nicht das Passwort selbst. Das Standartpasswort kann unter diesen Pfad eingesehen werden.",
"disabled": "Account gesperrt. Für weitere Informationen den Serveradministrator kontaktieren",
"forgotPassword": "Passwort vergessen",
"incorrect": "Benutzername oder Passwort falsch",
"login": "Einloggen",
"password": "Passwort",
"username": "Nutzername",

View File

@ -215,7 +215,10 @@
"version": "Version"
},
"login": {
"defaultPath": "The password you entered is the default credential path, not the password. Please find the default password in that location.",
"disabled": "User account disabled. Please contact your system administrator for more info.",
"forgotPassword": "Forgot Password",
"incorrect": "Incorrect username or password",
"login": "Log In",
"password": "Password",
"username": "Username",

View File

@ -111,6 +111,7 @@
"starting": "Inicio-retrasado",
"status": "Estado",
"stop": "Detener",
"storage": "Almacenamiento",
"version": "Versión",
"welcome": "Bienvenido a Crafty Controller"
},
@ -214,7 +215,10 @@
"version": "Versión"
},
"login": {
"defaultPath": "La contraseña introducida es la ruta default de las credenciales, no la contraseña. Busca la contraseña accediendo a la carpeta de la ruta",
"disabled": "Cuenta del usuario desactivada. Porfavor contacta al administrador para mas informacion.",
"forgotPassword": "Olvidé mi contraseña",
"incorrect": "El nombre de usuario o contraseña es incorrecto",
"login": "Iniciar Sesión",
"password": "Contraseña",
"username": "Usuario",
@ -326,6 +330,7 @@
"bePatientDeleteFiles": "Tenga paciencia mientras eliminamos su servidor del panel de Crafty y eliminamos todos los archivos. Esta pantalla se cerrará en unos momentos.",
"bePatientUpdate": "Tenga paciencia mientras actualizamos el servidor. El tiempo de descarga puede variar según la velocidad del Internet...<br /> Esta pantalla se actualizará en unos momentos.",
"cancel": "Cancelar",
"countPlayers": "Incluir el servidor en la cuenta total de jugadores",
"crashTime": "Tiempo de espera por crasheo",
"crashTimeDesc": "¿Cuanto tiempo esperar para considerar el servidor como crasheado?",
"deleteFilesQuestion": "¿Eliminar archivos del servidor del host?",
@ -510,6 +515,7 @@
"cpuUsage": "Uso de CPU",
"description": "Descripción",
"errorCalculatingUptime": "Error calculando tiempo de actividad",
"loadingMotd": "Cargando MOTD",
"memUsage": "Uso de memoria",
"offline": "Desconectado",
"online": "En línea",
@ -577,6 +583,7 @@
"serverUpload": "Subir servidor comprimido",
"serverVersion": "Versión del servidor",
"sizeInGB": "Tamaño en GB",
"unsupported": "Versiones de Minecraft inferiores a la 1.8 no estan soportadas por Crafty. Es posible instalarlas. Resultados pueden variar.",
"uploadButton": "Subir",
"uploadZip": "Subir archivo Zip para importar servidor",
"zipPath": "Ruta del servidor"
@ -591,6 +598,15 @@
"newServer": "Crear nuevo Servidor",
"servers": "Servidores"
},
"startup": {
"almost": "Terminando. Espera un momento...",
"internals": "Configurando e inicializando los componentes internos de Crafty",
"internet": "Verificando conexion a internet",
"server": "Inicializando ",
"serverInit": "Inicializando Servidores",
"starting": "Crafty esta iniciando...",
"tasks": "Iniciando el programador de tareas"
},
"userConfig": {
"apiKey": "Claves API",
"auth": "¿Autorizado? ",

View File

@ -215,7 +215,10 @@
"version": "Version"
},
"login": {
"defaultPath": "Ce que tu as renseigné n'est pas le mot de passe, mais le chemin du fichier où le trouver.",
"disabled": "Ce compte est désactivé. Merci de contacter l'administrateur de ton serveur pour plus d'informations.",
"forgotPassword": "Mot de Passe Oublié",
"incorrect": "Identifiant et/ou mot de passe incorrect.",
"login": "Connexion",
"password": "Mot de Passe",
"username": "Nom d'Utilisateur",

View File

@ -215,7 +215,10 @@
"version": "גרסה"
},
"login": {
"defaultPath": "הסיסמה שהזנת היא נתיב האישורים המוגדר כברירת מחדל, ולא הסיסמה עצמה. אנא מצא את הסיסמה המוגדרת כברירת מחדל במיקום זה.",
"disabled": "חשבון המשתמש מושבת. אנא פנה למנהל המערכת שלך לקבלת מידע נוסף.",
"forgotPassword": "שכחתי סיסמה",
"incorrect": "שם משתמש או סיסמה שגויים",
"login": "התחברות",
"password": "סיסמה",
"username": "שם משתמש",

View File

@ -215,7 +215,10 @@
"version": "VERSHUN"
},
"login": {
"defaultPath": "Silleh hooman, dat iz da dafault secret path, not da passwurd. Plz find da default passwurd in dat spot.",
"disabled": "User account no play. Plz boop ur system hooman for moar infoz.",
"forgotPassword": "FORGWOTS YOUR SEEKRET",
"incorrect": "U gotz wrong name or passwurd",
"login": "WOG INZ",
"password": "SEEKRET",
"username": "USERNAEM",

View File

@ -216,7 +216,10 @@
"version": "Versija"
},
"login": {
"defaultPath": "Parole ko ievadijāt ir celš uz noklusētās paroles vietu, nevis noklusētā parole. Lūdzu apskatiet noklusēto paroli šajā vietā.",
"disabled": "Lietotāja konts atspējots. Lūdzu sazinieties ar savu sistēmas administratoru priekš papildus informācijas.",
"forgotPassword": "Aizmirsu Paroli",
"incorrect": "Nepareizs lietotājvārds vai parole",
"login": "Ieiet",
"password": "Parole",
"username": "Lietotājvārds",

View File

@ -215,7 +215,10 @@
"version": "Versie"
},
"login": {
"defaultPath": "Het ingevoerde wachtwoord is het pad naar de standaardreferentie, niet het wachtwoord zelf. Raadpleeg de standaardwachtwoord op de aangegeven locatie.",
"disabled": "Gebruikersaccount uitgeschakeld. Neem voor meer informatie contact op met uw systeembeheerder.",
"forgotPassword": "Wachtwoord vergeten",
"incorrect": "Verkeerde gebruikersnaam of wachtwoord",
"login": "Log In",
"password": "Wachtwoord",
"username": "gebruikersnaam",

View File

@ -215,7 +215,10 @@
"version": "Wersja"
},
"login": {
"defaultPath": "Hasło które wprowadziłeś jest podstawową ścieżką w której przechowywane są dane logowania. Znajdź podstawowe hasło w tej lokalizacji.",
"disabled": "Konto tego użytkownika jest wyłączone. Skontaktuj się z administratorem by uzyskać więcej informacji.",
"forgotPassword": "Zapomniałem hasła",
"incorrect": "Niepoprawny login lub hasło/Niepoprawna nazwa użytkownika lub hasło",
"login": "Zaloguj się",
"password": "Hasło",
"username": "Nazwa użytkownika",

View File

@ -215,7 +215,10 @@
"version": "เวอร์ชั่น"
},
"login": {
"defaultPath": "รหัสผ่านที่คุณกรอกคือเส้นทางข้อมูลเริ่มต้น ไม่ใช่รหัสผ่าน กรุณาค้นหารหัสผ่านเริ่มต้นในตำแหน่งนั้น",
"disabled": "บัญชีผู้ใช้ถูกปิดใช้งาน กรุณาติดต่อผู้ดูแลระบบของคุณสำหรับข้อมูลเพิ่มเติม",
"forgotPassword": "ลืมรหัสผ่าน",
"incorrect": "ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง",
"login": "เข้าสู่ระบบ",
"password": "รหัสผ่าน",
"username": "ชื่อผู้ใช้",

View File

@ -215,7 +215,10 @@
"version": "Sürüm"
},
"login": {
"defaultPath": "Girdiğiniz şifre varsayılan şifrenin konumudur, varsayılan şifre değil. Lütfen o konumda bulunan varsayılan şifreyi bulunuz.",
"disabled": "Bu kullanıcı hesabı engellenmiştir. Daha fazla bilgi için lütfen sunucu yöneticiniz ile konuşunuz.",
"forgotPassword": "Şifremi Unuttum",
"incorrect": "Kullanıcı adınız veya şifreniz yanlış.",
"login": "Oturum Aç",
"password": "Şifre",
"username": "Kullanıcı Adı",

View File

@ -85,7 +85,7 @@
"cpuCurFreq": "Швидкість CPU",
"cpuMaxFreq": "Максимальна швидкість CPU",
"cpuUsage": "Використання CPU",
"crashed": "Аварійне завершення",
"crashed": "Краш",
"dashboard": "Панель",
"delay-explained": "Служба/агент нещодавно запущено та затримує запуск серверів minecraft",
"host": "Хост",
@ -215,7 +215,10 @@
"version": "Версія"
},
"login": {
"defaultPath": "Пароль, який ви ввели, є шляхом до облікових даних за умовчанням, а не паролем. Будь ласка, знайдіть стандартний пароль у цьому місці.",
"disabled": "Користувача вимкнено. Зверніться до вашого системного адміністратора за допомогою.",
"forgotPassword": "Забули пароль",
"incorrect": "Неправильний логін або пароль",
"login": "Вхід",
"password": "Пароль",
"username": "Логін",
@ -351,7 +354,7 @@
"sendingRequest": "Надсилання вашого запиту...",
"serverAutoStart": "Сервер Авто-старт",
"serverAutostartDelay": "Сервер Авто-старт затримка",
"serverAutostartDelayDesc": "Затримка Авто-старту сервера (Якщо увімкнуто раніше)",
"serverAutostartDelayDesc": "Затримка Авто-старту сервера (Після запуску Crafty))",
"serverCrashDetection": "Детектор крашу сервера",
"serverExecutable": "Виконуваний файл Серверу",
"serverExecutableDesc": "Це виконуваний файл для запуску сервера",
@ -369,7 +372,7 @@
"serverPortDesc": "Цей порт призначений для статистики Crafty",
"serverStopCommand": "Команда зупинки сервера",
"serverStopCommandDesc": "Команда яка буде надсилатись, щоб зупинити сервер",
"showStatus": "Показувати на публічній сторінці статус",
"showStatus": "Показувати статус на публічній сторінці",
"shutdownTimeout": "Час відклику зупинки",
"statsHint1": "Цей порт на якому працює сервер. Це потрібно лиш для того щоб Crafty міг виводити статистику для цього сервера.",
"statsHint2": "Це не змінює порт вашого сервера. Ви мусите власноруч змінити налаштування в server.properties або іншому конфігураційному файлі.",
@ -406,7 +409,7 @@
"logs": "Логи",
"metrics": "Графік",
"playerControls": "Керування Гравцями",
"reset": "Повернутись нагору",
"reset": "Вниз",
"schedule": "Розклад",
"serverDetails": "Деталі сервера",
"terminal": "Термінал"

View File

@ -215,7 +215,10 @@
"version": "版本"
},
"login": {
"defaultPath": "您输入的密码是默认凭据的路径,不是其中的密码。请在此路径中找到默认密码。",
"disabled": "用户账号已禁用。请联系您的系统管理员以了解更多信息。",
"forgotPassword": "忘记密码",
"incorrect": "用户名或密码错误",
"login": "登录",
"password": "密码",
"username": "用户名",

View File

@ -4,13 +4,13 @@ argon2-cffi==23.1.0
cached_property==1.5.2
colorama==0.4.6
croniter==1.4.1
cryptography==41.0.7
cryptography==42.0.4
libgravatar==1.0.4
nh3==0.2.14
packaging==23.2
peewee==3.13
psutil==5.9.5
pyOpenSSL==23.3.0
pyOpenSSL==24.0.0
pyjwt==2.8.0
PyYAML==6.0.1
requests==2.31.0

View File

@ -3,7 +3,7 @@ sonar.organization=crafty-controller
# This is the name and version displayed in the SonarCloud UI.
sonar.projectName=Crafty 4
sonar.projectVersion=4.2.4
sonar.projectVersion=4.3.0
sonar.python.version=3.9, 3.10, 3.11
sonar.exclusions=app/migrations/**, app/frontend/static/assets/vendors/**