Merge branch 'dev' into feature/steamcmd

This commit is contained in:
Andrew 2023-09-07 20:51:09 -04:00
commit 8a2012929e
69 changed files with 4860 additions and 4393 deletions

View File

@ -1,11 +1,18 @@
# Changelog # Changelog
## --- [4.1.4] - 2023/TBD ## --- [4.2.0] - 2023/TBD
### New features ### New features
TBD - Finish and Activate Arcadia notification backend ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/621))
### Bug fixes ### Bug fixes
TBD - PWA: Removed the custom offline page in favour of browser default ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/607))
- Fix hidden servers appearing visible on public mobile status page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/612))
- Correctly handle if a server returns a string instead of json data on socket ping ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/614))
- Bump tornado to resolve #269 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/623))
- Bump crypto to resolve #267 & #268 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/622))
### Refactor
- Consolidate remaining frontend functions into API V2, and remove ajax internal API ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/585))
### Tweaks ### Tweaks
TBD - Polish/Enhance display for InApp Documentation ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/613))
- Add get_users command to Crafty's console ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/620))
### Lang ### Lang
TBD TBD
<br><br> <br><br>

View File

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

View File

@ -79,8 +79,8 @@ class ManagementController:
# Audit_Log Methods # Audit_Log Methods
# ********************************************************************************** # **********************************************************************************
@staticmethod @staticmethod
def get_actity_log(): def get_activity_log():
return HelpersManagement.get_actity_log() return HelpersManagement.get_activity_log()
def add_to_audit_log(self, user_id, log_msg, server_id=None, source_ip=None): def add_to_audit_log(self, user_id, log_msg, server_id=None, source_ip=None):
return self.management_helper.add_to_audit_log( return self.management_helper.add_to_audit_log(

View File

@ -31,7 +31,7 @@ class UsersController:
for permission in PermissionsCrafty.get_permissions_list() for permission in PermissionsCrafty.get_permissions_list()
], ],
}, },
"quantity": {"type": "number", "minimum": 0}, "quantity": {"type": "number", "minimum": -1},
"enabled": {"type": "boolean"}, "enabled": {"type": "boolean"},
} }
self.user_jsonschema_props: t.Final = { self.user_jsonschema_props: t.Final = {
@ -46,7 +46,7 @@ class UsersController:
"password": { "password": {
"type": "string", "type": "string",
"maxLength": 20, "maxLength": 20,
"minLength": 4, "minLength": 6,
"examples": ["crafty"], "examples": ["crafty"],
"title": "Password", "title": "Password",
}, },
@ -73,6 +73,8 @@ class UsersController:
"examples": [False], "examples": [False],
"title": "Superuser", "title": "Superuser",
}, },
"manager": {"type": ["integer", "null"]},
"theme": {"type": "string"},
"permissions": { "permissions": {
"type": "array", "type": "array",
"items": { "items": {
@ -84,7 +86,7 @@ class UsersController:
"roles": { "roles": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string", "type": "integer",
"minLength": 1, "minLength": 1,
}, },
}, },

View File

@ -16,6 +16,12 @@ logger = logging.getLogger(__name__)
class Server: class Server:
def __init__(self, data): def __init__(self, data):
if isinstance(data, str):
logger.error(
"Failed to calculate stats. Expected object. "
f"Server returned string: {data}"
)
return
self.description = data.get("description") self.description = data.get("description")
# print(self.description) # print(self.description)
if isinstance(self.description, dict): if isinstance(self.description, dict):

View File

@ -145,7 +145,7 @@ class HelpersManagement:
# Audit_Log Methods # Audit_Log Methods
# ********************************************************************************** # **********************************************************************************
@staticmethod @staticmethod
def get_actity_log(): def get_activity_log():
query = AuditLog.select() query = AuditLog.select()
return DatabaseShortcuts.return_db_rows(query) return DatabaseShortcuts.return_db_rows(query)

View File

@ -45,6 +45,7 @@ class Users(BaseModel):
manager = IntegerField(default=None, null=True) manager = IntegerField(default=None, null=True)
pfp = CharField(default="/static/assets/images/faces-clipart/pic-3.png") pfp = CharField(default="/static/assets/images/faces-clipart/pic-3.png")
theme = CharField(default="default") theme = CharField(default="default")
cleared_notifs = CharField(default="default")
class Meta: class Meta:
table_name = "users" table_name = "users"
@ -171,6 +172,7 @@ class HelperUsers:
"roles": [], "roles": [],
"servers": [], "servers": [],
"support_logs": "", "support_logs": "",
"cleared_notifs": "",
} }
user = model_to_dict(Users.get(Users.user_id == user_id)) user = model_to_dict(Users.get(Users.user_id == user_id))

View File

@ -92,6 +92,9 @@ class MainPrompt(cmd.Cmd):
self.controller.users.update_user(user_id, {"password": new_pass}) self.controller.users.update_user(user_id, {"password": new_pass})
def do_get_users(self, _line):
Console.info(self.controller.users.get_all_usernames())
@staticmethod @staticmethod
def do_threads(_line): def do_threads(_line):
for thread in threading.enumerate(): for thread in threading.enumerate():

View File

@ -325,3 +325,12 @@ class FileHelpers:
else: else:
return "false" return "false"
return return
def unzip_server(self, zip_path, user_id):
if Helpers.check_file_perms(zip_path):
temp_dir = tempfile.mkdtemp()
with zipfile.ZipFile(zip_path, "r") as zip_ref:
# extracts archive to temp directory
zip_ref.extractall(temp_dir)
if user_id:
return temp_dir

View File

@ -580,20 +580,16 @@ class Helpers:
return version_data return version_data
@staticmethod def get_announcements(self):
def get_announcements(): data = []
data = (
'[{"id":"1","date":"Unknown",'
'"title":"Error getting Announcements",'
'"desc":"Error getting Announcements","link":""}]'
)
try: try:
response = requests.get("https://craftycontrol.com/notify.json", timeout=2) response = requests.get("https://craftycontrol.com/notify", timeout=2)
data = json.loads(response.content) data = json.loads(response.content)
except Exception as e: except Exception as e:
logger.error(f"Failed to fetch notifications with error: {e}") logger.error(f"Failed to fetch notifications with error: {e}")
if self.update_available:
data.append(self.update_available)
return data return data
def get_version_string(self): def get_version_string(self):
@ -1093,87 +1089,6 @@ class Helpers:
return data return data
def generate_tree(self, folder, output=""):
dir_list = []
unsorted_files = []
file_list = os.listdir(folder)
for item in file_list:
if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item)
elif str(item) != self.ignored_names:
unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted(
unsorted_files, key=str.casefold
)
for raw_filename in file_list:
filename = html.escape(raw_filename)
rel = os.path.join(folder, raw_filename)
dpath = os.path.join(folder, filename)
if os.path.isdir(rel):
if filename not in self.ignored_names:
output += f"""<li id="{dpath}li" class="tree-item"
data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}"
class="tree-caret tree-ctx-item tree-folder">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}"
data-name="{filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i>
{filename}
</span>
</div><li>
\n"""
else:
if filename not in self.ignored_names:
output += f"""<li id="{dpath}li"
class="d-block tree-ctx-item tree-file tree-item"
data-path="{dpath}"
data-name="{filename}"
onclick="clickOnFile(event)"><span style="margin-right: 6px;">
<i class="far fa-file"></i></span>{filename}</li>"""
return output
def generate_dir(self, folder, output=""):
dir_list = []
unsorted_files = []
file_list = os.listdir(folder)
for item in file_list:
if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item)
elif str(item) != self.ignored_names:
unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted(
unsorted_files, key=str.casefold
)
output += f"""<ul class="tree-nested d-block" id="{folder}ul">"""
for raw_filename in file_list:
filename = html.escape(raw_filename)
dpath = os.path.join(folder, filename)
rel = os.path.join(folder, raw_filename)
if os.path.isdir(rel):
if filename not in self.ignored_names:
output += f"""<li id="{dpath}li" class="tree-item"
data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}"
class="tree-caret tree-ctx-item tree-folder">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}"
data-name="{filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i>
{filename}
</span>
</div><li>"""
else:
if filename not in self.ignored_names:
output += f"""<li id="{dpath}li"
class="d-block tree-ctx-item tree-file tree-item"
data-path="{dpath}"
data-name="{filename}"
onclick="clickOnFile(event)"><span style="margin-right: 6px;">
<i class="far fa-file"></i></span>{filename}</li>"""
output += "</ul>\n"
return output
@staticmethod @staticmethod
def generate_zip_tree(folder, output=""): def generate_zip_tree(folder, output=""):
file_list = os.listdir(folder) file_list = os.listdir(folder)
@ -1217,23 +1132,6 @@ class Helpers:
</input></div><li>""" </input></div><li>"""
return output return output
def unzip_server(self, zip_path, user_id):
if Helpers.check_file_perms(zip_path):
temp_dir = tempfile.mkdtemp()
with zipfile.ZipFile(zip_path, "r") as zip_ref:
# extracts archive to temp directory
zip_ref.extractall(temp_dir)
if user_id:
self.websocket_helper.broadcast_user(
user_id, "send_temp_path", {"path": temp_dir}
)
def backup_select(self, path, user_id):
if user_id:
self.websocket_helper.broadcast_user(
user_id, "send_temp_path", {"path": path}
)
@staticmethod @staticmethod
def unzip_backup_archive(backup_path, zip_name): def unzip_backup_archive(backup_path, zip_name):
zip_path = os.path.join(backup_path, zip_name) zip_path = os.path.join(backup_path, zip_name)

View File

@ -5,6 +5,7 @@ from datetime import datetime
import platform import platform
import shutil import shutil
import time import time
import json
import logging import logging
import threading import threading
from peewee import DoesNotExist from peewee import DoesNotExist
@ -88,6 +89,17 @@ class Controller:
def set_project_root(self, root_dir): def set_project_root(self, root_dir):
self.project_root = root_dir self.project_root = root_dir
def set_config_json(self, data):
current_config = self.helper.get_all_settings()
for key in current_config:
if key in data:
current_config[key] = data[key]
keys = list(current_config.keys())
keys.sort()
sorted_data = {i: current_config[i] for i in keys}
with open(self.helper.settings_file, "w", encoding="utf-8") as f:
json.dump(sorted_data, f, indent=4)
def package_support_logs(self, exec_user): def package_support_logs(self, exec_user):
if exec_user["preparing"]: if exec_user["preparing"]:
return return
@ -304,15 +316,6 @@ class Controller:
Helpers.ensure_dir_exists(new_server_path) Helpers.ensure_dir_exists(new_server_path)
Helpers.ensure_dir_exists(backup_path) Helpers.ensure_dir_exists(backup_path)
def _copy_import_dir_files(existing_server_path):
existing_server_path = Helpers.get_os_understandable_path(
existing_server_path
)
try:
FileHelpers.copy_dir(existing_server_path, new_server_path, True)
except shutil.Error as ex:
logger.error(f"Server import failed with error: {ex}")
def _create_server_properties_if_needed(port, empty=False): def _create_server_properties_if_needed(port, empty=False):
properties_file = os.path.join(new_server_path, "server.properties") properties_file = os.path.join(new_server_path, "server.properties")
has_properties = os.path.exists(properties_file) has_properties = os.path.exists(properties_file)
@ -340,22 +343,25 @@ class Controller:
server_file = f"{create_data['type']}-{create_data['version']}.jar" server_file = f"{create_data['type']}-{create_data['version']}.jar"
# Create an EULA file # Create an EULA file
with open( if "agree_to_eula" in create_data:
os.path.join(new_server_path, "eula.txt"), "w", encoding="utf-8" with open(
) as file: os.path.join(new_server_path, "eula.txt"), "w", encoding="utf-8"
file.write( ) as file:
"eula=" + ("true" if create_data["agree_to_eula"] else "false") file.write(
) "eula="
+ ("true" if create_data["agree_to_eula"] else "false")
)
elif root_create_data["create_type"] == "import_server": elif root_create_data["create_type"] == "import_server":
_copy_import_dir_files(create_data["existing_server_path"])
server_file = create_data["jarfile"] server_file = create_data["jarfile"]
elif root_create_data["create_type"] == "import_zip": elif root_create_data["create_type"] == "import_zip":
# TODO: Copy files from the zip file to the new server directory # TODO: Copy files from the zip file to the new server directory
server_file = create_data["jarfile"] server_file = create_data["jarfile"]
raise NotImplementedError("Not yet implemented") raise NotImplementedError("Not yet implemented")
_create_server_properties_if_needed( # self.import_helper.import_java_zip_server()
create_data["server_properties_port"], if data["create_type"] == "minecraft_java":
) _create_server_properties_if_needed(
create_data["server_properties_port"],
)
min_mem = create_data["mem_min"] min_mem = create_data["mem_min"]
max_mem = create_data["mem_max"] max_mem = create_data["mem_max"]
@ -368,30 +374,72 @@ class Controller:
def _wrap_jar_if_windows(): def _wrap_jar_if_windows():
return f'"{server_file}"' if Helpers.is_os_windows() else server_file return f'"{server_file}"' if Helpers.is_os_windows() else server_file
server_command = ( if root_create_data["create_type"] == "download_jar":
f"java -Xms{_gibs_to_mibs(min_mem)}M " if Helpers.is_os_windows():
f"-Xmx{_gibs_to_mibs(max_mem)}M " # Let's check for and setup for install server commands
f"-jar {_wrap_jar_if_windows()} nogui" if create_data["type"] == "forge":
) server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f'-jar "{server_file}" --installServer'
)
else:
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f'-jar "{server_file}" nogui'
)
else:
if create_data["type"] == "forge":
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f"-jar {server_file} --installServer"
)
else:
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f"-jar {server_file} nogui"
)
else:
server_command = (
f"java -Xms{_gibs_to_mibs(min_mem)}M "
f"-Xmx{_gibs_to_mibs(max_mem)}M "
f"-jar {_wrap_jar_if_windows()} nogui"
)
elif data["create_type"] == "minecraft_bedrock": elif data["create_type"] == "minecraft_bedrock":
if root_create_data["create_type"] == "import_server": if root_create_data["create_type"] == "import_server":
existing_server_path = Helpers.get_os_understandable_path( existing_server_path = Helpers.get_os_understandable_path(
create_data["existing_server_path"] create_data["existing_server_path"]
) )
try: if Helpers.is_os_windows():
FileHelpers.copy_dir(existing_server_path, new_server_path, True) server_command = (
except shutil.Error as ex: f'"{os.path.join(new_server_path, create_data["executable"])}"'
logger.error(f"Server import failed with error: {ex}") )
else:
server_command = f"./{create_data['executable']}"
logger.debug("command: " + server_command)
server_file = create_data["executable"]
elif root_create_data["create_type"] == "import_zip": elif root_create_data["create_type"] == "import_zip":
# TODO: Copy files from the zip file to the new server directory # TODO: Copy files from the zip file to the new server directory
raise NotImplementedError("Not yet implemented") raise NotImplementedError("Not yet implemented")
else:
server_file = "bedrock_server"
if Helpers.is_os_windows():
# if this is windows we will override the linux bedrock server name.
server_file = "bedrock_server.exe"
full_jar_path = os.path.join(new_server_path, server_file)
if self.helper.is_os_windows():
server_command = f'"{full_jar_path}"'
else:
server_command = f"./{server_file}"
_create_server_properties_if_needed(0, True) _create_server_properties_if_needed(0, True)
server_command = create_data["command"] server_command = create_data.get("command", server_command)
server_file = (
"./bedrock_server" # HACK: This is a hack to make the server start
)
elif data["create_type"] == "custom": elif data["create_type"] == "custom":
# TODO: working_directory, executable_update # TODO: working_directory, executable_update
if root_create_data["create_type"] == "raw_exec": if root_create_data["create_type"] == "raw_exec":
@ -455,131 +503,85 @@ class Controller:
server_host=monitoring_host, server_host=monitoring_host,
server_type=monitoring_type, server_type=monitoring_type,
) )
if data["create_type"] == "minecraft_java":
if ( if root_create_data["create_type"] == "download_jar":
data["create_type"] == "minecraft_java" # modded update urls from server jars will only update the installer
and root_create_data["create_type"] == "download_jar" if create_data["category"] != "modded":
): server_obj = self.servers.get_server_obj(new_server_id)
# modded update urls from server jars will only update the installer url = (
if create_data["category"] != "modded": f"https://serverjars.com/api/fetchJar/{create_data['category']}"
server_obj = self.servers.get_server_obj(new_server_id) f"/{create_data['type']}/{create_data['version']}"
url = ( )
f"https://serverjars.com/api/fetchJar/{create_data['category']}" server_obj.executable_update_url = url
f"/{create_data['type']}/{create_data['version']}" self.servers.update_server(server_obj)
self.server_jars.download_jar(
create_data["category"],
create_data["type"],
create_data["version"],
full_jar_path,
new_server_id,
) )
server_obj.executable_update_url = url elif root_create_data["create_type"] == "import_server":
self.servers.update_server(server_obj) ServersController.set_import(new_server_id)
self.server_jars.download_jar( self.import_helper.import_jar_server(
create_data["category"], create_data["existing_server_path"],
create_data["type"], new_server_path,
create_data["version"], monitoring_port,
full_jar_path, new_server_id,
new_server_id, )
) elif root_create_data["create_type"] == "import_zip":
ServersController.set_import(new_server_id)
elif data["create_type"] == "minecraft_bedrock":
if root_create_data["create_type"] == "download_exe":
ServersController.set_import(new_server_id)
self.import_helper.download_bedrock_server(
new_server_path, new_server_id
)
elif root_create_data["create_type"] == "import_server":
ServersController.set_import(new_server_id)
full_exe_path = os.path.join(new_server_path, create_data["executable"])
self.import_helper.import_bedrock_server(
create_data["existing_server_path"],
new_server_path,
monitoring_port,
full_exe_path,
new_server_id,
)
elif root_create_data["create_type"] == "import_zip":
ServersController.set_import(new_server_id)
full_exe_path = os.path.join(new_server_path, create_data["executable"])
self.import_helper.import_bedrock_zip_server(
create_data["zip_path"],
new_server_path,
os.path.join(create_data["zip_root"], create_data["executable"]),
monitoring_port,
new_server_id,
)
exec_user = self.users.get_user_by_id(int(user_id))
captured_roles = data.get("roles", [])
# These lines create a new Role for the Server with full permissions
# 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"
)
role_id = self.roles.add_role(
f"Creator of Server with uuid={new_server_uuid}",
exec_user["user_id"],
)
self.server_perms.add_role_server(new_server_id, role_id, "11111111")
self.users.add_role_to_user(exec_user["user_id"], role_id)
else:
for role in captured_roles:
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, server_fs_uuid
def create_jar_server(
self,
jar: str,
server: str,
version: str,
name: str,
min_mem: int,
max_mem: int,
port: int,
user_id: int,
):
server_id = Helpers.create_uuid()
server_dir = os.path.join(self.helper.servers_dir, server_id)
backup_path = os.path.join(self.helper.backup_path, server_id)
if Helpers.is_os_windows():
server_dir = Helpers.wtol_path(server_dir)
backup_path = Helpers.wtol_path(backup_path)
server_dir.replace(" ", "^ ")
backup_path.replace(" ", "^ ")
server_file = f"{server}-{version}.jar"
# make the dir - perhaps a UUID?
Helpers.ensure_dir_exists(server_dir)
Helpers.ensure_dir_exists(backup_path)
try:
# do a eula.txt
with open(
os.path.join(server_dir, "eula.txt"), "w", encoding="utf-8"
) as file:
file.write("eula=false")
file.close()
# setup server.properties with the port
with open(
os.path.join(server_dir, "server.properties"), "w", encoding="utf-8"
) as file:
file.write(f"server-port={port}")
file.close()
except Exception as e:
logger.error(f"Unable to create required server files due to :{e}")
return False
if Helpers.is_os_windows():
# Let's check for and setup for install server commands
if server == "forge":
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f'-jar "{server_file}" --installServer'
)
else:
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f'-jar "{server_file}" nogui'
)
else:
if server == "forge":
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f"-jar {server_file} --installServer"
)
else:
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f"-jar {server_file} nogui"
)
server_log_file = "./logs/latest.log"
server_stop = "stop"
new_id = self.register_server(
name,
server_id,
server_dir,
backup_path,
server_command,
server_file,
server_log_file,
server_stop,
port,
user_id,
server_type="minecraft-java",
)
# modded update urls from server jars will only update the installer
if jar != "modded":
server_obj = self.servers.get_server_obj(new_id)
url = f"https://serverjars.com/api/fetchJar/{jar}/{server}/{version}"
server_obj.executable_update_url = url
self.servers.update_server(server_obj)
# download the jar
self.server_jars.download_jar(
jar, server, version, os.path.join(server_dir, server_file), new_id
)
return new_id
@staticmethod @staticmethod
def verify_jar_server(server_path: str, server_jar: str): def verify_jar_server(server_path: str, server_jar: str):
server_path = Helpers.get_os_understandable_path(server_path) server_path = Helpers.get_os_understandable_path(server_path)
@ -597,123 +599,6 @@ class Controller:
return False return False
return True return True
def import_jar_server(
self,
server_name: str,
server_path: str,
server_jar: str,
min_mem: int,
max_mem: int,
port: int,
user_id: int,
):
server_id = Helpers.create_uuid()
new_server_dir = os.path.join(self.helper.servers_dir, server_id)
backup_path = os.path.join(self.helper.backup_path, server_id)
if Helpers.is_os_windows():
new_server_dir = Helpers.wtol_path(new_server_dir)
backup_path = Helpers.wtol_path(backup_path)
new_server_dir.replace(" ", "^ ")
backup_path.replace(" ", "^ ")
Helpers.ensure_dir_exists(new_server_dir)
Helpers.ensure_dir_exists(backup_path)
server_path = Helpers.get_os_understandable_path(server_path)
full_jar_path = os.path.join(new_server_dir, server_jar)
if Helpers.is_os_windows():
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f'-jar "{full_jar_path}" nogui'
)
else:
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f"-jar {full_jar_path} nogui"
)
server_log_file = "./logs/latest.log"
server_stop = "stop"
new_id = self.register_server(
server_name,
server_id,
new_server_dir,
backup_path,
server_command,
server_jar,
server_log_file,
server_stop,
port,
user_id,
server_type="minecraft-java",
)
ServersController.set_import(new_id)
self.import_helper.import_jar_server(server_path, new_server_dir, port, new_id)
return new_id
def import_zip_server(
self,
server_name: str,
zip_path: str,
server_jar: str,
min_mem: int,
max_mem: int,
port: int,
user_id: int,
):
server_id = Helpers.create_uuid()
new_server_dir = os.path.join(self.helper.servers_dir, server_id)
backup_path = os.path.join(self.helper.backup_path, server_id)
if Helpers.is_os_windows():
new_server_dir = Helpers.wtol_path(new_server_dir)
backup_path = Helpers.wtol_path(backup_path)
new_server_dir.replace(" ", "^ ")
backup_path.replace(" ", "^ ")
temp_dir = Helpers.get_os_understandable_path(zip_path)
Helpers.ensure_dir_exists(new_server_dir)
Helpers.ensure_dir_exists(backup_path)
full_jar_path = os.path.join(new_server_dir, server_jar)
if Helpers.is_os_windows():
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f'-jar "{full_jar_path}" nogui'
)
else:
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f"-jar {full_jar_path} nogui"
)
logger.debug("command: " + server_command)
server_log_file = "./logs/latest.log"
server_stop = "stop"
new_id = self.register_server(
server_name,
server_id,
new_server_dir,
backup_path,
server_command,
server_jar,
server_log_file,
server_stop,
port,
user_id,
server_type="minecraft-java",
)
ServersController.set_import(new_id)
self.import_helper.import_java_zip_server(
temp_dir, new_server_dir, port, new_id
)
return new_id
# ********************************************************************************** # **********************************************************************************
# BEDROCK IMPORTS # BEDROCK IMPORTS
# ********************************************************************************** # **********************************************************************************
@ -1120,6 +1005,8 @@ class Controller:
"the new directory." "the new directory."
}, },
) )
self.helper.dir_migration = False
return return
# set the cached serve dir # set the cached serve dir
self.helper.servers_dir = new_server_path self.helper.servers_dir = new_server_path

View File

@ -41,10 +41,10 @@ scheduler_intervals = {
class TasksManager: class TasksManager:
controller: Controller controller: Controller
def __init__(self, helper, controller): def __init__(self, helper, controller, file_helper):
self.helper: Helpers = helper self.helper: Helpers = helper
self.controller: Controller = controller self.controller: Controller = controller
self.tornado: Webserver = Webserver(helper, controller, self) self.tornado: Webserver = Webserver(helper, controller, self, file_helper)
try: try:
self.tz = get_localzone() self.tz = get_localzone()
except ZoneInfoNotFoundError as e: except ZoneInfoNotFoundError as e:
@ -738,12 +738,21 @@ class TasksManager:
def check_for_updates(self): def check_for_updates(self):
logger.info("Checking for Crafty updates...") logger.info("Checking for Crafty updates...")
self.helper.update_available = self.helper.check_remote_version() self.helper.update_available = self.helper.check_remote_version()
remote = self.helper.update_available
if self.helper.update_available: if self.helper.update_available:
logger.info(f"Found new version {self.helper.update_available}") logger.info(f"Found new version {self.helper.update_available}")
else: else:
logger.info( logger.info(
"No updates found! You are on the most up to date Crafty version." "No updates found! You are on the most up to date Crafty version."
) )
if self.helper.update_available:
self.helper.update_available = {
"id": str(remote),
"title": f"{remote} Update Available",
"date": "",
"desc": "Release notes are available by clicking this notification.",
"link": "https://gitlab.com/crafty-controller/crafty-4/-/releases",
}
logger.info("Refreshing Gravatar PFPs...") logger.info("Refreshing Gravatar PFPs...")
for user in HelperUsers.get_all_users(): for user in HelperUsers.get_all_users():
if user.email: if user.email:

View File

@ -1,698 +0,0 @@
import os
import html
import pathlib
import re
import logging
import time
import urllib.parse
import bleach
import tornado.web
import tornado.escape
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.shared.console import Console
from app.classes.shared.helpers import Helpers
from app.classes.shared.server import ServerOutBuf
from app.classes.web.base_handler import BaseHandler
logger = logging.getLogger(__name__)
class AjaxHandler(BaseHandler):
def render_page(self, template, page_data):
self.render(
template,
data=page_data,
translate=self.translator.translate,
)
@tornado.web.authenticated
def get(self, page):
_, _, exec_user = self.current_user
error = bleach.clean(self.get_argument("error", "WTF Error!"))
template = "panel/denied.html"
page_data = {"user_data": exec_user, "error": error}
if page == "error":
template = "public/error.html"
self.render_page(template, page_data)
elif page == "server_log":
server_id = self.get_argument("id", None)
full_log = self.get_argument("full", False)
if server_id is None:
logger.warning("Server ID not found in server_log ajax call")
self.redirect("/panel/error?error=Server ID Not Found")
return
server_id = bleach.clean(server_id)
server_data = self.controller.servers.get_server_data_by_id(server_id)
if not server_data:
logger.warning("Server Data not found in server_log ajax call")
self.redirect("/panel/error?error=Server ID Not Found")
return
if not server_data["log_path"]:
logger.warning(
f"Log path not found in server_log ajax call ({server_id})"
)
if full_log:
log_lines = self.helper.get_setting("max_log_lines")
data = Helpers.tail_file(
# If the log path is absolute it returns it as is
# If it is relative it joins the paths below like normal
pathlib.Path(server_data["path"], server_data["log_path"]),
log_lines,
)
else:
data = ServerOutBuf.lines.get(server_id, [])
for line in data:
try:
line = re.sub("(\033\\[(0;)?[0-9]*[A-z]?(;[0-9])?m?)", "", line)
line = re.sub("[A-z]{2}\b\b", "", line)
line = self.helper.log_colors(html.escape(line))
self.write(f"<span class='box'>{line}<br /></span>")
# self.write(d.encode("utf-8"))
except Exception as e:
logger.warning(f"Skipping Log Line due to error: {e}")
elif page == "announcements":
data = Helpers.get_announcements()
page_data["notify_data"] = data
self.render_page("ajax/notify.html", page_data)
elif page == "get_zip_tree":
path = self.get_argument("path", None)
self.write(
Helpers.get_os_understandable_path(path)
+ "\n"
+ Helpers.generate_zip_tree(path)
)
self.finish()
elif page == "get_zip_dir":
path = self.get_argument("path", None)
self.write(
Helpers.get_os_understandable_path(path)
+ "\n"
+ Helpers.generate_zip_dir(path)
)
self.finish()
elif page == "get_backup_tree":
server_id = self.get_argument("id", None)
folder = self.get_argument("path", None)
output = ""
dir_list = []
unsorted_files = []
file_list = os.listdir(folder)
for item in file_list:
if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item)
else:
unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted(
unsorted_files, key=str.casefold
)
output += f"""<ul class="tree-nested d-block" id="{folder}ul">"""
for raw_filename in file_list:
filename = html.escape(raw_filename)
rel = os.path.join(folder, raw_filename)
dpath = os.path.join(folder, filename)
if str(dpath) in self.controller.management.get_excluded_backup_dirs(
server_id
):
if os.path.isdir(rel):
output += f"""<li class="tree-item" data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" class="checkBoxClass" name="root_path" value="{dpath}" checked>
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i>
<strong>{filename}</strong>
</span>
</input></div><li>
\n"""
else:
output += f"""<li
class="d-block tree-ctx-item tree-file"
data-path="{dpath}"
data-name="{filename}"
onclick=""><input type='checkbox' class="checkBoxClass" name='root_path' value="{dpath}" checked><span style="margin-right: 6px;">
<i class="far fa-file"></i></span></input>{filename}</li>"""
else:
if os.path.isdir(rel):
output += f"""<li class="tree-item" data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" class="checkBoxClass" name="root_path" value="{dpath}">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i>
<strong>{filename}</strong>
</span>
</input></div><li>
\n"""
else:
output += f"""<li
class="d-block tree-ctx-item tree-file"
data-path="{dpath}"
data-name="{filename}"
onclick=""><input type='checkbox' class="checkBoxClass" name='root_path' value="{dpath}">
<span style="margin-right: 6px;"><i class="far fa-file">
</i></span></input>{filename}</li>"""
self.write(Helpers.get_os_understandable_path(folder) + "\n" + output)
self.finish()
elif page == "get_backup_dir":
server_id = self.get_argument("id", None)
folder = self.get_argument("path", None)
output = ""
dir_list = []
unsorted_files = []
file_list = os.listdir(folder)
for item in file_list:
if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item)
else:
unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted(
unsorted_files, key=str.casefold
)
output += f"""<ul class="tree-nested d-block" id="{folder}ul">"""
for raw_filename in file_list:
filename = html.escape(raw_filename)
rel = os.path.join(folder, raw_filename)
dpath = os.path.join(folder, filename)
if str(dpath) in self.controller.management.get_excluded_backup_dirs(
server_id
):
if os.path.isdir(rel):
output += f"""<li class="tree-item" data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" name="root_path" value="{dpath}" checked>
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
<i class="far fa-folder"></i>
<i class="far fa-folder-open"></i>
<strong>{filename}</strong>
</span>
</input></div><li>"""
else:
output += f"""<li
class="tree-item tree-nested d-block tree-ctx-item tree-file"
data-path="{dpath}"
data-name="{filename}"
onclick=""><input type='checkbox' name='root_path' value='{dpath}' checked><span style="margin-right: 6px;">
<i class="far fa-file"></i></span></input>{filename}</li>"""
else:
if os.path.isdir(rel):
output += f"""<li class="tree-item" data-path="{dpath}">
\n<div id="{dpath}" data-path="{dpath}" data-name="{filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" name="root_path" value="{dpath}">
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
<i class="far fa-folder"></i>
<i class="far fa-folder-open"></i>
<strong>{filename}</strong>
</span>
</input></div><li>"""
else:
output += f"""<li
class="tree-item tree-nested d-block tree-ctx-item tree-file"
data-path="{dpath}"
data-name="{filename}"
onclick=""><input type='checkbox' name='root_path' value='{dpath}'>
<span style="margin-right: 6px;"><i class="far fa-file">
</i></span></input>{filename}</li>"""
self.write(Helpers.get_os_understandable_path(folder) + "\n" + output)
self.finish()
elif page == "get_dir":
server_id = self.get_argument("id", None)
path = self.get_argument("path", None)
if not self.check_server_id(server_id, "get_tree"):
return
server_id = bleach.clean(server_id)
if Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"], path
):
self.write(
Helpers.get_os_understandable_path(path)
+ "\n"
+ Helpers.generate_dir(path)
)
self.finish()
@tornado.web.authenticated
def post(self, page):
api_key, _, exec_user = self.current_user
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
server_id = self.get_argument("id", None)
permissions = {
"Commands": EnumPermissionsServer.COMMANDS,
"Terminal": EnumPermissionsServer.TERMINAL,
"Logs": EnumPermissionsServer.LOGS,
"Schedule": EnumPermissionsServer.SCHEDULE,
"Backup": EnumPermissionsServer.BACKUP,
"Files": EnumPermissionsServer.FILES,
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
}
user_perms = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
if page == "send_command":
command = self.get_body_argument("command", default=None, strip=True)
server_id = self.get_argument("id", None)
if server_id is None:
logger.warning("Server ID not found in send_command ajax call")
Console.warning("Server ID not found in send_command ajax call")
svr_obj = self.controller.servers.get_server_instance_by_id(server_id)
if command == svr_obj.settings["stop_command"]:
logger.info(
"Stop command detected as terminal input - intercepting."
+ f"Starting Crafty's stop process for server with id: {server_id}"
)
self.controller.management.send_command(
exec_user["user_id"], server_id, self.get_remote_ip(), "stop_server"
)
command = None
elif command == "restart":
logger.info(
"Restart command detected as terminal input - intercepting."
+ f"Starting Crafty's stop process for server with id: {server_id}"
)
self.controller.management.send_command(
exec_user["user_id"],
server_id,
self.get_remote_ip(),
"restart_server",
)
command = None
if command:
if svr_obj.check_running():
svr_obj.send_command(command)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Sent command to "
f"{self.controller.servers.get_server_friendly_name(server_id)} "
f"terminal: {command}",
server_id,
self.get_remote_ip(),
)
elif page == "send_order":
self.controller.users.update_server_order(
exec_user["user_id"], bleach.clean(self.get_argument("order"))
)
return
elif page == "backup_now":
server_id = self.get_argument("id", None)
if server_id is None:
logger.error("Server ID is none. Canceling backup!")
return
server = self.controller.servers.get_server_instance_by_id(server_id)
self.controller.management.add_to_audit_log_raw(
self.controller.users.get_user_by_id(exec_user["user_id"])["username"],
exec_user["user_id"],
server_id,
f"Backup now executed for server {server_id} ",
source_ip=self.get_remote_ip(),
)
server.backup_server()
elif page == "select_photo":
if exec_user["superuser"]:
photo = urllib.parse.unquote(self.get_argument("photo", ""))
opacity = self.get_argument("opacity", 100)
self.controller.management.set_login_opacity(int(opacity))
if photo == "login_1.jpg":
self.controller.management.set_login_image("login_1.jpg")
self.controller.cached_login = f"{photo}"
else:
self.controller.management.set_login_image(f"custom/{photo}")
self.controller.cached_login = f"custom/{photo}"
return
elif page == "delete_photo":
if exec_user["superuser"]:
photo = urllib.parse.unquote(self.get_argument("photo", None))
if photo and photo != "login_1.jpg":
os.remove(
os.path.join(
self.controller.project_root,
f"app/frontend/static/assets/images/auth/custom/{photo}",
)
)
current = self.controller.cached_login
split = current.split("/")
if len(split) == 1:
current_photo = current
else:
current_photo = split[1]
if current_photo == photo:
self.controller.management.set_login_image("login_1.jpg")
self.controller.cached_login = "login_1.jpg"
return
elif page == "eula":
server_id = self.get_argument("id", None)
svr = self.controller.servers.get_server_instance_by_id(server_id)
svr.agree_eula(exec_user["user_id"])
elif page == "restore_backup":
if not permissions["Backup"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Backups")
return
server_id = bleach.clean(self.get_argument("id", None))
zip_name = bleach.clean(self.get_argument("zip_file", None))
svr_obj = self.controller.servers.get_server_obj(server_id)
server_data = self.controller.servers.get_server_data_by_id(server_id)
# import the server again based on zipfile
if server_data["type"] == "minecraft-java":
backup_path = svr_obj.backup_path
if Helpers.validate_traversal(backup_path, zip_name):
temp_dir = Helpers.unzip_backup_archive(backup_path, zip_name)
new_server = self.controller.import_zip_server(
svr_obj.server_name,
temp_dir,
server_data["executable"],
"1",
"2",
server_data["server_port"],
server_data["created_by"],
)
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"]
)
# preserve current schedules
for schedule in self.controller.management.get_schedules_by_server(
server_id
):
self.tasks_manager.update_job(
schedule.schedule_id, {"server_id": new_server_id}
)
# preserve execution command
new_server_obj = self.controller.servers.get_server_obj(
new_server_id
)
new_server_obj.execution_command = server_data["execution_command"]
# reset executable path
if svr_obj.path in svr_obj.executable:
new_server_obj.executable = str(svr_obj.executable).replace(
svr_obj.path, new_server_obj.path
)
# reset run command path
if svr_obj.path in svr_obj.execution_command:
new_server_obj.execution_command = str(
svr_obj.execution_command
).replace(svr_obj.path, new_server_obj.path)
# reset log path
if svr_obj.path in svr_obj.log_path:
new_server_obj.log_path = str(svr_obj.log_path).replace(
svr_obj.path, new_server_obj.path
)
self.controller.servers.update_server(new_server_obj)
# preserve backup config
backup_config = self.controller.management.get_backup_config(
server_id
)
excluded_dirs = []
server_obj = self.controller.servers.get_server_obj(server_id)
loop_backup_path = self.helper.wtol_path(server_obj.path)
for item in self.controller.management.get_excluded_backup_dirs(
server_id
):
item_path = self.helper.wtol_path(item)
bu_path = os.path.relpath(item_path, loop_backup_path)
bu_path = os.path.join(new_server_obj.path, bu_path)
excluded_dirs.append(bu_path)
self.controller.management.set_backup_config(
new_server_id,
new_server_obj.backup_path,
backup_config["max_backups"],
excluded_dirs,
backup_config["compress"],
backup_config["shutdown"],
)
# remove old server's tasks
try:
self.tasks_manager.remove_all_server_tasks(server_id)
except:
logger.info("No active tasks found for server")
self.controller.remove_server(server_id, True)
self.redirect("/panel/dashboard")
else:
backup_path = svr_obj.backup_path
if Helpers.validate_traversal(backup_path, zip_name):
temp_dir = Helpers.unzip_backup_archive(backup_path, zip_name)
new_server = self.controller.import_bedrock_zip_server(
svr_obj.server_name,
temp_dir,
server_data["executable"],
server_data["server_port"],
server_data["created_by"],
)
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"]
)
# preserve current schedules
for schedule in self.controller.management.get_schedules_by_server(
server_id
):
self.tasks_manager.update_job(
schedule.schedule_id, {"server_id": new_server_id}
)
# preserve execution command
new_server_obj = self.controller.servers.get_server_obj(
new_server_id
)
new_server_obj.execution_command = server_data["execution_command"]
# reset executable path
if server_obj.path in server_obj.executable:
new_server_obj.executable = str(server_obj.executable).replace(
server_obj.path, new_server_obj.path
)
# reset run command path
if server_obj.path in server_obj.execution_command:
new_server_obj.execution_command = str(
server_obj.execution_command
).replace(server_obj.path, new_server_obj.path)
# reset log path
if server_obj.path in server_obj.log_path:
new_server_obj.log_path = str(server_obj.log_path).replace(
server_obj.path, new_server_obj.path
)
self.controller.servers.update_server(new_server_obj)
# preserve backup config
backup_config = self.controller.management.get_backup_config(
server_id
)
excluded_dirs = []
server_obj = self.controller.servers.get_server_obj(server_id)
loop_backup_path = self.helper.wtol_path(server_obj.path)
for item in self.controller.management.get_excluded_backup_dirs(
server_id
):
item_path = self.helper.wtol_path(item)
bu_path = os.path.relpath(item_path, loop_backup_path)
bu_path = os.path.join(new_server_obj.path, bu_path)
excluded_dirs.append(bu_path)
self.controller.management.set_backup_config(
new_server_id,
new_server_obj.backup_path,
backup_config["max_backups"],
excluded_dirs,
backup_config["compress"],
backup_config["shutdown"],
)
try:
self.tasks_manager.remove_all_server_tasks(server_id)
except:
logger.info("No active tasks found for server")
self.controller.remove_server(server_id, True)
self.redirect("/panel/dashboard")
elif page == "unzip_server":
path = urllib.parse.unquote(self.get_argument("path", ""))
if not path:
path = os.path.join(
self.controller.project_root,
"imports",
urllib.parse.unquote(self.get_argument("file", "")),
)
if Helpers.check_file_exists(path):
self.helper.unzip_server(path, exec_user["user_id"])
else:
user_id = exec_user["user_id"]
if user_id:
time.sleep(5)
user_lang = self.controller.users.get_user_lang_by_id(user_id)
self.helper.websocket_helper.broadcast_user(
user_id,
"send_start_error",
{
"error": self.helper.translation.translate(
"error", "no-file", user_lang
)
},
)
return
elif page == "backup_select":
path = self.get_argument("path", None)
self.helper.backup_select(path, exec_user["user_id"])
return
elif page == "jar_cache":
if not superuser:
self.redirect("/panel/error?error=Not a super user")
return
self.controller.server_jars.manual_refresh_cache()
return
elif page == "update_server_dir":
if self.helper.dir_migration:
return
for server in self.controller.servers.get_all_servers_stats():
if server["stats"]["running"]:
self.helper.websocket_helper.broadcast_user(
exec_user["user_id"],
"send_start_error",
{
"error": "You must stop all servers before "
"starting a storage migration."
},
)
return
if not superuser:
self.redirect("/panel/error?error=Not a super user")
return
if self.helper.is_env_docker():
self.redirect(
"/panel/error?error=This feature is not"
" supported on docker environments"
)
return
new_dir = urllib.parse.unquote(self.get_argument("server_dir"))
self.controller.update_master_server_dir(new_dir, exec_user["user_id"])
return
@tornado.web.authenticated
def delete(self, page):
api_key, _, exec_user = self.current_user
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
server_id = self.get_argument("id", None)
permissions = {
"Commands": EnumPermissionsServer.COMMANDS,
"Terminal": EnumPermissionsServer.TERMINAL,
"Logs": EnumPermissionsServer.LOGS,
"Schedule": EnumPermissionsServer.SCHEDULE,
"Backup": EnumPermissionsServer.BACKUP,
"Files": EnumPermissionsServer.FILES,
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
}
user_perms = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
if page == "del_backup":
if not permissions["Backup"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Backups")
return
file_path = Helpers.get_os_understandable_path(
self.get_body_argument("file_path", default=None, strip=True)
)
server_id = self.get_argument("id", None)
Console.warning(f"Delete {file_path} for server {server_id}")
if not self.check_server_id(server_id, "del_backup"):
return
server_id = bleach.clean(server_id)
server_info = self.controller.servers.get_server_data_by_id(server_id)
if not (
self.helper.is_subdir(
file_path, Helpers.get_os_understandable_path(server_info["path"])
)
or self.helper.is_subdir(
file_path,
Helpers.get_os_understandable_path(server_info["backup_path"]),
)
) or not Helpers.check_file_exists(os.path.abspath(file_path)):
logger.warning(f"Invalid path in del_backup ajax call ({file_path})")
Console.warning(f"Invalid path in del_backup ajax call ({file_path})")
return
# Delete the file
if Helpers.validate_traversal(
Helpers.get_os_understandable_path(server_info["backup_path"]),
file_path,
):
os.remove(file_path)
def check_server_id(self, server_id, page_name):
if server_id is None:
logger.warning(
f"Server ID not defined in {page_name} ajax call ({server_id})"
)
Console.warning(
f"Server ID not defined in {page_name} ajax call ({server_id})"
)
return
server_id = bleach.clean(server_id)
# does this server id exist?
if not self.controller.servers.server_id_exists(server_id):
logger.warning(
f"Server ID not found in {page_name} ajax call ({server_id})"
)
Console.warning(
f"Server ID not found in {page_name} ajax call ({server_id})"
)
return
return True

View File

@ -8,6 +8,7 @@ import tornado.web
from app.classes.models.crafty_permissions import EnumPermissionsCrafty from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.models.users import ApiKeys from app.classes.models.users import ApiKeys
from app.classes.shared.helpers import Helpers from app.classes.shared.helpers import Helpers
from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.main_controller import Controller from app.classes.shared.main_controller import Controller
from app.classes.shared.translation import Translation from app.classes.shared.translation import Translation
from app.classes.models.management import DatabaseShortcuts from app.classes.models.management import DatabaseShortcuts
@ -24,15 +25,22 @@ class BaseHandler(tornado.web.RequestHandler):
helper: Helpers helper: Helpers
controller: Controller controller: Controller
translator: Translation translator: Translation
file_helper: FileHelpers
# noinspection PyAttributeOutsideInit # noinspection PyAttributeOutsideInit
def initialize( def initialize(
self, helper=None, controller=None, tasks_manager=None, translator=None self,
helper=None,
controller=None,
tasks_manager=None,
translator=None,
file_helper=None,
): ):
self.helper = helper self.helper = helper
self.controller = controller self.controller = controller
self.tasks_manager = tasks_manager self.tasks_manager = tasks_manager
self.translator = translator self.translator = translator
self.file_helper = file_helper
def set_default_headers(self) -> None: def set_default_headers(self) -> None:
""" """

View File

@ -1,464 +0,0 @@
import os
import logging
import bleach
import tornado.web
import tornado.escape
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.shared.console import Console
from app.classes.shared.helpers import Helpers
from app.classes.shared.file_helpers import FileHelpers
from app.classes.web.base_handler import BaseHandler
logger = logging.getLogger(__name__)
class FileHandler(BaseHandler):
def render_page(self, template, page_data):
self.render(
template,
data=page_data,
translate=self.translator.translate,
)
@tornado.web.authenticated
def get(self, page):
api_key, _, exec_user = self.current_user
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
server_id = self.get_argument("id", None)
permissions = {
"Commands": EnumPermissionsServer.COMMANDS,
"Terminal": EnumPermissionsServer.TERMINAL,
"Logs": EnumPermissionsServer.LOGS,
"Schedule": EnumPermissionsServer.SCHEDULE,
"Backup": EnumPermissionsServer.BACKUP,
"Files": EnumPermissionsServer.FILES,
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
}
user_perms = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
if page == "get_file":
if not permissions["Files"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
file_path = Helpers.get_os_understandable_path(
self.get_argument("file_path", None)
)
if not self.check_server_id(server_id, "get_file"):
return
server_id = bleach.clean(server_id)
if not self.helper.is_subdir(
file_path,
Helpers.get_os_understandable_path(
self.controller.servers.get_server_data_by_id(server_id)["path"]
),
) or not Helpers.check_file_exists(os.path.abspath(file_path)):
logger.warning(
f"Invalid path in get_file file file ajax call ({file_path})"
)
Console.warning(
f"Invalid path in get_file file file ajax call ({file_path})"
)
return
error = None
try:
with open(file_path, encoding="utf-8") as file:
file_contents = file.read()
except UnicodeDecodeError:
file_contents = ""
error = "UnicodeDecodeError"
self.write({"content": file_contents, "error": error})
self.finish()
elif page == "get_tree":
if not permissions["Files"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
path = self.get_argument("path", None)
if not self.check_server_id(server_id, "get_tree"):
return
server_id = bleach.clean(server_id)
if Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"], path
):
self.write(
Helpers.get_os_understandable_path(path)
+ "\n"
+ self.helper.generate_tree(path)
)
self.finish()
elif page == "get_dir":
if not permissions["Files"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
path = self.get_argument("path", None)
if not self.check_server_id(server_id, "get_tree"):
return
server_id = bleach.clean(server_id)
if Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"], path
):
self.write(
Helpers.get_os_understandable_path(path)
+ "\n"
+ self.helper.generate_dir(path)
)
self.finish()
@tornado.web.authenticated
def post(self, page):
api_key, _, exec_user = self.current_user
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
server_id = self.get_argument("id", None)
permissions = {
"Commands": EnumPermissionsServer.COMMANDS,
"Terminal": EnumPermissionsServer.TERMINAL,
"Logs": EnumPermissionsServer.LOGS,
"Schedule": EnumPermissionsServer.SCHEDULE,
"Backup": EnumPermissionsServer.BACKUP,
"Files": EnumPermissionsServer.FILES,
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
}
user_perms = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
if page == "create_file":
if not permissions["Files"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
file_parent = Helpers.get_os_understandable_path(
self.get_body_argument("file_parent", default=None, strip=True)
)
file_name = self.get_body_argument("file_name", default=None, strip=True)
file_path = os.path.join(file_parent, file_name)
if not self.check_server_id(server_id, "create_file"):
return
server_id = bleach.clean(server_id)
if not self.helper.is_subdir(
file_path,
Helpers.get_os_understandable_path(
self.controller.servers.get_server_data_by_id(server_id)["path"]
),
) or Helpers.check_file_exists(os.path.abspath(file_path)):
logger.warning(
f"Invalid path in create_file file ajax call ({file_path})"
)
Console.warning(
f"Invalid path in create_file file ajax call ({file_path})"
)
return
# Create the file by opening it
with open(file_path, "w", encoding="utf-8") as file_object:
file_object.close()
elif page == "create_dir":
if not permissions["Files"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
dir_parent = Helpers.get_os_understandable_path(
self.get_body_argument("dir_parent", default=None, strip=True)
)
dir_name = self.get_body_argument("dir_name", default=None, strip=True)
dir_path = os.path.join(dir_parent, dir_name)
if not self.check_server_id(server_id, "create_dir"):
return
server_id = bleach.clean(server_id)
if not self.helper.is_subdir(
dir_path,
Helpers.get_os_understandable_path(
self.controller.servers.get_server_data_by_id(server_id)["path"]
),
) or Helpers.check_path_exists(os.path.abspath(dir_path)):
logger.warning(
f"Invalid path in create_dir file ajax call ({dir_path})"
)
Console.warning(
f"Invalid path in create_dir file ajax call ({dir_path})"
)
return
# Create the directory
os.mkdir(dir_path)
elif page == "unzip_file":
if not permissions["Files"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
path = Helpers.get_os_understandable_path(self.get_argument("path", None))
if Helpers.is_os_windows():
path = Helpers.wtol_path(path)
FileHelpers.unzip_file(path)
self.redirect(f"/panel/server_detail?id={server_id}&subpage=files")
return
@tornado.web.authenticated
def delete(self, page):
api_key, _, exec_user = self.current_user
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
server_id = self.get_argument("id", None)
permissions = {
"Commands": EnumPermissionsServer.COMMANDS,
"Terminal": EnumPermissionsServer.TERMINAL,
"Logs": EnumPermissionsServer.LOGS,
"Schedule": EnumPermissionsServer.SCHEDULE,
"Backup": EnumPermissionsServer.BACKUP,
"Files": EnumPermissionsServer.FILES,
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
}
user_perms = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
if page == "del_file":
if not permissions["Files"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
file_path = Helpers.get_os_understandable_path(
self.get_body_argument("file_path", default=None, strip=True)
)
Console.warning(f"Delete {file_path} for server {server_id}")
if not self.check_server_id(server_id, "del_file"):
return
server_id = bleach.clean(server_id)
server_info = self.controller.servers.get_server_data_by_id(server_id)
if not (
self.helper.is_subdir(
file_path, Helpers.get_os_understandable_path(server_info["path"])
)
or self.helper.is_subdir(
file_path,
Helpers.get_os_understandable_path(server_info["backup_path"]),
)
) or not Helpers.check_file_exists(os.path.abspath(file_path)):
logger.warning(f"Invalid path in del_file file ajax call ({file_path})")
Console.warning(
f"Invalid path in del_file file ajax call ({file_path})"
)
return
# Delete the file
FileHelpers.del_file(file_path)
elif page == "del_dir":
if not permissions["Files"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
dir_path = Helpers.get_os_understandable_path(
self.get_body_argument("dir_path", default=None, strip=True)
)
Console.warning(f"Delete {dir_path} for server {server_id}")
if not self.check_server_id(server_id, "del_dir"):
return
server_id = bleach.clean(server_id)
server_info = self.controller.servers.get_server_data_by_id(server_id)
if not self.helper.is_subdir(
dir_path, Helpers.get_os_understandable_path(server_info["path"])
) or not Helpers.check_path_exists(os.path.abspath(dir_path)):
logger.warning(f"Invalid path in del_file file ajax call ({dir_path})")
Console.warning(f"Invalid path in del_file file ajax call ({dir_path})")
return
# Delete the directory
# os.rmdir(dir_path) # Would only remove empty directories
if Helpers.validate_traversal(
Helpers.get_os_understandable_path(server_info["path"]), dir_path
):
# Removes also when there are contents
FileHelpers.del_dirs(dir_path)
@tornado.web.authenticated
def put(self, page):
api_key, _, exec_user = self.current_user
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
server_id = self.get_argument("id", None)
permissions = {
"Commands": EnumPermissionsServer.COMMANDS,
"Terminal": EnumPermissionsServer.TERMINAL,
"Logs": EnumPermissionsServer.LOGS,
"Schedule": EnumPermissionsServer.SCHEDULE,
"Backup": EnumPermissionsServer.BACKUP,
"Files": EnumPermissionsServer.FILES,
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
}
user_perms = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
if page == "save_file":
if not permissions["Files"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
file_contents = self.get_body_argument(
"file_contents", default=None, strip=True
)
file_path = Helpers.get_os_understandable_path(
self.get_body_argument("file_path", default=None, strip=True)
)
if not self.check_server_id(server_id, "save_file"):
return
server_id = bleach.clean(server_id)
if not self.helper.is_subdir(
file_path,
Helpers.get_os_understandable_path(
self.controller.servers.get_server_data_by_id(server_id)["path"]
),
) or not Helpers.check_file_exists(os.path.abspath(file_path)):
logger.warning(
f"Invalid path in save_file file ajax call ({file_path})"
)
Console.warning(
f"Invalid path in save_file file ajax call ({file_path})"
)
return
# Open the file in write mode and store the content in file_object
with open(file_path, "w", encoding="utf-8") as file_object:
file_object.write(file_contents)
@tornado.web.authenticated
def patch(self, page):
api_key, _, exec_user = self.current_user
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
server_id = self.get_argument("id", None)
permissions = {
"Commands": EnumPermissionsServer.COMMANDS,
"Terminal": EnumPermissionsServer.TERMINAL,
"Logs": EnumPermissionsServer.LOGS,
"Schedule": EnumPermissionsServer.SCHEDULE,
"Backup": EnumPermissionsServer.BACKUP,
"Files": EnumPermissionsServer.FILES,
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
}
user_perms = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
if page == "rename_file":
if not permissions["Files"] in user_perms:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access to Files")
return
item_path = Helpers.get_os_understandable_path(
self.get_body_argument("item_path", default=None, strip=True)
)
new_item_name = self.get_body_argument(
"new_item_name", default=None, strip=True
)
if not self.check_server_id(server_id, "rename_file"):
return
server_id = bleach.clean(server_id)
if item_path is None or new_item_name is None:
logger.warning("Invalid path(s) in rename_file file ajax call")
Console.warning("Invalid path(s) in rename_file file ajax call")
return
if not self.helper.is_subdir(
item_path,
Helpers.get_os_understandable_path(
self.controller.servers.get_server_data_by_id(server_id)["path"]
),
) or not Helpers.check_path_exists(os.path.abspath(item_path)):
logger.warning(
f"Invalid old name path in rename_file file ajax call ({server_id})"
)
Console.warning(
f"Invalid old name path in rename_file file ajax call ({server_id})"
)
return
new_item_path = os.path.join(os.path.split(item_path)[0], new_item_name)
if not self.helper.is_subdir(
new_item_path,
Helpers.get_os_understandable_path(
self.controller.servers.get_server_data_by_id(server_id)["path"]
),
) or Helpers.check_path_exists(os.path.abspath(new_item_path)):
logger.warning(
f"Invalid new name path in rename_file file ajax call ({server_id})"
)
Console.warning(
f"Invalid new name path in rename_file file ajax call ({server_id})"
)
return
# RENAME
os.rename(item_path, new_item_path)
def check_server_id(self, server_id, page_name):
if server_id is None:
logger.warning(
f"Server ID not defined in {page_name} file ajax call ({server_id})"
)
Console.warning(
f"Server ID not defined in {page_name} file ajax call ({server_id})"
)
return
server_id = bleach.clean(server_id)
# does this server id exist?
if not self.controller.servers.server_id_exists(server_id):
logger.warning(
f"Server ID not found in {page_name} file ajax call ({server_id})"
)
Console.warning(
f"Server ID not found in {page_name} file ajax call ({server_id})"
)
return
return True

View File

@ -1415,40 +1415,8 @@ class PanelHandler(BaseHandler):
template = "panel/panel_edit_role.html" template = "panel/panel_edit_role.html"
elif page == "remove_role":
role_id = bleach.clean(self.get_argument("id", None))
if (
not superuser
and self.controller.roles.get_role(role_id)["manager"]
!= exec_user["user_id"]
):
self.redirect(
"/panel/error?error=Unauthorized access: not superuser not"
" role manager"
)
return
if role_id is None:
self.redirect("/panel/error?error=Invalid Role ID")
return
# does this user id exist?
target_role = self.controller.roles.get_role(role_id)
if not target_role:
self.redirect("/panel/error?error=Invalid Role ID")
return
self.controller.roles.remove_role(role_id)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Removed role {target_role['role_name']} (RID:{role_id})",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.redirect("/panel/panel_config")
elif page == "activity_logs": elif page == "activity_logs":
page_data["audit_logs"] = self.controller.management.get_actity_log() page_data["audit_logs"] = self.controller.management.get_activity_log()
template = "panel/activity_logs.html" template = "panel/activity_logs.html"
@ -1530,606 +1498,3 @@ class PanelHandler(BaseHandler):
utc_offset=(time.timezone * -1 / 60 / 60), utc_offset=(time.timezone * -1 / 60 / 60),
translate=self.translator.translate, translate=self.translator.translate,
) )
@tornado.web.authenticated
def post(self, page):
api_key, _token_data, exec_user = self.current_user
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
server_id = self.get_argument("id", None)
permissions = {
"Commands": EnumPermissionsServer.COMMANDS,
"Terminal": EnumPermissionsServer.TERMINAL,
"Logs": EnumPermissionsServer.LOGS,
"Schedule": EnumPermissionsServer.SCHEDULE,
"Backup": EnumPermissionsServer.BACKUP,
"Files": EnumPermissionsServer.FILES,
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
}
if superuser:
# defined_servers = self.controller.servers.list_defined_servers()
exec_user_role = {"Super User"}
exec_user_crafty_permissions = (
self.controller.crafty_perms.list_defined_crafty_permissions()
)
else:
exec_user_crafty_permissions = (
self.controller.crafty_perms.get_crafty_permissions_list(
exec_user["user_id"]
)
)
# defined_servers =
# self.controller.servers.get_authorized_servers(exec_user["user_id"])
exec_user_role = set()
for r in exec_user["roles"]:
role = self.controller.roles.get_role(r)
exec_user_role.add(role["role_name"])
if page == "server_backup":
logger.debug(self.request.arguments)
server_id = self.check_server_id()
if not server_id:
return
if (
not permissions["Backup"]
in self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
and not superuser
):
self.redirect(
"/panel/error?error=Unauthorized access: User not authorized"
)
return
server_obj = self.controller.servers.get_server_obj(server_id)
compress = self.get_argument("compress", False)
shutdown = self.get_argument("shutdown", False)
check_changed = self.get_argument("changed")
before = self.get_argument("backup_before", "")
after = self.get_argument("backup_after", "")
if str(check_changed) == str(1):
checked = self.get_body_arguments("root_path")
else:
checked = self.controller.management.get_excluded_backup_dirs(server_id)
if superuser:
backup_path = self.get_argument("backup_path", None)
if Helpers.is_os_windows():
backup_path.replace(" ", "^ ")
backup_path = Helpers.wtol_path(backup_path)
else:
backup_path = server_obj.backup_path
max_backups = bleach.clean(self.get_argument("max_backups", None))
server_obj = self.controller.servers.get_server_obj(server_id)
server_obj.backup_path = backup_path
self.controller.servers.update_server(server_obj)
self.controller.management.set_backup_config(
server_id,
max_backups=max_backups,
excluded_dirs=checked,
compress=bool(compress),
shutdown=bool(shutdown),
before=before,
after=after,
)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Edited server {server_id}: updated backups",
server_id,
self.get_remote_ip(),
)
self.tasks_manager.reload_schedule_from_db()
self.redirect(f"/panel/server_detail?id={server_id}&subpage=backup")
elif page == "config_json":
try:
data = {}
with open(self.helper.settings_file, "r", encoding="utf-8") as f:
keys = json.load(f).keys()
this_uuid = self.get_argument("uuid")
for key in keys:
arg_data = self.get_argument(key)
if arg_data.startswith(this_uuid):
arg_data = arg_data.split(",")
arg_data.pop(0)
data[key] = arg_data
else:
try:
data[key] = int(arg_data)
except:
if arg_data == "True":
data[key] = True
elif arg_data == "False":
data[key] = False
else:
data[key] = arg_data
keys = list(data.keys())
keys.sort()
sorted_data = {i: data[i] for i in keys}
with open(self.helper.settings_file, "w", encoding="utf-8") as f:
json.dump(sorted_data, f, indent=4)
except Exception as e:
logger.critical(
"Config File Error: Unable to read "
f"{self.helper.settings_file} due to {e}"
)
self.redirect("/panel/config_json")
elif page == "edit_user":
if bleach.clean(self.get_argument("username", None)).lower() == "system":
self.redirect(
"/panel/error?error=Unauthorized access: "
"system user is not editable"
)
user_id = bleach.clean(self.get_argument("id", None))
user = self.controller.users.get_user_by_id(user_id)
username = bleach.clean(self.get_argument("username", None).lower())
theme = bleach.clean(self.get_argument("theme", "default"))
if (
username != self.controller.users.get_user_by_id(user_id)["username"]
and username in self.controller.users.get_all_usernames()
):
self.redirect(
"/panel/error?error=Duplicate User: Useranme already exists."
)
password0 = bleach.clean(self.get_argument("password0", None))
password1 = bleach.clean(self.get_argument("password1", None))
email = bleach.clean(self.get_argument("email", "default@example.com"))
enabled = int(float(self.get_argument("enabled", "0")))
try:
hints = int(bleach.clean(self.get_argument("hints")))
hints = True
except:
hints = False
lang = bleach.clean(
self.get_argument("language"), self.helper.get_setting("language")
)
if superuser:
# Checks if user is trying to change super user status of self.
# We don't want that. Automatically make them stay super user
# since we know they are.
if str(exec_user["user_id"]) != str(user_id):
superuser = int(bleach.clean(self.get_argument("superuser", "0")))
else:
superuser = 1
else:
superuser = 0
if exec_user["superuser"]:
manager = self.get_argument("manager")
if manager == "":
manager = None
else:
manager = int(manager)
else:
manager = user["manager"]
if (
not exec_user["superuser"]
and int(exec_user["user_id"]) != user["manager"]
):
if username is None or username == "":
self.redirect("/panel/error?error=Invalid username")
return
if user_id is None:
self.redirect("/panel/error?error=Invalid User ID")
return
if (
EnumPermissionsCrafty.USER_CONFIG
not in exec_user_crafty_permissions
):
if str(user_id) != str(exec_user["user_id"]):
self.redirect(
"/panel/error?error=Unauthorized access: not a user editor"
)
return
user_data = {
"username": username,
"password": password0,
"email": email,
"lang": lang,
"hints": hints,
"theme": theme,
}
self.controller.users.update_user(user_id, user_data=user_data)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Edited user {username} (UID:{user_id}) password",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.redirect("/panel/panel_config")
return
# does this user id exist?
if not self.controller.users.user_id_exists(user_id):
self.redirect("/panel/error?error=Invalid User ID")
return
else:
if password0 != password1:
self.redirect("/panel/error?error=Passwords must match")
return
roles = self.get_user_role_memberships()
permissions_mask, server_quantity = self.get_perms_quantity()
# if email is None or "":
# email = "default@example.com"
user_data = {
"username": username,
"manager": manager,
"password": password0,
"email": email,
"enabled": enabled,
"roles": roles,
"lang": lang,
"superuser": superuser,
"hints": hints,
"theme": theme,
}
user_crafty_data = {
"permissions_mask": permissions_mask,
"server_quantity": server_quantity,
}
self.controller.users.update_user(
user_id, user_data=user_data, user_crafty_data=user_crafty_data
)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Edited user {username} (UID:{user_id}) with roles {roles} "
f"and permissions {permissions_mask}",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.redirect("/panel/panel_config")
elif page == "edit_user_apikeys":
user_id = self.get_argument("id", None)
name = self.get_argument("name", None)
superuser = self.get_argument("superuser", None) == "1"
if name is None or name == "":
self.redirect("/panel/error?error=Invalid API key name")
return
if user_id is None:
self.redirect("/panel/error?error=Invalid User ID")
return
# does this user id exist?
if not self.controller.users.user_id_exists(user_id):
self.redirect("/panel/error?error=Invalid User ID")
return
if str(user_id) != str(exec_user["user_id"]) and not exec_user["superuser"]:
self.redirect(
"/panel/error?error=You do not have access to change"
+ "this user's api key."
)
return
crafty_permissions_mask = self.get_perms()
server_permissions_mask = self.get_perms_server()
self.controller.users.add_user_api_key(
name,
user_id,
superuser,
server_permissions_mask,
crafty_permissions_mask,
)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Added API key {name} with crafty permissions "
f"{crafty_permissions_mask}"
f" and {server_permissions_mask} for user with UID: {user_id}",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.redirect(f"/panel/edit_user_apikeys?id={user_id}")
elif page == "get_token":
key_id = self.get_argument("id", None)
if key_id is None:
self.redirect("/panel/error?error=Invalid Key ID")
return
key = self.controller.users.get_user_api_key(key_id)
# does this user id exist?
if key is None:
self.redirect("/panel/error?error=Invalid Key ID")
return
if (
str(key.user_id) != str(exec_user["user_id"])
and not exec_user["superuser"]
):
self.redirect(
"/panel/error?error=You are not authorized to access this key."
)
return
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Generated a new API token for the key {key.name} "
f"from user with UID: {key.user_id}",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.write(
self.controller.authentication.generate(
key.user_id_id, {"token_id": key.token_id}
)
)
self.finish()
elif page == "add_user":
username = bleach.clean(self.get_argument("username", None).lower())
if username.lower() == "system":
self.redirect(
"/panel/error?error=Unauthorized access: "
"username system is reserved for the Crafty system."
" Please choose a different username."
)
return
password0 = bleach.clean(self.get_argument("password0", None))
password1 = bleach.clean(self.get_argument("password1", None))
email = bleach.clean(self.get_argument("email", "default@example.com"))
enabled = int(float(self.get_argument("enabled", "0")))
theme = bleach.clean(self.get_argument("theme"), "default")
hints = True
lang = bleach.clean(
self.get_argument("lang", self.helper.get_setting("language"))
)
# We don't want a non-super user to be able to create a super user.
if superuser:
new_superuser = int(bleach.clean(self.get_argument("superuser", "0")))
else:
new_superuser = 0
if EnumPermissionsCrafty.USER_CONFIG not in exec_user_crafty_permissions:
self.redirect(
"/panel/error?error=Unauthorized access: not a user editor"
)
return
if (
not self.controller.crafty_perms.can_add_user(exec_user["user_id"])
and not exec_user["superuser"]
):
self.redirect(
"/panel/error?error=Unauthorized access: quantity limit reached"
)
return
if username is None or username == "":
self.redirect("/panel/error?error=Invalid username")
return
if exec_user["superuser"]:
manager = self.get_argument("manager")
if manager == "":
manager = None
else:
manager = int(manager)
else:
manager = int(exec_user["user_id"])
# does this user id exist?
if self.controller.users.get_id_by_name(username) is not None:
self.redirect("/panel/error?error=User exists")
return
if password0 != password1:
self.redirect("/panel/error?error=Passwords must match")
return
roles = self.get_user_role_memberships()
permissions_mask, server_quantity = self.get_perms_quantity()
user_id = self.controller.users.add_user(
username,
manager=manager,
password=password0,
email=email,
enabled=enabled,
superuser=new_superuser,
theme=theme,
)
user_data = {"roles": roles, "lang": lang, "hints": True}
user_crafty_data = {
"permissions_mask": permissions_mask,
"server_quantity": server_quantity,
}
self.controller.users.update_user(
user_id, user_data=user_data, user_crafty_data=user_crafty_data
)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Added user {username} (UID:{user_id})",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Edited user {username} (UID:{user_id}) with roles {roles}",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.redirect("/panel/panel_config")
elif page == "edit_role":
role_id = bleach.clean(self.get_argument("id", None))
role_name = bleach.clean(self.get_argument("role_name", None))
role = self.controller.roles.get_role(role_id)
if (
EnumPermissionsCrafty.ROLES_CONFIG not in exec_user_crafty_permissions
and exec_user["user_id"] != role["manager"]
and not exec_user["superuser"]
):
self.redirect(
"/panel/error?error=Unauthorized access: not a role editor"
)
return
if role_name is None or role_name == "":
self.redirect("/panel/error?error=Invalid username")
return
if role_id is None:
self.redirect("/panel/error?error=Invalid Role ID")
return
# does this user id exist?
if not self.controller.roles.role_id_exists(role_id):
self.redirect("/panel/error?error=Invalid Role ID")
return
if exec_user["superuser"]:
manager = self.get_argument("manager", None)
if manager == "":
manager = None
else:
manager = role["manager"]
servers = self.get_role_servers()
self.controller.roles.update_role_advanced(
role_id, role_name, servers, manager
)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"edited role {role_name} (RID:{role_id}) with servers {servers}",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.redirect("/panel/panel_config")
elif page == "add_role":
role_name = bleach.clean(self.get_argument("role_name", None))
if exec_user["superuser"]:
manager = self.get_argument("manager", None)
if manager == "":
manager = None
else:
manager = exec_user["user_id"]
if EnumPermissionsCrafty.ROLES_CONFIG not in exec_user_crafty_permissions:
self.redirect(
"/panel/error?error=Unauthorized access: not a role editor"
)
return
if (
not self.controller.crafty_perms.can_add_role(exec_user["user_id"])
and not exec_user["superuser"]
):
self.redirect(
"/panel/error?error=Unauthorized access: quantity limit reached"
)
return
if role_name is None or role_name == "":
self.redirect("/panel/error?error=Invalid role name")
return
# does this user id exist?
if self.controller.roles.get_roleid_by_name(role_name) is not None:
self.redirect("/panel/error?error=Role exists")
return
servers = self.get_role_servers()
role_id = self.controller.roles.add_role_advanced(
role_name, servers, manager
)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"created role {role_name} (RID:{role_id})",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.redirect("/panel/panel_config")
else:
self.set_status(404)
page_data = {
"lang": self.helper.get_setting("language"),
"lang_page": Helpers.get_lang_page(self.helper.get_setting("language")),
}
self.render(
"public/404.html", translate=self.translator.translate, data=page_data
)
@tornado.web.authenticated
def delete(self, page):
api_key, _token_data, exec_user = self.current_user
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
page_data = {
# todo: make this actually pull and compare version data
"update_available": False,
"version_data": self.helper.get_version_string(),
"user_data": exec_user,
"hosts_data": self.controller.management.get_latest_hosts_stats(),
"show_contribute": self.helper.get_setting("show_contribute_link", True),
"lang": self.controller.users.get_user_lang_by_id(exec_user["user_id"]),
"lang_page": Helpers.get_lang_page(
self.controller.users.get_user_lang_by_id(exec_user["user_id"])
),
}
if page == "remove_apikey":
key_id = bleach.clean(self.get_argument("id", None))
if not superuser:
self.redirect("/panel/error?error=Unauthorized access: not superuser")
return
if key_id is None or self.controller.users.get_user_api_key(key_id) is None:
self.redirect("/panel/error?error=Invalid Key ID")
return
# does this user id exist?
target_key = self.controller.users.get_user_api_key(key_id)
if not target_key:
self.redirect("/panel/error?error=Invalid Key ID")
return
key_obj = self.controller.users.get_user_api_key(key_id)
if key_obj.user_id != exec_user["user_id"] and not exec_user["superuser"]:
self.redirect(
"/panel/error?error=You do not have access to change"
+ "this user's api key."
)
return
self.controller.users.delete_user_api_key(key_id)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"Removed API key {target_key} "
f"(ID: {key_id}) from user {exec_user['user_id']}",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.finish()
self.redirect(f"/panel/edit_user_apikeys?id={key_obj.user_id}")
else:
self.set_status(404)
self.render(
"public/404.html",
data=page_data,
translate=self.translator.translate,
)

View File

@ -26,6 +26,17 @@ from app.classes.web.routes.api.servers.server.stdin import ApiServersServerStdi
from app.classes.web.routes.api.servers.server.tasks.index import ( from app.classes.web.routes.api.servers.server.tasks.index import (
ApiServersServerTasksIndexHandler, ApiServersServerTasksIndexHandler,
) )
from app.classes.web.routes.api.servers.server.backups.index import (
ApiServersServerBackupsIndexHandler,
)
from app.classes.web.routes.api.servers.server.backups.backup.index import (
ApiServersServerBackupsBackupIndexHandler,
)
from app.classes.web.routes.api.servers.server.files import (
ApiServersServerFilesIndexHandler,
ApiServersServerFilesCreateHandler,
ApiServersServerFilesZipHandler,
)
from app.classes.web.routes.api.servers.server.tasks.task.children import ( from app.classes.web.routes.api.servers.server.tasks.task.children import (
ApiServersServerTasksTaskChildrenHandler, ApiServersServerTasksTaskChildrenHandler,
) )
@ -38,8 +49,22 @@ from app.classes.web.routes.api.users.user.index import ApiUsersUserIndexHandler
from app.classes.web.routes.api.users.user.permissions import ( from app.classes.web.routes.api.users.user.permissions import (
ApiUsersUserPermissionsHandler, ApiUsersUserPermissionsHandler,
) )
from app.classes.web.routes.api.users.user.api import ApiUsersUserKeyHandler
from app.classes.web.routes.api.users.user.pfp import ApiUsersUserPfpHandler from app.classes.web.routes.api.users.user.pfp import ApiUsersUserPfpHandler
from app.classes.web.routes.api.users.user.public import ApiUsersUserPublicHandler from app.classes.web.routes.api.users.user.public import ApiUsersUserPublicHandler
from app.classes.web.routes.api.crafty.announcements.index import (
ApiAnnounceIndexHandler,
)
from app.classes.web.routes.api.crafty.config.index import (
ApiCraftyConfigIndexHandler,
ApiCraftyCustomizeIndexHandler,
)
from app.classes.web.routes.api.crafty.config.server_dir import (
ApiCraftyConfigServerDirHandler,
)
from app.classes.web.routes.api.crafty.clogs.index import ApiCraftyLogIndexHandler
from app.classes.web.routes.api.crafty.imports.index import ApiImportFilesIndexHandler
from app.classes.web.routes.api.crafty.exe_cache import ApiCraftyExeCacheIndexHandler
def api_handlers(handler_args): def api_handlers(handler_args):
@ -55,12 +80,52 @@ def api_handlers(handler_args):
ApiAuthInvalidateTokensHandler, ApiAuthInvalidateTokensHandler,
handler_args, handler_args,
), ),
(
r"/api/v2/crafty/announcements/?",
ApiAnnounceIndexHandler,
handler_args,
),
(
r"/api/v2/crafty/config/?",
ApiCraftyConfigIndexHandler,
handler_args,
),
(
r"/api/v2/crafty/config/customize/?",
ApiCraftyCustomizeIndexHandler,
handler_args,
),
(
r"/api/v2/crafty/config/servers_dir/?",
ApiCraftyConfigServerDirHandler,
handler_args,
),
(
r"/api/v2/crafty/logs/([a-z0-9_]+)/?",
ApiCraftyLogIndexHandler,
handler_args,
),
(
r"/api/v2/import/file/unzip/?",
ApiImportFilesIndexHandler,
handler_args,
),
# User routes # User routes
( (
r"/api/v2/users/?", r"/api/v2/users/?",
ApiUsersIndexHandler, ApiUsersIndexHandler,
handler_args, handler_args,
), ),
(
r"/api/v2/users/([0-9]+)/key/?",
ApiUsersUserKeyHandler,
handler_args,
),
(
r"/api/v2/users/([0-9]+)/key/([0-9]+)/?",
ApiUsersUserKeyHandler,
handler_args,
),
( (
r"/api/v2/users/([0-9]+)/?", r"/api/v2/users/([0-9]+)/?",
ApiUsersUserIndexHandler, ApiUsersUserIndexHandler,
@ -107,11 +172,41 @@ def api_handlers(handler_args):
ApiServersIndexHandler, ApiServersIndexHandler,
handler_args, handler_args,
), ),
(
r"/api/v2/crafty/exeCache/?",
ApiCraftyExeCacheIndexHandler,
handler_args,
),
( (
r"/api/v2/servers/([0-9]+)/?", r"/api/v2/servers/([0-9]+)/?",
ApiServersServerIndexHandler, ApiServersServerIndexHandler,
handler_args, handler_args,
), ),
(
r"/api/v2/servers/([0-9]+)/backups/?",
ApiServersServerBackupsIndexHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/backups/backup/?",
ApiServersServerBackupsBackupIndexHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/files/?",
ApiServersServerFilesIndexHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/files/create/?",
ApiServersServerFilesCreateHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/files/zip/?",
ApiServersServerFilesZipHandler,
handler_args,
),
( (
r"/api/v2/servers/([0-9]+)/tasks/?", r"/api/v2/servers/([0-9]+)/tasks/?",
ApiServersServerTasksIndexHandler, ApiServersServerTasksIndexHandler,

View File

@ -0,0 +1,110 @@
import logging
import json
from jsonschema import ValidationError, validate
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
notif_schema = {
"type": "object",
"properties": {
"id": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
}
class ApiAnnounceIndexHandler(BaseApiHandler):
def get(self):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_exec_user_crafty_permissions,
_,
_,
_user,
) = auth_data
data = self.helper.get_announcements()
cleared = str(
self.controller.users.get_user_by_id(auth_data[4]["user_id"])[
"cleared_notifs"
]
).split(",")
res = [d.get("id", None) for d in data]
# remove notifs that are no longer in Crafty.
for item in cleared[:]:
if item not in res:
cleared.remove(item)
updata = {"cleared_notifs": ",".join(cleared)}
self.controller.users.update_user(auth_data[4]["user_id"], updata)
if len(cleared) > 0:
for item in data[:]:
if item["id"] in cleared:
data.remove(item)
self.finish_json(
200,
{
"status": "ok",
"data": data,
},
)
def post(self):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_exec_user_crafty_permissions,
_,
_,
_user,
) = auth_data
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, notif_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
announcements = self.helper.get_announcements()
res = [d.get("id", None) for d in announcements]
cleared_notifs = str(
self.controller.users.get_user_by_id(auth_data[4]["user_id"])[
"cleared_notifs"
]
).split(",")
# remove notifs that are no longer in Crafty.
for item in cleared_notifs[:]:
if item not in res:
cleared_notifs.remove(item)
if str(data["id"]) in str(res):
cleared_notifs.append(data["id"])
else:
self.finish_json(200, {"status": "error", "error": "INVALID_DATA"})
return
updata = {"cleared_notifs": ",".join(cleared_notifs)}
self.controller.users.update_user(auth_data[4]["user_id"], updata)
self.finish_json(
200,
{
"status": "ok",
"data": {},
},
)

View File

@ -0,0 +1,34 @@
from app.classes.web.base_api_handler import BaseApiHandler
class ApiCraftyLogIndexHandler(BaseApiHandler):
def get(self, log_type: str):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
superuser,
_,
) = auth_data
if not superuser:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
log_types = ["audit", "session", "schedule"]
if log_type not in log_types:
raise NotImplementedError
if log_type == "audit":
return self.finish_json(
200,
{"status": "ok", "data": self.controller.management.get_activity_log()},
)
if log_type == "session":
raise NotImplementedError
if log_type == "schedule":
raise NotImplementedError

View File

@ -0,0 +1,312 @@
import os
import json
from jsonschema import ValidationError, validate
import orjson
from playhouse.shortcuts import model_to_dict
from app.classes.shared.file_helpers import FileHelpers
from app.classes.web.base_api_handler import BaseApiHandler
config_json_schema = {
"type": "object",
"properties": {
"http_port": {"type": "integer"},
"https_port": {"type": "integer"},
"language": {
"type": "string",
},
"cookie_expire": {"type": "integer"},
"show_errors": {"type": "boolean"},
"history_max_age": {"type": "integer"},
"stats_update_frequency_seconds": {"type": "integer"},
"delete_default_json": {"type": "boolean"},
"show_contribute_link": {"type": "boolean"},
"virtual_terminal_lines": {"type": "integer"},
"max_log_lines": {"type": "integer"},
"max_audit_entries": {"type": "integer"},
"disabled_language_files": {"type": "array"},
"stream_size_GB": {"type": "integer"},
"keywords": {"type": "array"},
"allow_nsfw_profile_pictures": {"type": "boolean"},
"enable_user_self_delete": {"type": "boolean"},
"reset_secrets_on_next_boot": {"type": "boolean"},
"monitored_mounts": {"type": "array"},
"dir_size_poll_freq_minutes": {"type": "integer"},
"crafty_logs_delete_after_days": {"type": "integer"},
},
"additionalProperties": False,
"minProperties": 1,
}
customize_json_schema = {
"type": "object",
"properties": {
"photo": {"type": "string"},
"opacity": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
}
photo_delete_schema = {
"type": "object",
"properties": {
"photo": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
}
DEFAULT_PHOTO = "login_1.jpg"
class ApiCraftyConfigIndexHandler(BaseApiHandler):
def get(self):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
superuser,
_,
) = auth_data
# GET /api/v2/roles?ids=true
get_only_ids = self.get_query_argument("ids", None) == "true"
if not superuser:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.finish_json(
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()],
},
)
def patch(self):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
superuser,
user,
) = auth_data
if not superuser:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = orjson.loads(self.request.body)
except orjson.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, config_json_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
self.controller.set_config_json(data)
self.controller.management.add_to_audit_log(
user["user_id"],
"edited config.json",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.finish_json(
200,
{"status": "ok"},
)
class ApiCraftyCustomizeIndexHandler(BaseApiHandler):
def get(self):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
superuser,
_,
) = auth_data
# GET /api/v2/roles?ids=true
get_only_ids = self.get_query_argument("ids", None) == "true"
if not superuser:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.finish_json(
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()],
},
)
def patch(self):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
superuser,
user,
) = auth_data
if not superuser:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = orjson.loads(self.request.body)
except orjson.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, customize_json_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
if not self.helper.validate_traversal(
os.path.join(
self.controller.project_root,
"app/frontend/static/assets/images/auth/",
),
os.path.join(
self.controller.project_root,
f"app/frontend/static/assets/images/auth/{data['photo']}",
),
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": "TRIED TO REACH FILES THAT ARE"
" NOT SUPPOSED TO BE ACCESSIBLE",
},
)
self.controller.management.add_to_audit_log(
user["user_id"],
f"customized login photo: {data['photo']}/{data['opacity']}",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.controller.management.set_login_opacity(int(data["opacity"]))
if data["photo"] == DEFAULT_PHOTO:
self.controller.management.set_login_image(DEFAULT_PHOTO)
self.controller.cached_login = f"{data['photo']}"
else:
self.controller.management.set_login_image(f"custom/{data['photo']}")
self.controller.cached_login = f"custom/{data['photo']}"
self.finish_json(
200,
{
"status": "ok",
"data": {"photo": data["photo"], "opacity": data["opacity"]},
},
)
def delete(self):
auth_data = self.authenticate_user()
if not auth_data:
return
if not auth_data[4]["superuser"]:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, photo_delete_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
if not self.helper.validate_traversal(
os.path.join(
self.controller.project_root,
"app",
"frontend",
"/static/assets/images/auth/",
),
os.path.join(
self.controller.project_root,
"app",
"frontend",
"/static/assets/images/auth/",
data["photo"],
),
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": "TRIED TO REACH FILES THAT ARE"
" NOT SUPPOSED TO BE ACCESSIBLE",
},
)
if data["photo"] == DEFAULT_PHOTO:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID FILE",
"error_data": "CANNOT DELETE DEFAULT",
},
)
FileHelpers.del_file(
os.path.join(
self.controller.project_root,
f"app/frontend/static/assets/images/auth/custom/{data['photo']}",
)
)
current = self.controller.cached_login
split = current.split("/")
if len(split) == 1:
current_photo = current
else:
current_photo = split[1]
if current_photo == data["photo"]:
self.controller.management.set_login_image(DEFAULT_PHOTO)
self.controller.cached_login = DEFAULT_PHOTO
return self.finish_json(200, {"status": "ok"})

View File

@ -0,0 +1,115 @@
from jsonschema import ValidationError, validate
import orjson
from playhouse.shortcuts import model_to_dict
from app.classes.web.base_api_handler import BaseApiHandler
server_dir_schema = {
"type": "object",
"properties": {
"new_dir": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
}
class ApiCraftyConfigServerDirHandler(BaseApiHandler):
def get(self):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
superuser,
_,
) = auth_data
# GET /api/v2/roles?ids=true
get_only_ids = self.get_query_argument("ids", None) == "true"
if not superuser:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.finish_json(
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()],
},
)
def patch(self):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
_,
_,
) = auth_data
if not auth_data:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if not auth_data[4]["superuser"]:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if self.helper.is_env_docker():
raise NotImplementedError
try:
data = orjson.loads(self.request.body)
except orjson.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, server_dir_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
if self.helper.dir_migration:
return self.finish_json(
400,
{
"status": "error",
"error": "IN PROGRESS",
"error_data": "Migration already in progress. Please be patient",
},
)
for server in self.controller.servers.get_all_servers_stats():
if server["stats"]["running"]:
return self.finish_json(
400,
{
"status": "error",
"error": "SERVER RUNNING",
},
)
new_dir = data["new_dir"]
self.controller.update_master_server_dir(new_dir, auth_data[4]["user_id"])
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"updated master servers dir to {new_dir}/servers",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.finish_json(
200,
{"status": "ok"},
)

View File

@ -0,0 +1,27 @@
from app.classes.web.base_api_handler import BaseApiHandler
class ApiCraftyExeCacheIndexHandler(BaseApiHandler):
def get(self):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_,
_,
_,
_,
) = auth_data
if not auth_data[4]["superuser"]:
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.controller.server_jars.manual_refresh_cache()
self.finish_json(
200,
{
"status": "ok",
"data": self.controller.server_jars.get_serverjar_data(),
},
)

View File

@ -0,0 +1,128 @@
import os
import logging
import json
import html
from jsonschema import validate
from jsonschema.exceptions import ValidationError
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.shared.helpers import Helpers
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
files_get_schema = {
"type": "object",
"properties": {
"page": {"type": "string", "minLength": 1},
"folder": {"type": "string"},
"upload": {"type": "boolean", "default": "False"},
"unzip": {"type": "boolean", "default": "True"},
},
"additionalProperties": False,
"minProperties": 1,
}
class ApiImportFilesIndexHandler(BaseApiHandler):
def post(self):
auth_data = self.authenticate_user()
if not auth_data:
return
if (
EnumPermissionsCrafty.SERVER_CREATION
not in self.controller.crafty_perms.get_crafty_permissions_list(
auth_data[4]["user_id"]
)
and not auth_data[4]["superuser"]
):
# if the user doesn't have Files or Backup permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, files_get_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
# TODO: limit some columns for specific permissions?
folder = data["folder"]
user_id = auth_data[4]["user_id"]
root_path = False
if data["unzip"]:
# This is awful. Once uploads go to return
# JSON we need to remove this and just send
# the path.
if data["upload"]:
folder = os.path.join(self.controller.project_root, "imports", folder)
if Helpers.check_file_exists(folder):
folder = self.file_helper.unzip_server(folder, user_id)
root_path = True
else:
if user_id:
user_lang = self.controller.users.get_user_lang_by_id(user_id)
self.helper.websocket_helper.broadcast_user(
user_id,
"send_start_error",
{
"error": self.helper.translation.translate(
"error", "no-file", user_lang
)
},
)
else:
if not self.helper.check_path_exists(folder) and user_id:
user_lang = self.controller.users.get_user_lang_by_id(user_id)
self.helper.websocket_helper.broadcast_user(
user_id,
"send_start_error",
{
"error": self.helper.translation.translate(
"error", "no-file", user_lang
)
},
)
return_json = {
"root_path": {
"path": folder,
"top": root_path,
}
}
dir_list = []
unsorted_files = []
file_list = os.listdir(folder)
for item in file_list:
if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item)
else:
unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted(
unsorted_files, key=str.casefold
)
for raw_filename in file_list:
filename = html.escape(raw_filename)
rel = os.path.join(folder, raw_filename)
dpath = os.path.join(folder, filename)
dpath = self.helper.wtol_path(dpath)
if os.path.isdir(rel):
return_json[filename] = {
"path": dpath,
"dir": True,
}
else:
return_json[filename] = {
"path": dpath,
"dir": False,
}
self.finish_json(200, {"status": "ok", "data": return_json})

View File

@ -28,9 +28,39 @@ create_role_schema = {
"required": ["server_id", "permissions"], "required": ["server_id", "permissions"],
}, },
}, },
"manager": {"type": ["integer", "null"]},
}, },
"required": ["name"],
"additionalProperties": False, "additionalProperties": False,
"minProperties": 1,
}
basic_create_role_schema = {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
},
"servers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"server_id": {
"type": "integer",
"minimum": 1,
},
"permissions": {
"type": "string",
"pattern": "^[01]{8}$", # 8 bits, see EnumPermissionsServer
},
},
"required": ["server_id", "permissions"],
},
},
},
"additionalProperties": False,
"minProperties": 1,
} }
@ -86,7 +116,10 @@ class ApiRolesIndexHandler(BaseApiHandler):
) )
try: try:
validate(data, create_role_schema) if auth_data[4]["superuser"]:
validate(data, create_role_schema)
else:
validate(data, basic_create_role_schema)
except ValidationError as e: except ValidationError as e:
return self.finish_json( return self.finish_json(
400, 400,
@ -98,6 +131,9 @@ class ApiRolesIndexHandler(BaseApiHandler):
) )
role_name = data["name"] role_name = data["name"]
manager = data.get("manager", None)
if manager == self.controller.users.get_id_by_name("SYSTEM") or manager == 0:
manager = None
# Get the servers # Get the servers
servers_dict = {server["server_id"]: server for server in data["servers"]} servers_dict = {server["server_id"]: server for server in data["servers"]}
@ -116,9 +152,7 @@ class ApiRolesIndexHandler(BaseApiHandler):
400, {"status": "error", "error": "ROLE_NAME_ALREADY_EXISTS"} 400, {"status": "error", "error": "ROLE_NAME_ALREADY_EXISTS"}
) )
role_id = self.controller.roles.add_role_advanced( role_id = self.controller.roles.add_role_advanced(role_name, servers, manager)
role_name, servers, user["user_id"]
)
self.controller.management.add_to_audit_log( self.controller.management.add_to_audit_log(
user["user_id"], user["user_id"],

View File

@ -153,9 +153,18 @@ class ApiRolesRoleIndexHandler(BaseApiHandler):
}, },
) )
manager = data.get(
"manager", self.controller.roles.get_role(role_id)["manager"]
)
if manager == self.controller.users.get_id_by_name("system") or manager == 0:
manager = None
try: try:
self.controller.roles.update_role_advanced( self.controller.roles.update_role_advanced(
role_id, data.get("role_name", None), data.get("servers", None) role_id,
data.get("name", None),
data.get("servers", None),
manager,
) )
except DoesNotExist: except DoesNotExist:
return self.finish_json(404, {"status": "error", "error": "ROLE_NOT_FOUND"}) return self.finish_json(404, {"status": "error", "error": "ROLE_NOT_FOUND"})

View File

@ -24,6 +24,7 @@ new_server_schema = {
"examples": ["My Server"], "examples": ["My Server"],
"minLength": 2, "minLength": 2,
}, },
"roles": {"title": "Roles to add", "type": "array", "examples": [1, 2, 3]},
"stop_command": { "stop_command": {
"title": "Stop command", "title": "Stop command",
"description": '"" means the default for the server creation type.', "description": '"" means the default for the server creation type.',
@ -133,8 +134,13 @@ new_server_schema = {
"mem_min", "mem_min",
"mem_max", "mem_max",
"server_properties_port", "server_properties_port",
"agree_to_eula", "category",
], ],
"category": {
"title": "Jar Category",
"type": "string",
"examples": ["modded", "vanilla"],
},
"properties": { "properties": {
"type": { "type": {
"title": "Server JAR Type", "title": "Server JAR Type",
@ -185,7 +191,6 @@ new_server_schema = {
"mem_min", "mem_min",
"mem_max", "mem_max",
"server_properties_port", "server_properties_port",
"agree_to_eula",
], ],
"properties": { "properties": {
"existing_server_path": { "existing_server_path": {
@ -240,7 +245,6 @@ new_server_schema = {
"mem_min", "mem_min",
"mem_max", "mem_max",
"server_properties_port", "server_properties_port",
"agree_to_eula",
], ],
"properties": { "properties": {
"zip_path": { "zip_path": {
@ -336,12 +340,24 @@ new_server_schema = {
"title": "Creation type", "title": "Creation type",
"type": "string", "type": "string",
"default": "import_server", "default": "import_server",
"enum": ["import_server", "import_zip"], "enum": ["download_exe", "import_server", "import_zip"],
},
"download_exe_create_data": {
"title": "Import server data",
"type": "object",
"required": [],
"properties": {
"agree_to_eula": {
"title": "Agree to the EULA",
"type": "boolean",
"enum": [True],
},
},
}, },
"import_server_create_data": { "import_server_create_data": {
"title": "Import server data", "title": "Import server data",
"type": "object", "type": "object",
"required": ["existing_server_path", "command"], "required": ["existing_server_path", "executable"],
"properties": { "properties": {
"existing_server_path": { "existing_server_path": {
"title": "Server path", "title": "Server path",
@ -350,6 +366,14 @@ new_server_schema = {
"examples": ["/var/opt/server"], "examples": ["/var/opt/server"],
"minLength": 1, "minLength": 1,
}, },
"executable": {
"title": "Executable File",
"description": "File Crafty should execute"
"on server launch",
"type": "string",
"examples": ["bedrock_server.exe"],
"minlength": 1,
},
"command": { "command": {
"title": "Command", "title": "Command",
"type": "string", "type": "string",
@ -371,6 +395,14 @@ new_server_schema = {
"examples": ["/var/opt/server.zip"], "examples": ["/var/opt/server.zip"],
"minLength": 1, "minLength": 1,
}, },
"executable": {
"title": "Executable File",
"description": "File Crafty should execute"
"on server launch",
"type": "string",
"examples": ["bedrock_server.exe"],
"minlength": 1,
},
"zip_root": { "zip_root": {
"title": "Server root directory", "title": "Server root directory",
"description": "The server root in the ZIP archive", "description": "The server root in the ZIP archive",
@ -394,7 +426,9 @@ new_server_schema = {
"allOf": [ "allOf": [
{ {
"if": { "if": {
"properties": {"create_type": {"const": "import_exec"}} "properties": {
"create_type": {"const": "import_server"}
}
}, },
"then": {"required": ["import_server_create_data"]}, "then": {"required": ["import_server_create_data"]},
}, },
@ -404,6 +438,16 @@ new_server_schema = {
}, },
"then": {"required": ["import_zip_create_data"]}, "then": {"required": ["import_zip_create_data"]},
}, },
{
"if": {
"properties": {"create_type": {"const": "download_exe"}}
},
"then": {
"required": [
"download_exe_create_data",
]
},
},
], ],
}, },
{ {
@ -411,6 +455,7 @@ new_server_schema = {
"oneOf": [ "oneOf": [
{"required": ["import_server_create_data"]}, {"required": ["import_server_create_data"]},
{"required": ["import_zip_create_data"]}, {"required": ["import_zip_create_data"]},
{"required": ["download_exe_create_data"]},
], ],
}, },
], ],
@ -651,7 +696,6 @@ class ApiServersIndexHandler(BaseApiHandler):
return self.finish_json( return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)} 400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
) )
try: try:
validate(data, new_server_schema) validate(data, new_server_schema)
except ValidationError as e: except ValidationError as e:

View File

@ -31,6 +31,8 @@ class ApiServersServerActionHandler(BaseApiHandler):
if action == "clone_server": if action == "clone_server":
return self._clone_server(server_id, auth_data[4]["user_id"]) return self._clone_server(server_id, auth_data[4]["user_id"])
if action == "eula":
return self._agree_eula(server_id, auth_data[4]["user_id"])
self.controller.management.send_command( self.controller.management.send_command(
auth_data[4]["user_id"], server_id, self.get_remote_ip(), action auth_data[4]["user_id"], server_id, self.get_remote_ip(), action
@ -41,6 +43,11 @@ class ApiServersServerActionHandler(BaseApiHandler):
{"status": "ok"}, {"status": "ok"},
) )
def _agree_eula(self, server_id, user):
svr = self.controller.servers.get_server_instance_by_id(server_id)
svr.agree_eula(user)
return self.finish_json(200, {"status": "ok"})
def _clone_server(self, server_id, user_id): def _clone_server(self, server_id, user_id):
def is_name_used(name): def is_name_used(name):
return Servers.select().where(Servers.server_name == name).exists() return Servers.select().where(Servers.server_name == name).exists()

View File

@ -0,0 +1,210 @@
import logging
import json
import os
from apscheduler.jobstores.base import JobLookupError
from jsonschema import validate
from jsonschema.exceptions import ValidationError
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.shared.file_helpers import FileHelpers
from app.classes.web.base_api_handler import BaseApiHandler
from app.classes.shared.helpers import Helpers
logger = logging.getLogger(__name__)
backup_schema = {
"type": "object",
"properties": {
"filename": {"type": "string", "minLength": 5},
},
"additionalProperties": False,
"minProperties": 1,
}
class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
def get(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if (
EnumPermissionsServer.BACKUP
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.finish_json(200, self.controller.management.get_backup_config(server_id))
def delete(self, server_id: str):
auth_data = self.authenticate_user()
backup_conf = self.controller.management.get_backup_config(server_id)
if not auth_data:
return
if (
EnumPermissionsServer.BACKUP
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, backup_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
try:
FileHelpers.del_file(
os.path.join(backup_conf["backup_path"], data["filename"])
)
except Exception:
return self.finish_json(
400, {"status": "error", "error": "NO BACKUP FOUND"}
)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"Edited server {server_id}: removed backup {data['filename']}",
server_id,
self.get_remote_ip(),
)
return self.finish_json(200, {"status": "ok"})
def post(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if (
EnumPermissionsServer.BACKUP
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, backup_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
try:
svr_obj = self.controller.servers.get_server_obj(server_id)
server_data = self.controller.servers.get_server_data_by_id(server_id)
zip_name = data["filename"]
# import the server again based on zipfile
if server_data["type"] == "minecraft-java":
backup_path = svr_obj.backup_path
if Helpers.validate_traversal(backup_path, zip_name):
temp_dir = Helpers.unzip_backup_archive(backup_path, zip_name)
new_server = self.controller.import_zip_server(
svr_obj.server_name,
temp_dir,
server_data["executable"],
"1",
"2",
server_data["server_port"],
server_data["created_by"],
)
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"]
)
# preserve current schedules
for schedule in self.controller.management.get_schedules_by_server(
server_id
):
self.tasks_manager.update_job(
schedule.schedule_id, {"server_id": new_server_id}
)
# preserve execution command
new_server_obj = self.controller.servers.get_server_obj(
new_server_id
)
new_server_obj.execution_command = server_data["execution_command"]
# reset executable path
if svr_obj.path in svr_obj.executable:
new_server_obj.executable = str(svr_obj.executable).replace(
svr_obj.path, new_server_obj.path
)
# reset run command path
if svr_obj.path in svr_obj.execution_command:
new_server_obj.execution_command = str(
svr_obj.execution_command
).replace(svr_obj.path, new_server_obj.path)
# reset log path
if svr_obj.path in svr_obj.log_path:
new_server_obj.log_path = str(svr_obj.log_path).replace(
svr_obj.path, new_server_obj.path
)
self.controller.servers.update_server(new_server_obj)
# preserve backup config
backup_config = self.controller.management.get_backup_config(
server_id
)
excluded_dirs = []
server_obj = self.controller.servers.get_server_obj(server_id)
loop_backup_path = self.helper.wtol_path(server_obj.path)
for item in self.controller.management.get_excluded_backup_dirs(
server_id
):
item_path = self.helper.wtol_path(item)
bu_path = os.path.relpath(item_path, loop_backup_path)
bu_path = os.path.join(new_server_obj.path, bu_path)
excluded_dirs.append(bu_path)
self.controller.management.set_backup_config(
new_server_id,
new_server_obj.backup_path,
backup_config["max_backups"],
excluded_dirs,
backup_config["compress"],
backup_config["shutdown"],
)
# remove old server's tasks
try:
self.tasks_manager.remove_all_server_tasks(server_id)
except JobLookupError as e:
logger.info("No active tasks found for server: {e}")
self.controller.remove_server(server_id, True)
except Exception:
return self.finish_json(
400, {"status": "error", "error": "NO BACKUP FOUND"}
)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"Restored server {server_id} backup {data['filename']}",
server_id,
self.get_remote_ip(),
)
return self.finish_json(200, {"status": "ok"})

View File

@ -0,0 +1,123 @@
import logging
import json
from jsonschema import validate
from jsonschema.exceptions import ValidationError
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
backup_patch_schema = {
"type": "object",
"properties": {
"backup_path": {"type": "string", "minLength": 1},
"max_backups": {"type": "integer"},
"compress": {"type": "boolean"},
"shutdown": {"type": "boolean"},
"backup_before": {"type": "string"},
"backup_after": {"type": "string"},
"exclusions": {"type": "array"},
},
"additionalProperties": False,
"minProperties": 1,
}
basic_backup_patch_schema = {
"type": "object",
"properties": {
"max_backups": {"type": "integer"},
"compress": {"type": "boolean"},
"shutdown": {"type": "boolean"},
"backup_before": {"type": "string"},
"backup_after": {"type": "string"},
"exclusions": {"type": "array"},
},
"additionalProperties": False,
"minProperties": 1,
}
class ApiServersServerBackupsIndexHandler(BaseApiHandler):
def get(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if (
EnumPermissionsServer.BACKUP
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.finish_json(200, self.controller.management.get_backup_config(server_id))
def patch(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
if auth_data[4]["superuser"]:
validate(data, backup_patch_schema)
else:
validate(data, basic_backup_patch_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.BACKUP
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.controller.management.set_backup_config(
server_id,
data.get(
"backup_path",
self.controller.management.get_backup_config(server_id)["backup_path"],
),
data.get(
"max_backups",
self.controller.management.get_backup_config(server_id)["max_backups"],
),
data.get("exclusions"),
data.get(
"compress",
self.controller.management.get_backup_config(server_id)["compress"],
),
data.get(
"shutdown",
self.controller.management.get_backup_config(server_id)["shutdown"],
),
data.get(
"backup_before",
self.controller.management.get_backup_config(server_id)["before"],
),
data.get(
"backup_after",
self.controller.management.get_backup_config(server_id)["after"],
),
)
return self.finish_json(200, {"status": "ok"})

View File

@ -0,0 +1,555 @@
import os
import logging
import json
import html
from jsonschema import validate
from jsonschema.exceptions import ValidationError
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.shared.helpers import Helpers
from app.classes.shared.file_helpers import FileHelpers
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
files_get_schema = {
"type": "object",
"properties": {
"page": {"type": "string", "minLength": 1},
"path": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
}
files_patch_schema = {
"type": "object",
"properties": {
"path": {"type": "string"},
"contents": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
}
files_unzip_schema = {
"type": "object",
"properties": {
"folder": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
}
files_create_schema = {
"type": "object",
"properties": {
"parent": {"type": "string"},
"name": {"type": "string"},
"directory": {"type": "boolean"},
},
"additionalProperties": False,
"minProperties": 1,
}
files_rename_schema = {
"type": "object",
"properties": {
"path": {"type": "string"},
"new_name": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
}
file_delete_schema = {
"type": "object",
"properties": {
"filename": {"type": "string", "minLength": 5},
},
"additionalProperties": False,
"minProperties": 1,
}
class ApiServersServerFilesIndexHandler(BaseApiHandler):
def post(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.FILES
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
or EnumPermissionsServer.BACKUP
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Files or Backup permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, files_get_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
data["path"],
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
if os.path.isdir(data["path"]):
# TODO: limit some columns for specific permissions?
folder = data["path"]
return_json = {
"root_path": {
"path": folder,
"top": data["path"]
== self.controller.servers.get_server_data_by_id(server_id)["path"],
}
}
dir_list = []
unsorted_files = []
file_list = os.listdir(folder)
for item in file_list:
if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item)
else:
unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted(
unsorted_files, key=str.casefold
)
for raw_filename in file_list:
filename = html.escape(raw_filename)
rel = os.path.join(folder, raw_filename)
dpath = os.path.join(folder, filename)
if str(dpath) in self.controller.management.get_excluded_backup_dirs(
server_id
):
if os.path.isdir(rel):
return_json[filename] = {
"path": dpath,
"dir": True,
"excluded": True,
}
else:
return_json[filename] = {
"path": dpath,
"dir": False,
"excluded": True,
}
else:
if os.path.isdir(rel):
return_json[filename] = {
"path": dpath,
"dir": True,
"excluded": False,
}
else:
return_json[filename] = {
"path": dpath,
"dir": False,
"excluded": False,
}
self.finish_json(200, {"status": "ok", "data": return_json})
else:
try:
with open(data["path"], encoding="utf-8") as file:
file_contents = file.read()
except UnicodeDecodeError as ex:
self.finish_json(
400,
{"status": "error", "error": "DECODE_ERROR", "error_data": str(ex)},
)
self.finish_json(200, {"status": "ok", "data": file_contents})
def delete(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.FILES
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Files permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, file_delete_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
data["filename"],
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
if os.path.isdir(data["filename"]):
FileHelpers.del_dirs(data["filename"])
else:
FileHelpers.del_file(data["filename"])
return self.finish_json(200, {"status": "ok"})
def patch(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.FILES
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Files permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, files_patch_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
data["path"],
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
file_path = Helpers.get_os_understandable_path(data["path"])
file_contents = data["contents"]
# Open the file in write mode and store the content in file_object
with open(file_path, "w", encoding="utf-8") as file_object:
file_object.write(file_contents)
return self.finish_json(200, {"status": "ok"})
def put(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.FILES
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Files permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, files_create_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
path = os.path.join(data["parent"], data["name"])
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
path,
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
if Helpers.check_path_exists(os.path.abspath(path)):
return self.finish_json(
400,
{
"status": "error",
"error": "FILE EXISTS",
"error_data": str(e),
},
)
if data["directory"]:
os.mkdir(path)
else:
# Create the file by opening it
with open(path, "w", encoding="utf-8") as file_object:
file_object.close()
return self.finish_json(200, {"status": "ok"})
class ApiServersServerFilesCreateHandler(BaseApiHandler):
def patch(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.FILES
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Files permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, files_rename_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
path = data["path"]
new_item_name = data["new_name"]
new_item_path = os.path.join(os.path.split(path)[0], new_item_name)
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
path,
) or not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
new_item_path,
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
if Helpers.check_path_exists(os.path.abspath(new_item_path)):
return self.finish_json(
400,
{
"status": "error",
"error": "FILE EXISTS",
"error_data": {},
},
)
os.rename(path, new_item_path)
return self.finish_json(200, {"status": "ok"})
def put(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.FILES
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Files permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, files_create_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
path = os.path.join(data["parent"], data["name"])
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
path,
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
if Helpers.check_path_exists(os.path.abspath(path)):
return self.finish_json(
400,
{
"status": "error",
"error": "FILE EXISTS",
"error_data": str(e),
},
)
if data["directory"]:
os.mkdir(path)
else:
# Create the file by opening it
with open(path, "w", encoding="utf-8") as file_object:
file_object.close()
return self.finish_json(200, {"status": "ok"})
class ApiServersServerFilesZipHandler(BaseApiHandler):
def post(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.FILES
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Files permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, files_unzip_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
folder = data["folder"]
user_id = auth_data[4]["user_id"]
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
folder,
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
if Helpers.check_file_exists(folder):
folder = self.file_helper.unzip_file(folder, user_id)
else:
if user_id:
return self.finish_json(
400,
{
"status": "error",
"error": "FILE_DOES_NOT_EXIST",
"error_data": str(e),
},
)
return self.finish_json(200, {"status": "ok"})

View File

@ -74,6 +74,6 @@ class ApiServersServerLogsHandler(BaseApiHandler):
if use_html: if use_html:
for line in lines: for line in lines:
self.write(f"{line}<br />") line = f"{line}<br />"
else:
self.finish_json(200, {"status": "ok", "data": lines}) self.finish_json(200, {"status": "ok", "data": lines})

View File

@ -93,10 +93,17 @@ class ApiUsersIndexHandler(BaseApiHandler):
"error_data": str(e), "error_data": str(e),
}, },
) )
username = data["username"] username = data["username"]
username = str(username).lower() username = str(username).lower()
manager = int(user["user_id"]) manager = data.get("manager", None)
if user["superuser"]:
if (
manager == self.controller.users.get_id_by_name("SYSTEM")
or manager == 0
):
manager = None
else:
manager = int(user["user_id"])
password = data["password"] password = data["password"]
email = data.get("email", "default@example.com") email = data.get("email", "default@example.com")
enabled = data.get("enabled", True) enabled = data.get("enabled", True)

View File

@ -0,0 +1,243 @@
import json
import logging
from jsonschema import ValidationError, validate
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
class ApiUsersUserKeyHandler(BaseApiHandler):
def get(self, user_id: str, key_id=None):
auth_data = self.authenticate_user()
if not auth_data:
return
if key_id:
key = self.controller.users.get_user_api_key(key_id)
# does this user id exist?
if key is None:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID DATA",
"error_data": "INVALID KEY",
},
)
if (
str(key.user_id) != str(auth_data[4]["user_id"])
and not auth_data[4]["superuser"]
):
return self.finish_json(
400,
{
"status": "error",
"error": "NOT AUTHORIZED",
"error_data": "TRIED TO EDIT KEY WIHTOUT AUTH",
},
)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"Generated a new API token for the key {key.name} "
f"from user with UID: {key.user_id}",
server_id=0,
source_ip=self.get_remote_ip(),
)
data_key = self.controller.authentication.generate(
key.user_id_id, {"token_id": key.token_id}
)
return self.finish_json(
200,
{"status": "ok", "data": data_key},
)
if (
str(user_id) != str(auth_data[4]["user_id"])
and not auth_data[4]["superuser"]
):
return self.finish_json(
400,
{
"status": "error",
"error": "NOT AUTHORIZED",
"error_data": "TRIED TO EDIT KEY WIHTOUT AUTH",
},
)
keys = []
for key in self.controller.users.get_user_api_keys(str(user_id)):
keys.append(
{
"id": key.token_id,
"name": key.name,
"server_permissions": key.server_permissions,
"crafty_permissions": key.crafty_permissions,
"superuser": key.superuser,
}
)
self.finish_json(
200,
{
"status": "ok",
"data": keys,
},
)
def patch(self, user_id: str):
user_key_schema = {
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 3},
"server_permissions_mask": {
"type": "string",
"pattern": "^[01]{8}$", # 8 bits, see EnumPermissionsServer
},
"crafty_permissions_mask": {
"type": "string",
"pattern": "^[01]{3}$", # 8 bits, see EnumPermissionsCrafty
},
"superuser": {"type": "boolean"},
},
"additionalProperties": False,
"minProperties": 1,
}
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_exec_user_crafty_permissions,
_,
_superuser,
user,
) = auth_data
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, user_key_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
if user_id == "@me":
user_id = user["user_id"]
# does this user id exist?
if not self.controller.users.user_id_exists(user_id):
return self.finish_json(
400,
{
"status": "error",
"error": "USER NOT FOUND",
"error_data": "USER_NOT_FOUND",
},
)
if (
str(user_id) != str(auth_data[4]["user_id"])
and not auth_data[4]["superuser"]
):
return self.finish_json(
400,
{
"status": "error",
"error": "NOT AUTHORIZED",
"error_data": "TRIED TO EDIT KEY WIHTOUT AUTH",
},
)
key_id = self.controller.users.add_user_api_key(
data["name"],
user_id,
data["superuser"],
data["server_permissions_mask"],
data["crafty_permissions_mask"],
)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"Added API key {data['name']} with crafty permissions "
f"{data['crafty_permissions_mask']}"
f" and {data['server_permissions_mask']} for user with UID: {user_id}",
server_id=0,
source_ip=self.get_remote_ip(),
)
self.finish_json(200, {"status": "ok", "data": {"id": key_id}})
def delete(self, _user_id: str, key_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
(
_,
_exec_user_crafty_permissions,
_,
_,
_user,
) = auth_data
if key_id:
key = self.controller.users.get_user_api_key(key_id)
# does this user id exist?
if key is None:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID DATA",
"error_data": "INVALID KEY",
},
)
# does this user id exist?
target_key = self.controller.users.get_user_api_key(key_id)
if not target_key:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID KEY",
"error_data": "INVALID KEY ID",
},
)
if (
target_key.user_id != auth_data[4]["user_id"]
and not auth_data[4]["superuser"]
):
return self.finish_json(
400,
{
"status": "error",
"error": "NOT AUTHORIZED",
"error_data": "TRIED TO EDIT KEY WIHTOUT AUTH",
},
)
self.controller.users.delete_user_api_key(key_id)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"Removed API key {target_key} "
f"(ID: {key_id}) from user {auth_data[4]['user_id']}",
server_id=0,
source_ip=self.get_remote_ip(),
)
return self.finish_json(
200,
{"status": "ok", "data": {"id": key_id}},
)

View File

@ -166,7 +166,13 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
return self.finish_json( return self.finish_json(
400, {"status": "error", "error": "INVALID_USERNAME"} 400, {"status": "error", "error": "INVALID_USERNAME"}
) )
if self.controller.users.get_id_by_name(data["username"]) is not None: if self.controller.users.get_id_by_name(
data["username"]
) is not None and self.controller.users.get_id_by_name(
data["username"]
) != int(
user_id
):
return self.finish_json( return self.finish_json(
400, {"status": "error", "error": "USER_EXISTS"} 400, {"status": "error", "error": "USER_EXISTS"}
) )
@ -210,13 +216,13 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
400, {"status": "error", "error": "INVALID_ROLES_MODIFY"} 400, {"status": "error", "error": "INVALID_ROLES_MODIFY"}
) )
if "password" in data and str(user["user_id"] == str(user_id)):
# TODO: edit your own password
return self.finish_json(
400, {"status": "error", "error": "INVALID_PASSWORD_MODIFY"}
)
user_obj = HelperUsers.get_user_model(user_id) user_obj = HelperUsers.get_user_model(user_id)
if "password" in data and str(user["user_id"]) != str(user_id):
if str(user["user_id"]) != str(user_obj.manager):
# TODO: edit your own password
return self.finish_json(
400, {"status": "error", "error": "INVALID_PASSWORD_MODIFY"}
)
if "roles" in data: if "roles" in data:
roles: t.Set[str] = set(data.pop("roles")) roles: t.Set[str] = set(data.pop("roles"))
@ -236,6 +242,12 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
user_id, removed_roles user_id, removed_roles
) )
if "manager" in data and (
data["manager"] == self.controller.users.get_id_by_name("SYSTEM")
or data["manager"] == 0
):
data["manager"] = None
if "permissions" in data: if "permissions" in data:
permissions: t.List[UsersController.ApiPermissionDict] = data.pop( permissions: t.List[UsersController.ApiPermissionDict] = data.pop(
"permissions" "permissions"
@ -246,7 +258,7 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
limit_role_creation = 0 limit_role_creation = 0
for permission in permissions: for permission in permissions:
self.controller.crafty_perms.set_permission( permissions_mask = self.controller.crafty_perms.set_permission(
permissions_mask, permissions_mask,
EnumPermissionsCrafty.__members__[permission["name"]], EnumPermissionsCrafty.__members__[permission["name"]],
"1" if permission["enabled"] else "0", "1" if permission["enabled"] else "0",

View File

@ -1,14 +1,10 @@
import json import json
import logging import logging
import os
import time
import tornado.web import tornado.web
import tornado.escape import tornado.escape
import bleach
from app.classes.models.crafty_permissions import EnumPermissionsCrafty from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.shared.helpers import Helpers from app.classes.shared.helpers import Helpers
from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.main_models import DatabaseShortcuts from app.classes.shared.main_models import DatabaseShortcuts
from app.classes.web.base_handler import BaseHandler from app.classes.web.base_handler import BaseHandler
@ -191,500 +187,3 @@ class ServerHandler(BaseHandler):
data=page_data, data=page_data,
translate=self.translator.translate, translate=self.translator.translate,
) )
@tornado.web.authenticated
def post(self, page):
api_key, _token_data, exec_user = self.current_user
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.superuser
template = "public/404.html"
page_data = {
"version_data": "version_data_here", # TODO
"user_data": exec_user,
"show_contribute": self.helper.get_setting("show_contribute_link", True),
"background": self.controller.cached_login,
"lang": self.controller.users.get_user_lang_by_id(exec_user["user_id"]),
"lang_page": Helpers.get_lang_page(
self.controller.users.get_user_lang_by_id(exec_user["user_id"])
),
}
if page == "command":
server_id = bleach.clean(self.get_argument("id", None))
command = bleach.clean(self.get_argument("command", None))
if server_id is not None:
if command == "clone_server":
if (
not superuser
and not self.controller.crafty_perms.can_create_server(
exec_user["user_id"]
)
):
time.sleep(3)
self.helper.websocket_helper.broadcast_user(
exec_user["user_id"],
"send_start_error",
{
"error": "<i class='fas fa-exclamation-triangle'"
" style='font-size:48px;color:red'>"
"</i> Not a server creator or server limit reached."
},
)
return
def is_name_used(name):
for server in self.controller.servers.get_all_defined_servers():
if server["server_name"] == name:
return True
return
template = "/panel/dashboard"
server_data = self.controller.servers.get_server_data_by_id(
server_id
)
new_server_name = server_data.get("server_name") + " (Copy)"
name_counter = 1
while is_name_used(new_server_name):
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
)
# copy the old server
FileHelpers.copy_dir(server_data.get("path"), new_server_path)
# TODO get old server DB data to individual variables
stop_command = server_data.get("stop_command")
new_server_command = str(server_data.get("execution_command"))
new_executable = server_data.get("executable")
new_server_log_file = str(
Helpers.get_os_understandable_path(server_data.get("log_path"))
)
backup_path = os.path.join(self.helper.backup_path, new_server_uuid)
server_port = server_data.get("server_port")
server_type = server_data.get("type")
created_by = exec_user["user_id"]
new_server_id = self.controller.servers.create_server(
new_server_name,
new_server_uuid,
new_server_path,
backup_path,
new_server_command,
new_executable,
new_server_log_file,
stop_command,
server_type,
created_by,
server_port,
)
if not exec_user["superuser"]:
new_server_uuid = self.controller.servers.get_server_data_by_id(
new_server_id
).get("server_uuid")
role_id = self.controller.roles.add_role(
f"Creator of Server with uuid={new_server_uuid}",
exec_user["user_id"],
)
self.controller.server_perms.add_role_server(
new_server_id, role_id, "11111111"
)
self.controller.users.add_role_to_user(
exec_user["user_id"], role_id
)
self.controller.servers.init_all_servers()
return
self.controller.management.send_command(
exec_user["user_id"], server_id, self.get_remote_ip(), command
)
if page == "step1":
if not superuser and not self.controller.crafty_perms.can_create_server(
exec_user["user_id"]
):
self.redirect(
"/panel/error?error=Unauthorized access: "
"not a server creator or server limit reached"
)
return
if not superuser:
user_roles = self.controller.roles.get_all_roles()
else:
user_roles = self.get_user_roles()
server = bleach.clean(self.get_argument("server", ""))
server_name = bleach.clean(self.get_argument("server_name", ""))
min_mem = bleach.clean(self.get_argument("min_memory", ""))
max_mem = bleach.clean(self.get_argument("max_memory", ""))
port = bleach.clean(self.get_argument("port", ""))
if int(port) < 1 or int(port) > 65535:
self.redirect(
"/panel/error?error=Constraint Error: "
"Port must be greater than 0 and less than 65535"
)
return
import_type = bleach.clean(self.get_argument("create_type", ""))
import_server_path = bleach.clean(self.get_argument("server_path", ""))
import_server_jar = bleach.clean(self.get_argument("server_jar", ""))
server_parts = server.split("|")
captured_roles = []
for role in user_roles:
if bleach.clean(self.get_argument(str(role), "")) == "on":
captured_roles.append(role)
if not server_name:
self.redirect("/panel/error?error=Server name cannot be empty!")
return
if import_type == "import_jar":
if self.helper.is_subdir(
self.controller.project_root, import_server_path
):
self.redirect(
"/panel/error?error=Loop Error: The selected path will cause"
" an infinite copy loop. Make sure Crafty's directory is not"
" in your server path."
)
return
good_path = self.controller.verify_jar_server(
import_server_path, import_server_jar
)
if not good_path:
self.redirect(
"/panel/error?error=Server path or Server Jar not found!"
)
return
new_server_id = self.controller.import_jar_server(
server_name,
import_server_path,
import_server_jar,
min_mem,
max_mem,
port,
exec_user["user_id"],
)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f'imported a jar server named "{server_name}"',
new_server_id,
self.get_remote_ip(),
)
elif import_type == "import_zip":
# here import_server_path means the zip path
zip_path = bleach.clean(self.get_argument("root_path"))
good_path = Helpers.check_path_exists(zip_path)
if not good_path:
self.redirect("/panel/error?error=Temp path not found!")
return
new_server_id = self.controller.import_zip_server(
server_name,
zip_path,
import_server_jar,
min_mem,
max_mem,
port,
exec_user["user_id"],
)
if new_server_id == "false":
self.redirect(
f"/panel/error?error=Zip file not accessible! "
f"You can fix this permissions issue with "
f"sudo chown -R crafty:crafty {import_server_path} "
f"And sudo chmod 2775 -R {import_server_path}"
)
return
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f'imported a zip server named "{server_name}"',
new_server_id,
self.get_remote_ip(),
)
else:
if len(server_parts) != 3:
self.redirect("/panel/error?error=Invalid server data")
return
jar_type, server_type, server_version = server_parts
# TODO: add server type check here and call the correct server
# add functions if not a jar
if server_type == "forge" and not self.helper.detect_java():
translation = self.helper.translation.translate(
"error",
"installerJava",
self.controller.users.get_user_lang_by_id(exec_user["user_id"]),
).format(server_name)
self.redirect(f"/panel/error?error={translation}")
return
new_server_id = self.controller.create_jar_server(
jar_type,
server_type,
server_version,
server_name,
min_mem,
max_mem,
port,
exec_user["user_id"],
)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f"created a {server_version} {str(server_type).capitalize()}"
f' server named "{server_name}"',
# Example: Admin created a 1.16.5 Bukkit server named "survival"
new_server_id,
self.get_remote_ip(),
)
# These lines create a new Role for the Server with full permissions
# and add the user to it if he's not a superuser
if len(captured_roles) == 0:
if not superuser:
new_server_uuid = self.controller.servers.get_server_data_by_id(
new_server_id
).get("server_uuid")
role_id = self.controller.roles.add_role(
f"Creator of Server with uuid={new_server_uuid}",
exec_user["user_id"],
)
self.controller.server_perms.add_role_server(
new_server_id, role_id, "11111111"
)
self.controller.users.add_role_to_user(
exec_user["user_id"], role_id
)
else:
for role in captured_roles:
role_id = role
self.controller.server_perms.add_role_server(
new_server_id, role_id, "11111111"
)
self.controller.servers.stats.record_stats()
self.redirect("/panel/dashboard")
if page == "bedrock_step1":
if not superuser and not self.controller.crafty_perms.can_create_server(
exec_user["user_id"]
):
self.redirect(
"/panel/error?error=Unauthorized access: "
"not a server creator or server limit reached"
)
return
if not superuser:
user_roles = self.controller.roles.get_all_roles()
else:
user_roles = self.controller.roles.get_all_roles()
server = bleach.clean(self.get_argument("server", ""))
server_name = bleach.clean(self.get_argument("server_name", ""))
port = bleach.clean(self.get_argument("port", ""))
if not port:
port = 19132
if int(port) < 1 or int(port) > 65535:
self.redirect(
"/panel/error?error=Constraint Error: "
"Port must be greater than 0 and less than 65535"
)
return
import_type = bleach.clean(self.get_argument("create_type", ""))
import_server_path = bleach.clean(self.get_argument("server_path", ""))
import_server_exe = bleach.clean(self.get_argument("server_jar", ""))
server_parts = server.split("|")
captured_roles = []
for role in user_roles:
if bleach.clean(self.get_argument(str(role), "")) == "on":
captured_roles.append(role)
if not server_name:
self.redirect("/panel/error?error=Server name cannot be empty!")
return
if import_type == "import_jar":
if self.helper.is_subdir(
self.controller.project_root, import_server_path
):
self.redirect(
"/panel/error?error=Loop Error: The selected path will cause"
" an infinite copy loop. Make sure Crafty's directory is not"
" in your server path."
)
return
good_path = self.controller.verify_jar_server(
import_server_path, import_server_exe
)
if not good_path:
self.redirect(
"/panel/error?error=Server path or Server Jar not found!"
)
return
new_server_id = self.controller.import_bedrock_server(
server_name,
import_server_path,
import_server_exe,
port,
exec_user["user_id"],
)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f'imported a jar server named "{server_name}"',
new_server_id,
self.get_remote_ip(),
)
elif import_type == "import_zip":
# here import_server_path means the zip path
zip_path = bleach.clean(self.get_argument("root_path"))
good_path = Helpers.check_path_exists(zip_path)
if not good_path:
self.redirect("/panel/error?error=Temp path not found!")
return
new_server_id = self.controller.import_bedrock_zip_server(
server_name,
zip_path,
import_server_exe,
port,
exec_user["user_id"],
)
if new_server_id == "false":
self.redirect(
f"/panel/error?error=Zip file not accessible! "
f"You can fix this permissions issue with"
f"sudo chown -R crafty:crafty {import_server_path} "
f"And sudo chmod 2775 -R {import_server_path}"
)
return
self.controller.management.add_to_audit_log(
exec_user["user_id"],
f'imported a zip server named "{server_name}"',
new_server_id,
self.get_remote_ip(),
)
else:
new_server_id = self.controller.create_bedrock_server(
server_name,
exec_user["user_id"],
)
self.controller.management.add_to_audit_log(
exec_user["user_id"],
"created a Bedrock " f'server named "{server_name}"',
# Example: Admin created a 1.16.5 Bukkit server named "survival"
new_server_id,
self.get_remote_ip(),
)
# These lines create a new Role for the Server with full permissions
# and add the user to it if he's not a superuser
if len(captured_roles) == 0:
if not superuser:
new_server_uuid = self.controller.servers.get_server_data_by_id(
new_server_id
).get("server_uuid")
role_id = self.controller.roles.add_role(
f"Creator of Server with uuid={new_server_uuid}",
exec_user["user_id"],
)
self.controller.server_perms.add_role_server(
new_server_id, role_id, "11111111"
)
self.controller.users.add_role_to_user(
exec_user["user_id"], role_id
)
else:
for role in captured_roles:
role_id = role
self.controller.server_perms.add_role_server(
new_server_id, role_id, "11111111"
)
self.controller.servers.stats.record_stats()
self.redirect("/panel/dashboard")
if page == "steam_cmd_step1":
if not superuser and not self.controller.crafty_perms.can_create_server(
exec_user["user_id"]
):
self.redirect(
"/panel/error?error=Unauthorized access: "
"not a server creator or server limit reached"
)
return
if not superuser:
user_roles = self.controller.roles.get_all_roles()
else:
user_roles = self.get_user_roles()
app_id = bleach.clean(self.get_argument("steam_server", ""))
server_name = bleach.clean(self.get_argument("server_name", ""))
captured_roles = []
for role in user_roles:
if bleach.clean(self.get_argument(str(role), "")) == "on":
captured_roles.append(role)
if not server_name:
self.redirect("/panel/error?error=Server name cannot be empty!")
return
new_server_id = self.controller.create_steam_server(
app_id,
server_name,
exec_user["user_id"],
)
# These lines create a new Role for the Server with full permissions
# and add the user to it if he's not a superuser
if len(captured_roles) == 0:
if not superuser:
new_server_uuid = self.controller.servers.get_server_data_by_id(
new_server_id
).get("server_uuid")
role_id = self.controller.roles.add_role(
f"Creator of Server with uuid={new_server_uuid}",
exec_user["user_id"],
)
self.controller.server_perms.add_role_server(
new_server_id, role_id, "11111111"
)
self.controller.users.add_role_to_user(
exec_user["user_id"], role_id
)
else:
for role in captured_roles:
role_id = role
self.controller.server_perms.add_role_server(
new_server_id, role_id, "11111111"
)
self.controller.servers.stats.record_stats()
self.redirect("/panel/dashboard")
try:
self.render(
template,
data=page_data,
translate=self.translator.translate,
)
except RuntimeError:
self.redirect("/panel/dashboard")

View File

@ -15,13 +15,11 @@ from app.classes.models.management import HelpersManagement
from app.classes.shared.console import Console from app.classes.shared.console import Console
from app.classes.shared.helpers import Helpers from app.classes.shared.helpers import Helpers
from app.classes.shared.main_controller import Controller from app.classes.shared.main_controller import Controller
from app.classes.web.file_handler import FileHandler
from app.classes.web.public_handler import PublicHandler from app.classes.web.public_handler import PublicHandler
from app.classes.web.panel_handler import PanelHandler from app.classes.web.panel_handler import PanelHandler
from app.classes.web.default_handler import DefaultHandler from app.classes.web.default_handler import DefaultHandler
from app.classes.web.routes.api.api_handlers import api_handlers from app.classes.web.routes.api.api_handlers import api_handlers
from app.classes.web.server_handler import ServerHandler from app.classes.web.server_handler import ServerHandler
from app.classes.web.ajax_handler import AjaxHandler
from app.classes.web.api_handler import ( from app.classes.web.api_handler import (
ServersStats, ServersStats,
NodeStats, NodeStats,
@ -48,13 +46,14 @@ class Webserver:
controller: Controller controller: Controller
helper: Helpers helper: Helpers
def __init__(self, helper, controller, tasks_manager): def __init__(self, helper, controller, tasks_manager, file_helper):
self.ioloop = None self.ioloop = None
self.http_server = None self.http_server = None
self.https_server = None self.https_server = None
self.helper = helper self.helper = helper
self.controller = controller self.controller = controller
self.tasks_manager = tasks_manager self.tasks_manager = tasks_manager
self.file_helper = file_helper
self._asyncio_patch() self._asyncio_patch()
@staticmethod @staticmethod
@ -146,13 +145,12 @@ class Webserver:
"controller": self.controller, "controller": self.controller,
"tasks_manager": self.tasks_manager, "tasks_manager": self.tasks_manager,
"translator": self.helper.translation, "translator": self.helper.translation,
"file_helper": self.file_helper,
} }
handlers = [ handlers = [
(r"/", DefaultHandler, handler_args), (r"/", DefaultHandler, handler_args),
(r"/panel/(.*)", PanelHandler, handler_args), (r"/panel/(.*)", PanelHandler, handler_args),
(r"/server/(.*)", ServerHandler, handler_args), (r"/server/(.*)", ServerHandler, handler_args),
(r"/ajax/(.*)", AjaxHandler, handler_args),
(r"/files/(.*)", FileHandler, handler_args),
(r"/ws", SocketHandler, handler_args), (r"/ws", SocketHandler, handler_args),
(r"/upload", UploadHandler, handler_args), (r"/upload", UploadHandler, handler_args),
(r"/status", StatusHandler, handler_args), (r"/status", StatusHandler, handler_args),

View File

@ -25,11 +25,13 @@ class UploadHandler(BaseHandler):
controller: Controller = None, controller: Controller = None,
tasks_manager=None, tasks_manager=None,
translator=None, translator=None,
file_helper=None,
): ):
self.helper = helper self.helper = helper
self.controller = controller self.controller = controller
self.tasks_manager = tasks_manager self.tasks_manager = tasks_manager
self.translator = translator self.translator = translator
self.file_helper = file_helper
def prepare(self): def prepare(self):
# Class & Function Defination # Class & Function Defination

View File

@ -18,12 +18,18 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
io_loop = None io_loop = None
def initialize( def initialize(
self, helper=None, controller=None, tasks_manager=None, translator=None self,
helper=None,
controller=None,
tasks_manager=None,
translator=None,
file_helper=None,
): ):
self.helper = helper self.helper = helper
self.controller = controller self.controller = controller
self.tasks_manager = tasks_manager self.tasks_manager = tasks_manager
self.translator = translator self.translator = translator
self.file_helper = file_helper
self.io_loop = tornado.ioloop.IOLoop.current() self.io_loop = tornado.ioloop.IOLoop.current()
def get_remote_ip(self): def get_remote_ip(self):

View File

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

View File

@ -0,0 +1,137 @@
function show_file_tree() {
$("#dir_select").modal();
}
function getDirView(event = false) {
if (event) {
try {
let path = event.target.parentElement.getAttribute('data-path');
if (event.target.parentElement.classList.contains('clicked')) {
if ($(`#${path}span`).hasClass('files-tree-title')) {
$(`#${path}ul`).toggleClass("d-block");
$(`#${path}span`).toggleClass("tree-caret-down");
}
return;
} else {
getTreeView(path);
}
} catch {
console.log("Well that failed");
}
} else if ($("#root_files_button").hasClass("clicked")) {
getTreeView($("#zip_server_path").val(), true);
} else {
getTreeView($("#file-uploaded").val(), true, true);
}
}
async function getTreeView(path, unzip = false, upload = false) {
const token = getCookie("_xsrf");
console.log("IN TREE VIEW")
console.log({ "page": "import", "folder": path, "upload": upload, "unzip": unzip });
let res = await fetch(`/api/v2/import/file/unzip/`, {
method: 'POST',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({ "page": "import", "folder": path, "upload": upload, "unzip": unzip }),
});
let responseData = await res.json();
if (responseData.status === "ok") {
console.log(responseData);
process_tree_response(responseData);
let x = document.querySelector('.bootbox');
if (x) {
x.remove()
}
x = document.querySelector('.modal-backdrop');
if (x) {
x.remove()
}
show_file_tree();
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
function process_tree_response(response) {
const styles = window.getComputedStyle(document.getElementById("lower_half"));
//If this value is still hidden we know the user is executing a zip import and not an upload
if (styles.visibility === "hidden") {
document.getElementById('zip_submit').disabled = false;
} else {
document.getElementById('upload_submit').disabled = false;
}
let path = response.data.root_path.path;
$(".root-input").val(response.data.root_path.path);
let text = `<ul class="tree-nested d-block" id="${path}ul">`;
Object.entries(response.data).forEach(([key, value]) => {
if (key === "root_path" || key === "db_stats") {
//continue is not valid in for each. Return acts as a continue.
return;
}
let dpath = value.path;
let filename = key;
if (value.dir) {
text += `<li class="tree-item" id="${dpath}li" data-path="${dpath}">
<div id="${dpath}" data-path="${dpath}" data-name="${filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="radio" name="root_path" value="${dpath}">
<span id="${dpath}span" class="files-tree-title" data-path="${dpath}" data-name="${filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i>
${filename}
</span>
</input></div><li>`
} else {
text += `<li
class="d-block tree-ctx-item tree-file"
data-path="${dpath}"
data-name="${filename}"
onclick="" id="${dpath}li"><input type='radio' class="checkBoxClass d-none file-check" name='root_path' value="${dpath}" disabled><span style="margin-right: 6px;">
<i class="far fa-file"></i></span></input>${filename}</li>
`
}
});
text += `</ul>`;
if (response.data.root_path.top) {
try {
document.getElementById('main-tree-div').innerHTML += text;
document.getElementById('main-tree').parentElement.classList.add("clicked");
} catch {
document.getElementById('files-tree').innerHTML = text;
}
} else {
try {
document.getElementById(path + "span").classList.add('tree-caret-down');
document.getElementById(path).innerHTML += text;
document.getElementById(path).classList.add("clicked");
} catch {
console.log("Bad")
}
let toggler = document.getElementById(path + "span");
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "span").addEventListener("click", function caretListener() {
document.getElementById(path + "ul").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
});
}
}
}
function getToggleMain(event) {
const path = event.target.parentElement.getAttribute('data-path');
document.getElementById("files-tree").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
document.getElementById(path + "span").classList.toggle("tree-caret");
}

View File

@ -1,6 +1,8 @@
// This is the "Offline page" service worker // This is the "Offline page" service worker
importScripts('https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js'); importScripts(
"https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js"
);
const CACHE = "crafty-controller"; const CACHE = "crafty-controller";
@ -8,39 +10,32 @@ const CACHE = "crafty-controller";
const offlineFallbackPage = "/offline"; const offlineFallbackPage = "/offline";
self.addEventListener("message", (event) => { self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") { if (event.data && event.data.type === "SKIP_WAITING") {
self.skipWaiting(); self.skipWaiting();
} }
});
self.addEventListener('install', async (event) => {
event.waitUntil(
caches.open(CACHE)
.then((cache) => cache.add(offlineFallbackPage))
);
}); });
if (workbox.navigationPreload.isSupported()) { if (workbox.navigationPreload.isSupported()) {
workbox.navigationPreload.enable(); workbox.navigationPreload.enable();
} }
self.addEventListener('fetch', (event) => { // self.addEventListener('fetch', (event) => {
if (event.request.mode === 'navigate') { // if (event.request.mode === 'navigate') {
event.respondWith((async () => { // event.respondWith((async () => {
try { // try {
const preloadResp = await event.preloadResponse; // const preloadResp = await event.preloadResponse;
if (preloadResp) { // if (preloadResp) {
return preloadResp; // return preloadResp;
} // }
const networkResp = await fetch(event.request); // const networkResp = await fetch(event.request);
return networkResp; // return networkResp;
} catch (error) { // } catch (error) {
const cache = await caches.open(CACHE); // const cache = await caches.open(CACHE);
const cachedResp = await cache.match(offlineFallbackPage); // const cachedResp = await cache.match(offlineFallbackPage);
return cachedResp; // return cachedResp;
} // }
})()); // })());
} // }
}); // });

View File

@ -1,14 +0,0 @@
{% for item in data['notify_data'] %}
<!-- <div class="hidden">{{ item['id'] }}</div>-->
<div class="event">
<p class="font-weight-medium">{{ item['title'] }}</p>
<a class="d-flex align-items-center">
<div class="badge badge-primary">{{ item['date'] }}</div>
<span class="text-muted ml-2">{{ item['desc'] }}</span>
</a>
</div>
{% end %}

View File

@ -18,7 +18,7 @@
href="https://cdn.datatables.net/v/bs4/dt-1.10.22/fh-3.1.7/r-2.2.6/sc-2.0.3/sp-1.2.2/datatables.min.css" /> href="https://cdn.datatables.net/v/bs4/dt-1.10.22/fh-3.1.7/r-2.2.6/sc-2.0.3/sp-1.2.2/datatables.min.css" />
<link rel="stylesheet" href="/static/assets/vendors/css/vendor.bundle.base.css"> <link rel="stylesheet" href="/static/assets/vendors/css/vendor.bundle.base.css">
<link rel="stylesheet" href="/static/assets/css/crafty.css"> <link rel="stylesheet" href="/static/assets/css/crafty.css">
<link rel="manifest" href="/static/assets/crafty.webmanifest"> <link rel="manifest" href="/static/assets/crafty.webmanifest">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
@ -412,20 +412,26 @@
}); });
} }
function eulaAgree(server_id, command) { async function eulaAgree(server_id, command) {
//< !--this getCookie function is in base.html-- > //< !--this getCookie function is in base.html-- >
var token = getCookie("_xsrf"); const token = getCookie("_xsrf");
$.ajax({ let res = await fetch(`/api/v2/servers/${server_id}/action/eula/`, {
type: "POST", method: 'POST',
headers: { 'X-XSRFToken': token }, headers: {
url: '/ajax/eula?id=' + server_id, 'X-XSRFToken': token
success: function (data) { },
console.log("got response:");
console.log(data);
location.reload();
}
}); });
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.reload();
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
} }
@ -538,7 +544,7 @@
}); });
$(document).ready(() => { $(document).ready(() => {
if ('serviceWorker' in navigator) { 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) { .then(function (registration) {
console.log('Service Worker Registered'); console.log('Service Worker Registered');
}); });

View File

@ -1,27 +1,32 @@
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link count-indicator"> <a class="nav-link count-indicator dropdown-toggle" id="notifDropdown" href="#" data-toggle="dropdown"
aria-expanded="false">
<i class="fas fa-broadcast-tower <i class="fas fa-broadcast-tower
{% if data.get('update_available') %} {% if data.get('update_available') %}
text-danger text-danger
{% end %} {% end %}
"></i> "></i><span id="notif-count" class="badge badge-notify"></span> </a>
<!-- <span class="count bg-success">3</span>--> <div class="dropdown-menu dropdown-menu-right navbar-dropdown notif-div" style="width: 40vw; max-height: 80vh;" aria-labelledby="notifDropdown">
</a> <ul style="padding-top: 10px;" id="announcements">
</ul>
</div>
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link count-indicator" href="/panel/panel_config"> <a class="nav-link" href="/panel/panel_config">
<i class="fas fa-cogs"></i> <i class="fas fa-cogs"></i>
</a> </a>
</li> </li>
<li class="nav-item dropdown user-dropdown"> <li class="nav-item dropdown user-dropdown">
<a class="nav-link dropdown-toggle" id="UserDropdown" href="#" data-toggle="dropdown" aria-expanded="false"> <a class="nav-link dropdown-toggle" id="UserDropdown" href="#" data-toggle="dropdown" aria-expanded="false">
<img class="img-xs rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}" alt="Profile image"> </a> <img class="img-xs rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}"
alt="Profile image"> </a>
<div class="dropdown-menu dropdown-menu-right navbar-dropdown" aria-labelledby="UserDropdown"> <div class="dropdown-menu dropdown-menu-right navbar-dropdown" aria-labelledby="UserDropdown">
<div class="dropdown-header text-center"> <div class="dropdown-header text-center">
<img class="img-md rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}" alt="Profile image"> <img class="img-md rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}"
alt="Profile image">
<p class="mb-1 mt-3 font-weight-semibold">{{ data['user_data']['username'] }}</p> <p class="mb-1 mt-3 font-weight-semibold">{{ data['user_data']['username'] }}</p>
<p class="font-weight-light text-muted mb-0">Roles: </p> <p class="font-weight-light text-muted mb-0">Roles: </p>
{% for r in data['user_role'] %} {% for r in data['user_role'] %}
@ -33,27 +38,130 @@
<p class="font-weight-light text-muted mb-0">Email: {{ data['user_data']['email'] }}</p> <p class="font-weight-light text-muted mb-0">Email: {{ data['user_data']['email'] }}</p>
</div> </div>
{% if data['user_data']['preparing'] %} {% if data['user_data']['preparing'] %}
<span class="dropdown-item" id="support_progress"><i class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs', data['lang']) }}<br><br></span> <span class="dropdown-item" id="support_progress"><i
class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs',
data['lang']) }}<br><br></span>
<span class="dropdown-item" id="support_progress"> <span class="dropdown-item" id="support_progress">
<div class="support_progress" style="height: 15px; width: 100%;"> <div class="support_progress" style="height: 15px; width: 100%;">
<div class="progress-bar progress-bar-striped progress-bar-animated" id="logs_progress_bar" role="progressbar" style="width:0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div> <div class="progress-bar progress-bar-striped progress-bar-animated" id="logs_progress_bar" role="progressbar"
style="width:0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
</div> </div>
</span> </span>
{% else %} {% else %}
<a class="dropdown-item" id="support_logs"><i class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs', data['lang']) }}</i></a> <a class="dropdown-item" id="support_logs"><i
class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs',
data['lang']) }}</i></a>
{% end %} {% end %}
{% if data['superuser'] %} {% if data['superuser'] %}
<a class="dropdown-item" href="/panel/activity_logs"><i class="dropdown-item-icon mdi mdi-calendar-check-outline text-primary"></i>{{ translate('notify', 'activityLog', data['lang']) }}</a> <a class="dropdown-item" href="/panel/activity_logs"><i
class="dropdown-item-icon mdi mdi-calendar-check-outline text-primary"></i>{{ translate('notify',
'activityLog', data['lang']) }}</a>
{% end %} {% end %}
<a class="dropdown-item" href="/logout"><i class="dropdown-item-icon mdi mdi-power text-primary"></i>{{ translate('notify', 'logout', data['lang']) }}</a> <a class="dropdown-item" href="/logout"><i class="dropdown-item-icon mdi mdi-power text-primary"></i>{{
translate('notify', 'logout', data['lang']) }}</a>
</div> </div>
</li> </li>
</ul> </ul>
<style>
.badge-notify {
background: var(--purple);
position: absolute;
-moz-transform: translate(-70%, -70%);
/* For Firefox */
-ms-transform: translate(-70%, -70%);
/* for IE */
-webkit-transform: translate(-70%, -70%);
/* For Safari, Chrome, iOS */
-o-transform: translate(-70%, -70%);
}
.clear-button:hover {
cursor: pointer;
}
/* Hide scrollbar for Chrome, Safari and Opera */
.notif-div::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.notif-div {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
</style>
<script> <script>
function pfpError(image) { function pfpError(image) {
image.onerror = ""; image.onerror = "";
image.src = "/static/assets/images/faces-clipart/pic-3.png"; image.src = "/static/assets/images/faces-clipart/pic-3.png";
return true; return true;
} }
function updateAnnouncements(data) {
console.log(data)
let text = "";
for (let value of data) {
text += `<li class="card-header header-sm justify-content-between align-items-center" id="${value.id}"><p style="float: right;"><i data-id="${value.id}"class="clear-button fa-regular fa-x"></i></p><a style="color: var(--purple);" href=${value.link} target="_blank"><h6>${value.title}</h6><small><p>${value.date}</p></small><p>${value.desc}</p></li></a>`
}
if (data.length > 0) {
localStorage.setItem("notif-count", data.length);
$("#notif-count").html(data.length);
$("#announcements").html(text);
} else {
$("#announcements").html(`<p style='margin-top: 15px;' class='text-center'><i class="fa fa-bell-slash" aria-hidden="true"></i>
</p>`);
}
$(".clear-button").on("click", function (event) {
console.log("CLEAR BUTTON")
let uuid = event.target.getAttribute("data-id");
$(`#${uuid}`).remove();
send_clear(uuid);
let notif_count = localStorage.getItem("notif-count") - 1;
if (notif_count > 0) {
localStorage.setItem("notif-count", notif_count);
$("#notif-count").html(notif_count);
} else {
$("#announcements").html(`<p style='margin-top: 15px;' class='text-center'><i class="fa fa-bell-slash" aria-hidden="true"></i>
</p>`)
$("#notif-count").html("");
}
});
}
async function getAnnouncements() {
var token = getCookie("_xsrf");
let res = await fetch(`/api/v2/crafty/announcements/`, {
method: 'GET',
headers: {
'X-XSRFToken': token
},
});
let responseData = await res.json();
console.log(responseData);
if (responseData.status === "ok") {
updateAnnouncements(responseData.data)
} else {
updateAnnouncements("<li><p>Trouble Getting Annoucements</p></li>")
}
}
async function send_clear(uuid) {
var token = getCookie("_xsrf");
let body = JSON.stringify({ "id": uuid });
console.log(body)
let res = await fetch(`/api/v2/crafty/announcements/`, {
method: 'POST',
headers: {
'X-XSRFToken': token,
},
body: body,
});
let responseData = await res.json();
console.log(responseData);
if (responseData.status === "ok") {
return
} else {
bootbox.alert(responseData.error)
}
}
$(document).ready(function () {
getAnnouncements();
})
</script> </script>

View File

@ -6,7 +6,8 @@
{% block title %}Crafty Controller - {{ translate('panelConfig', 'pageTitle', data['lang']) }}{% end %} {% block title %}Crafty Controller - {{ translate('panelConfig', 'pageTitle', data['lang']) }}{% end %}
{% block content %} {% block content %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/css/bootstrap-select.min.css"> <link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/css/bootstrap-select.min.css">
<div class="content-wrapper"> <div class="content-wrapper">
@ -50,7 +51,6 @@
<!-- Page Title Header Ends--> <!-- Page Title Header Ends-->
<form id="config-form" class="forms-sample" method="post" action="/panel/config_json"> <form id="config-form" class="forms-sample" method="post" action="/panel/config_json">
{% raw xsrf_form_html() %}
{% for item in data['config-json'].items() %} {% for item in data['config-json'].items() %}
{% if item[0] == "reset_secrets_on_next_boot" %} {% if item[0] == "reset_secrets_on_next_boot" %}
@ -73,8 +73,11 @@
</select> </select>
{% elif item[0] == 'disabled_language_files' %} {% elif item[0] == 'disabled_language_files' %}
<div class="input-group"> <div class="input-group">
<button type="button" class="btn btn-outline-default custom-picker" onclick="$('option', $('#lang_select')).each(function(element) {$(this).removeAttr('selected').prop('selected', false); $('.selectpicker').selectpicker('refresh')});">{{ translate('panelConfig', 'enableLang', data['lang']) }}</button> <button type="button" class="btn btn-outline-default custom-picker"
<select id="lang_select" class="form-control selectpicker show-tick custom-picker" data-icon-base="fas" data-tick-icon="fa-check" multiple data-style="custom-picker"> onclick="$('option', $('#lang_select')).each(function(element) {$(this).removeAttr('selected').prop('selected', false); $('.selectpicker').selectpicker('refresh')});">{{
translate('panelConfig', 'enableLang', data['lang']) }}</button>
<select id="lang_select" class="form-control selectpicker show-tick custom-picker"
data-icon-base="fas" data-tick-icon="fa-check" multiple data-style="custom-picker">
{% for lang in data['all_languages'] %} {% for lang in data['all_languages'] %}
{% if lang in item[1] %} {% if lang in item[1] %}
<option selected>{{lang}}</option> <option selected>{{lang}}</option>
@ -83,12 +86,17 @@
{% end %} {% end %}
{% end %} {% end %}
</select> </select>
<textarea id="disabled_lang" name="{{item[0]}}" class="form-control list hidden" rows="{{ len(data['all_languages']) }}" value="{{','.join(item[1])}}" hidden>{{','.join(item[1])}}</textarea> <textarea id="disabled_lang" name="{{item[0]}}" class="form-control list hidden"
rows="{{ len(data['all_languages']) }}" value="{{','.join(item[1])}}"
hidden>{{','.join(item[1])}}</textarea>
</div> </div>
{% elif item[0] == 'monitored_mounts'%} {% elif item[0] == 'monitored_mounts'%}
<div class="input-group"> <div class="input-group">
<button type="button" class="btn btn-outline-default custom-picker" onclick="$('option', $('#mount_select')).each(function(element) {$(this).removeAttr('selected').prop('selected', false); $('.selectpicker').selectpicker('refresh')});">{{ translate('panelConfig', 'noMounts', data['lang']) }}</button> <button type="button" class="btn btn-outline-default custom-picker"
<select id="mount_select" class="form-control selectpicker show-tick" data-icon-base="fas" data-tick-icon="fa-check" multiple data-style="custom-picker"> onclick="$('option', $('#mount_select')).each(function(element) {$(this).removeAttr('selected').prop('selected', false); $('.selectpicker').selectpicker('refresh')});">{{
translate('panelConfig', 'noMounts', data['lang']) }}</button>
<select id="mount_select" class="form-control selectpicker show-tick" data-icon-base="fas"
data-tick-icon="fa-check" multiple data-style="custom-picker">
{% for mount in data['all_partitions'] %} {% for mount in data['all_partitions'] %}
{% if mount in item[1] %} {% if mount in item[1] %}
<option selected>{{mount}}</option> <option selected>{{mount}}</option>
@ -97,10 +105,13 @@
{% end %} {% end %}
{% end %} {% end %}
</select> </select>
<textarea id="monitored_mounts" name="{{item[0]}}" class="form-control list hidden" rows="{{ len(data['all_partitions']) }}" value="{{','.join(item[1])}}" hidden>{{','.join(item[1])}}</textarea> <textarea id="monitored_mounts" name="{{item[0]}}" class="form-control list hidden"
rows="{{ len(data['all_partitions']) }}" value="{{','.join(item[1])}}"
hidden>{{','.join(item[1])}}</textarea>
</div> </div>
{% elif isinstance(item[1], list) %} {% elif isinstance(item[1], list) %}
<textarea value="{{','.join(item[1])}}" type="text" name="{{item[0]}}" class="form-control list">{{','.join(item[1])}}</textarea> <textarea id="{{item[0]}}" value="{{','.join(item[1])}}" type="text" name="{{item[0]}}"
class="form-control list">{{','.join(item[1])}}</textarea>
{% elif isinstance(item[1], bool) %} {% elif isinstance(item[1], bool) %}
<div style="margin-left: 30px;"> <div style="margin-left: 30px;">
{% if item[1] == True %} {% if item[1] == True %}
@ -116,9 +127,11 @@
{% end %} {% end %}
</div> </div>
{% elif isinstance(item[1], int) %} {% elif isinstance(item[1], int) %}
<input type="number" class="form-control" name="{{item[0]}}" id="{{item[0]}}" value="{{ item[1] }}" step="1" min="0" required> <input type="number" class="form-control" name="{{item[0]}}" id="{{item[0]}}" value="{{ item[1] }}"
step="1" min="0" required>
{% else %} {% else %}
<input type="text" class="form-control" name="{{item[0]}}" id="{{item[0]}}" value="{{ item[1] }}" step="2" min="0" required> <input type="text" class="form-control" name="{{item[0]}}" id="{{item[0]}}" value="{{ item[1] }}"
step="2" min="0" required>
{% end %} {% end %}
</div> </div>
{% end %} {% end %}
@ -156,36 +169,66 @@
{% block js %} {% block js %}
<script> <script>
$("#config-form").submit(function (e) { function replacer(key, value) {
let uuid = uuidv4(); if (key == "disabled_language_files") {
var token = getCookie("_xsrf") if (value == 0) {
return []
} else {
return value
}
}
if (typeof value == "boolean") {
return value
} else {
return (isNaN(value) ? value : +value);
}
}
$("#config-form").on("submit", async function (e) {
e.preventDefault(); e.preventDefault();
$("#submit-status").html('<i class="fa fa-spinner fa-spin"></i>'); $("#submit-status").html('<i class="fa fa-spinner fa-spin"></i>');
/* Convert multiple select to text list */ const token = getCookie("_xsrf")
let selected_Lang = $('#lang_select').val(); let configForm = document.getElementById("config-form");
$('#disabled_lang').val(selected_Lang);
let mounts = $('#mount_select').val(); let formData = new FormData(configForm);
$('#monitored_mounts').val(mounts); formData.delete("disabled_lang");
formData.delete("lang_select");
let class_list = document.getElementsByClassName("list"); //Create an object from the form data entries
let form_json = convertFormToJSON($("#config-form")); let formDataObject = Object.fromEntries(formData.entries());
for (let i = 0; i < class_list.length; i++) { //We need to make sure these are sent regardless of whether or not they're checked
let str = String($(class_list.item(i)).val()) formDataObject.disabled_language_files = $('#lang_select').val();
form_json[$(class_list.item(i)).attr("name")] = uuid + "," + str.replace(/\s/g, ''); formDataObject.monitored_mounts = $('#mount_select').val();
}; formDataObject.keywords = $('#keywords').val().split(",");
form_json['uuid'] = uuid; $('#config-form input[type="radio"]:checked').each(function () {
$.ajax({ if ($(this).val() == 'True') {
type: "POST", formDataObject[this.name] = true;
headers: { 'X-XSRFToken': token }, } else {
dataType: "text", formDataObject[this.name] = false;
url: '/panel/config_json', }
data: form_json,
success: function (data) {
$("#submit-status").html('<i class="fa fa-check"></i>');
},
}); });
console.log(formDataObject);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject, replacer);
console.log(formDataJsonString);
let res = await fetch(`/api/v2/crafty/config/`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token
},
body: formDataJsonString,
});
let responseData = await res.json();
if (responseData.status === "ok") {
$("#submit-status").html('<i class="fa fa-check"></i>');
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
}); });
function uuidv4() { function uuidv4() {
@ -257,7 +300,7 @@
}); });
$('.clear-comm').click(function () { $('.clear-comm').click(function () {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
$.ajax({ $.ajax({
type: "POST", type: "POST",
headers: { 'X-XSRFToken': token }, headers: { 'X-XSRFToken': token },
@ -268,7 +311,7 @@
}) })
$('.delete-photo').click(function () { $('.delete-photo').click(function () {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
let photo = $('#photo').find(":selected").val(); let photo = $('#photo').find(":selected").val();
$.ajax({ $.ajax({
type: "POST", type: "POST",
@ -281,7 +324,7 @@
}) })
$('.select-photo').click(function () { $('.select-photo').click(function () {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
let photo = $('#photo').find(":selected").val(); let photo = $('#photo').find(":selected").val();
$.ajax({ $.ajax({
type: "POST", type: "POST",

View File

@ -62,11 +62,14 @@
<div class="form-group"> <div class="form-group">
<div id="upload_input" class="input-group"> <div id="upload_input" class="input-group">
<div class="custom-file"> <div class="custom-file">
<input type="file" class="custom-file-input" id="file" name="file" multiple="false" required> <input type="file" class="custom-file-input" id="file" name="file" multiple="false"
<label id="fileLabel" class="custom-file-label" for="file">{{ translate('customLogin', 'labelLoginImage', data['lang']) }}</label> required>
<label id="fileLabel" class="custom-file-label" for="file">{{ translate('customLogin',
'labelLoginImage', data['lang']) }}</label>
</div> </div>
<div class="input-group-append"> <div class="input-group-append">
<button type="button" class="btn btn-info upload-button" id="upload-button" onclick="sendFile()" disabled>UPLOAD</button> <button type="button" class="btn btn-info upload-button" id="upload-button"
onclick="sendFile()" disabled>UPLOAD</button>
</div> </div>
</div> </div>
</div> </div>
@ -81,7 +84,8 @@
<div class="form-group row"> <div class="form-group row">
<label for="photo" class="col-sm-6 col-form-label">Selected Background Image</label> <label for="photo" class="col-sm-6 col-form-label">Selected Background Image</label>
<div class="col-sm-6"> <div class="col-sm-6">
<select class="form-select form-control form-control-lg select-css form-control-plaintext" id="photo" name="photo" form="photo_form" onchange="updateBackgroundPreview()"> <select class="form-select form-control form-control-lg select-css form-control-plaintext"
id="photo" name="photo" form="photo_form" onchange="updateBackgroundPreview()">
{% for image in data["backgrounds"] %} {% for image in data["backgrounds"] %}
<option value="{{image}}">{{image}}</option> <option value="{{image}}">{{image}}</option>
{% end %} {% end %}
@ -90,7 +94,9 @@
</div> </div>
<div id="photo_loading" class="form-group" hidden> <div id="photo_loading" class="form-group" hidden>
<div class="progress"> <div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div> <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar"
aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i
class="fa-solid fa-spinner"></i></div>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
@ -98,11 +104,13 @@
data['lang']) }}</label> data['lang']) }}</label>
<label class="col-sm-1" id="opacityValue">{{ data['login_opacity'] }}%</label> <label class="col-sm-1" id="opacityValue">{{ data['login_opacity'] }}%</label>
<div class="range col-sm-8"> <div class="range col-sm-8">
<input type="range" class="form-control-range" id="modal_opacity" name="modal_opacity" onchange="previewOpacity()" min="0" max="100" value="{{ data['login_opacity'] }}"> <input type="range" class="form-control-range" id="modal_opacity" name="modal_opacity"
onchange="previewOpacity()" min="0" max="100" value="{{ data['login_opacity'] }}">
</div> </div>
</div> </div>
<div id="login_preview" style="position: relative;"> <div id="login_preview" style="position: relative;">
<img id="bg-preview" src="../../static/assets/images/auth/{{ data['background'] }}" class="img-fluid" alt="Responsive image"> <img id="bg-preview" src="../../static/assets/images/auth/{{ data['background'] }}"
class="img-fluid" alt="Responsive image">
<div id="login-form-preview"> <div id="login-form-preview">
<div id="login-form-background" class="auto-form-wrapper login-modal"> <div id="login-form-background" class="auto-form-wrapper login-modal">
<div class="text-center auto-form-logo"> <div class="text-center auto-form-logo">
@ -166,17 +174,20 @@
</style> </style>
<div id="login_form_data"> <div id="login_form_data">
<input type="hidden" name="_xsrf" value="2|1d603267|809fb6bd82f677d440e484dde7c3a310|1671726040" disabled> <input type="hidden" name="_xsrf"
value="2|1d603267|809fb6bd82f677d440e484dde7c3a310|1671726040" disabled>
<div class="form-group"> <div class="form-group">
<label class="label">Username</label> <label class="label">Username</label>
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control login-text-input login-input" placeholder="Username" name="username" id="username" required="true" disabled> <input type="text" class="form-control login-text-input login-input"
placeholder="Username" name="username" id="username" required="true" disabled>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="label">Password</label> <label class="label">Password</label>
<div class="input-group"> <div class="input-group">
<input type="password" class="form-control login-text-input login-input" placeholder="Password" name="password" id="password" required="true" disabled> <input type="password" class="form-control login-text-input login-input"
placeholder="Password" name="password" id="password" required="true" disabled>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -195,7 +206,8 @@
<a href="#" class="text-small forgot-password" disabled>Forgot Password</a> <a href="#" class="text-small forgot-password" disabled>Forgot Password</a>
</div> </div>
<div class="text-block text-center my-3"> <div class="text-block text-center my-3">
<span class="text-small font-weight-semibold"><a href="https://craftycontrol.com/">Crafty Control <span class="text-small font-weight-semibold"><a
href="https://craftycontrol.com/">Crafty Control
4.0.20</a> </span> 4.0.20</a> </span>
</div> </div>
</div> </div>
@ -297,33 +309,50 @@
}); });
}); });
$('.delete-photo').click(function () { $('.delete-photo').click(async function () {
var token = getCookie("_xsrf")
let photo = $('#photo').find(":selected").val(); let photo = $('#photo').find(":selected").val();
$.ajax({ const token = getCookie("_xsrf")
type: "POST", let res = await fetch(`/api/v2/crafty/config/customize`, {
headers: { 'X-XSRFToken': token }, method: 'DELETE',
url: '/ajax/delete_photo?photo=' + encodeURIComponent(photo), headers: {
success: function (data) { 'X-XSRFToken': token
location.reload();
}, },
body: JSON.stringify({ "photo": photo }),
}); });
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.reload();
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}) })
$('.select-photo').click(function () { $('.select-photo').click(async function () {
var token = getCookie("_xsrf")
let photo = $('#photo').find(":selected").val(); let photo = $('#photo').find(":selected").val();
let opacity = $('#modal_opacity').val(); let opacity = $('#modal_opacity').val();
let enc_photo = encodeURIComponent(photo); console.log(JSON.stringify({ "photo": photo, "opacity": opacity }))
const token = getCookie("_xsrf")
$.ajax({ let res = await fetch(`/api/v2/crafty/config/customize`, {
type: "POST", method: 'PATCH',
headers: { 'X-XSRFToken': token }, headers: {
url: '/ajax/select_photo?photo=' + enc_photo + '&opacity=' + opacity, 'X-XSRFToken': token
success: function (data) {
window.location.reload();
}, },
body: JSON.stringify({ "photo": photo, "opacity": opacity }),
}); });
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.reload();
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}) })
$(document).ready(function () { $(document).ready(function () {

View File

@ -1,106 +1,103 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Crafty Controller</title>
<!-- plugins:css -->
<link rel="stylesheet" href="/static/assets/vendors/mdi/css/materialdesignicons.min.css" />
<link rel="stylesheet" href="/static/assets/vendors/flag-icon-css/css/flag-icon.min.css" />
<link rel="stylesheet" href="/static/assets/vendors/ti-icons/css/themify-icons.css" />
<link rel="stylesheet" href="/static/assets/vendors/typicons/typicons.css" />
<link rel="stylesheet" href="/static/assets/vendors/css/vendor.bundle.base.css" />
<head> <meta name="apple-mobile-web-app-capable" content="yes" />
<!-- Required meta tags --> <meta name="mobile-web-app-capable" content="yes" />
<meta charset="utf-8"> <meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="apple-mobile-web-app-title" content="Crafty" />
<title>Crafty Controller</title> <link rel="apple-touch-icon" href="../static/assets/images/Crafty_4-0.png" />
<!-- plugins:css -->
<link rel="stylesheet" href="/static/assets/vendors/mdi/css/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/assets/vendors/flag-icon-css/css/flag-icon.min.css">
<link rel="stylesheet" href="/static/assets/vendors/ti-icons/css/themify-icons.css">
<link rel="stylesheet" href="/static/assets/vendors/typicons/typicons.css">
<link rel="stylesheet" href="/static/assets/vendors/css/vendor.bundle.base.css">
<!-- endinject -->
<meta name="apple-mobile-web-app-capable" content="yes"> <!-- Plugin css for this page -->
<meta name="mobile-web-app-capable" content="yes" /> <!-- End Plugin css for this page -->
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <!-- Layout styles -->
<meta name="apple-mobile-web-app-title" content="Crafty"> <link rel="stylesheet" href="/static/assets/css/dark/style.css" />
<link rel="apple-touch-icon" href="../static/assets/images/Crafty_4-0.png"> <!-- End Layout styles -->
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/images/logo_small.svg" />
<link rel="alternate icon" href="/static/assets/images/favicon.png" />
</head>
<style>
.auth.auth-bg-1 {
background: url("../../static/assets/images/auth/{% raw data['background'] %}"),
url("/static/assets/images/auth/login_1.jpg");
background-size: cover;
}
</style>
<body class="dark-theme">
<!-- endinject --> <div class="container-scroller">
<!-- Plugin css for this page --> <div class="container-fluid page-body-wrapper full-page-wrapper">
<!-- End Plugin css for this page --> <div class="content-wrapper d-flex align-items-center auth auth-bg-1 theme-one" >
<!-- Layout styles --> <div class="row w-100">
<link rel="stylesheet" href="/static/assets/css/dark/style.css"> <div class="col-lg-4 mx-auto">
<!-- End Layout styles --> <div class="auto-form-wrapper">
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/images/logo_small.svg"> <div class="text-center">
<link rel="alternate icon" href="/static/assets/images/favicon.png" /> <img alt="Crafty Logo" src="/static/assets/images/logo_long.svg" /><br /><br />
</head> <div class="col-sm-12 grid-margin stretch-card">
<style> <div class="card card-statistics social-card google-card card-colored" >
.auth.auth-bg-1 { <div class="card-body">
background: url("../../static/assets/images/auth/{% raw data['background'] %}"), <h4 class="platform-name mb-3 mt-4 font-weight-semibold user-name" >
url("/static/assets/images/auth/login_1.jpg"); {{ translate('accessDenied', 'accessDenied', data['lang']) }}
background-size: cover; </h4>
} <h5 class="headline font-weight-medium">
</style> {{ translate('accessDenied', 'noAccess', data['lang']) }}
</h5>
<body class="dark-theme"> <p class="mb-2 comment font-weight-light">
<div class="container-scroller"> {{ translate('accessDenied', 'contactAdmin',
<div class="container-fluid page-body-wrapper full-page-wrapper"> data['lang']) }}<br /><br />
<div class="content-wrapper d-flex align-items-center auth auth-bg-1 theme-one"> <a class="d-inline font-weight-medium" href="https://discord.gg/9VJPhCE" > {{ translate('accessDenied', 'contact', data['lang']) }}</a>
<div class="row w-100"> </p>
<div class="col-lg-4 mx-auto"> </div>
<div class="auto-form-wrapper">
<div class="text-center">
<img src="/static/assets/images/logo_long.svg"><br /><br />
<div class="col-sm-12 grid-margin stretch-card">
<div class="card card-statistics social-card google-card card-colored">
<div class="card-body">
<h4 class="platform-name mb-3 mt-4 font-weight-semibold user-name">{{ translate('accessDenied',
'accessDenied', data['lang']) }}</h4>
<h5 class="headline font-weight-medium">{{ translate('accessDenied', 'noAccess', data['lang']) }}
</h5>
<p class="mb-2 comment font-weight-light">
{{ translate('accessDenied', 'contactAdmin', data['lang']) }}<br /><br />
<a class="d-inline font-weight-medium" href="https://discord.gg/9VJPhCE"> {{
translate('accessDenied', 'contact', data['lang']) }}</a>
</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- content-wrapper ends -->
</div> </div>
<!-- content-wrapper ends --> <!-- page-body-wrapper ends -->
</div> </div>
<!-- page-body-wrapper ends --> <!-- container-scroller -->
</div> <!-- plugins:js -->
<!-- container-scroller --> <script src="/static/assets/vendors/js/vendor.bundle.base.js"></script>
<!-- plugins:js --> <!-- endinject -->
<script src="/static/assets/vendors/js/vendor.bundle.base.js"></script> <!-- inject:js -->
<!-- endinject --> <script src="/static/assets/js/shared/off-canvas.js"></script>
<!-- inject:js --> <script src="/static/assets/js/shared/hoverable-collapse.js"></script>
<script src="/static/assets/js/shared/off-canvas.js"></script> <script src="/static/assets/js/shared/misc.js"></script>
<script src="/static/assets/js/shared/hoverable-collapse.js"></script> <script src="/static/assets/js/shared/settings.js"></script>
<script src="/static/assets/js/shared/misc.js"></script> <script src="/static/assets/js/shared/todolist.js"></script>
<script src="/static/assets/js/shared/settings.js"></script> <!-- endinject -->
<script src="/static/assets/js/shared/todolist.js"></script> <script>
<!-- endinject --> $(document).ready(function () {
<script> let login_opacity_div = document.getElementById("login_opacity");
$(document).ready(function () { let opacity = login_opacity_div.getAttribute("data-value");
let login_opacity_div = document.getElementById('login_opacity'); document.getElementById("login-form-background").style.background =
let opacity = login_opacity_div.getAttribute('data-value'); "rgb(34, 36, 55, " + opacity / 100 + ")";
document.getElementById('login-form-background').style.background = 'rgb(34, 36, 55, ' + (opacity / 100) + ')'; //Register Service worker for mobile app
//Register Service worker for mobile app if ("serviceWorker" in navigator) {
if ('serviceWorker' in navigator) { navigator.serviceWorker
navigator.serviceWorker.register('/static/assets/js/shared/service-worker.js', {scope: '/'}) .register("/static/assets/js/shared/service-worker.js", {
.then(function (registration) { scope: "/",
console.error('Service Worker Registered'); })
}); .then(function (registration) {
} console.log("Service Worker Registered");
}); });
}
</script> });
</body> </script>
</body>
</html> </html>

View File

@ -326,24 +326,30 @@
}); });
} }
$("#server-path").submit(function (e) { $("#server-path").submit(async function (e) {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
e.preventDefault(); e.preventDefault();
$("#submit-status").html('<i class="fa fa-spinner fa-spin"></i>'); $("#submit-status").html('<i class="fa fa-spinner fa-spin"></i>');
let path = $("#global_server_path").val(); let path = $("#global_server_path").val();
let encoded = encodeURIComponent(path); let res = await fetch(`/api/v2/crafty/config/servers_dir`, {
console.log(path) method: 'PATCH',
$.ajax({ headers: {
type: "POST", 'X-XSRFToken': token
headers: { 'X-XSRFToken': token },
dataType: "text",
url: '/ajax/update_server_dir',
data: {
"server_dir": encoded,
}, },
body: JSON.stringify({ "new_dir": path }),
}); });
let responseData = await res.json();
if (responseData.status === "ok") {
return
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}); });
$(document).ready(function () { $(document).ready(function () {

View File

@ -49,10 +49,7 @@
</ul> </ul>
<div class=""> <div class="">
<div class=""> <div class="">
<form id="role_form" class="forms-sample" method="post" action="{{ '/panel/add_role' if data['new_role'] else '/panel/edit_role' }}"> <form id="role_form" class="forms-sample">
{% raw xsrf_form_html() %}
<input type="hidden" name="id" value="{{ data['role']['role_id'] }}">
<input type="hidden" name="subpage" value="config">
<div class="card"> <div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center"> <div class="card-header header-sm d-flex justify-content-between align-items-center">
@ -61,7 +58,7 @@
<div class="card-body"> <div class="card-body">
<div class="form-group"> <div class="form-group">
<label for="role_name">{{ translate('rolesConfig', 'roleName', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('rolesConfig', 'roleDesc', data['lang']) }}</small> </label> <label for="role_name">{{ translate('rolesConfig', 'roleName', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('rolesConfig', 'roleDesc', data['lang']) }}</small> </label>
<input type="text" class="form-control" name="role_name" id="role_name" value="{{ data['role']['role_name'] }}" placeholder="Role Name" > <input type="text" class="form-control" name="name" id="role_name" value="{{ data['role']['role_name'] }}" placeholder="Role Name" >
</div> </div>
<br /> <br />
@ -188,11 +185,11 @@
<tr> <tr>
<td>{{ server['server_name'] }}</td> <td>{{ server['server_name'] }}</td>
<td> <td>
<input type="checkbox" class="" onclick="enable_disable(event)" data-id="{{server['server_id']}}" <input type="checkbox" class="access" onclick="enable_disable(event)" data-id="{{server['server_id']}}"
id="server_{{ server['server_id'] }}_access" id="server_{{ server['server_id'] }}_access"
name="server_{{ server['server_id'] }}_access" name="server_{{ server['server_id'] }}_access"
{{ 'checked' if server['server_id'] in data['role']['servers'] else '' }} {{ 'checked' if server['server_id'] in data['role']['servers'] else '' }}
autocomplete="off" value="1"> autocomplete="off" value="1" form="dummy">
</td> </td>
{% for permission in data['permissions_all'] %} {% for permission in data['permissions_all'] %}
{% if server['server_id'] in data['role']['servers'] %} {% if server['server_id'] in data['role']['servers'] %}
@ -201,14 +198,14 @@
id="permission_{{ server['server_id'] }}_{{ permission.name }}" id="permission_{{ server['server_id'] }}_{{ permission.name }}"
name="permission_{{ server['server_id'] }}_{{ permission.name }}" name="permission_{{ server['server_id'] }}_{{ permission.name }}"
{{ 'checked' if permission in data['permissions_dict'].get(server['server_id'], []) else '' }} {{ 'checked' if permission in data['permissions_dict'].get(server['server_id'], []) else '' }}
autocomplete="off" value="1"> autocomplete="off" value="1" form="dummy">
</td> </td>
{% else %} {% else %}
<td> <td>
<input type="checkbox" class="{{server['server_id']}}_perms" <input type="checkbox" class="{{server['server_id']}}_perms"
id="permission_{{ server['server_id'] }}_{{ permission.name }}" id="permission_{{ server['server_id'] }}_{{ permission.name }}"
name="permission_{{ server['server_id'] }}_{{ permission.name }}" name="permission_{{ server['server_id'] }}_{{ permission.name }}"
autocomplete="off" value="1" disabled> autocomplete="off" value="1" disabled form="dummy">
</td> </td>
{% end %} {% end %}
{% end %} {% end %}
@ -284,7 +281,7 @@
<a class="btn btn-sm btn-danger disabled"><i class="fas fa-trash"></i>{{ translate('rolesConfig', 'delRole', data['lang']) }}</a><br /> <a class="btn btn-sm btn-danger disabled"><i class="fas fa-trash"></i>{{ translate('rolesConfig', 'delRole', data['lang']) }}</a><br />
<small>{{ translate('rolesConfig', 'doesNotExist', data['lang']) }}</small> <small>{{ translate('rolesConfig', 'doesNotExist', data['lang']) }}</small>
{% else %} {% else %}
<a href="/panel/remove_role?id={{ data['role']['role_id'] }}" class="btn btn-sm btn-danger"><i class="fas fa-trash"></i>{{ translate('rolesConfig', 'delRole', data['lang']) }}</a> <button onclick="del_role()" class="btn btn-sm btn-danger"><i class="fas fa-trash"></i>{{ translate('rolesConfig', 'delRole', data['lang']) }}</button>
{% end %} {% end %}
</div> </div>
</div> </div>
@ -341,24 +338,87 @@
console.log( "ready!" ); console.log( "ready!" );
}); });
const roleId = new URLSearchParams(document.location.search).get('id'); const roleId = new URLSearchParams(document.location.search).get('id');
function replacer(key, value) {
if (key === "permissions"){
return value;
}
if (key === "servers" && value.length === 0){
return value;
}
if (typeof value == "boolean") {
console.log(value);
return value
} else {
return (isNaN(value) ? value : +value);
}
}
$("#config_form").on("submit", async function (e) { async function del_role(){
const token = getCookie("_xsrf")
let res = await fetch(`/api/v2/roles/${roleId}`, {
method: "DELETE",
headers: {
'X-XSRFToken': token
},
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.href = "/panel/panel_config";
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
}
$("#role_form").on("submit", async function (e) {
e.preventDefault(); e.preventDefault();
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
let configForm = document.getElementById("config_form"); let roleForm = document.getElementById("role_form");
let formData = new FormData(configForm); let server_ids = $('.access').map(function() {
if ($(this).is(':checked')){
return $(this).data('id');
}
}).get();
let servers = []
for(i=0; i < server_ids.length; i++){
let arrchecked = $(`.${server_ids[i]}_perms`).map(function() {
if(this.checked){
return "1";
}else{
return "0"
}
}).get();
servers.push({"server_id": server_ids[i], "permissions": arrchecked.join("")});
}
console.log(servers)
let formData = new FormData(roleForm);
//Create an object from the form data entries //Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries()); let formDataObject = Object.fromEntries(formData.entries());
let send_object = Object() formDataObject.servers = servers;
send_object.servers = [] console.log(formDataObject);
send_object.name = formDataObject.role_name
//We need to make sure these are sent regardless of whether or not they're checked
// Format the plain form data as JSON // Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject, replacer); let formDataJsonString = JSON.stringify(formDataObject, replacer);
let res = await fetch(`/api/v2/roles/${roleId}`, { console.log(formDataJsonString);
method: 'PATCH',
let url = `/api/v2/roles/`
let method = 'POST'
if (roleId){
url = `/api/v2/roles/${roleId}`
method = 'PATCH'
}
let res = await fetch(url, {
method: method,
headers: { headers: {
'X-XSRFToken': token 'X-XSRFToken': token
}, },
@ -366,14 +426,14 @@
}); });
let responseData = await res.json(); let responseData = await res.json();
if (responseData.status === "ok") { if (responseData.status === "ok") {
window.location.reload(); window.location.href = "/panel/panel_config";
} else { } else {
bootbox.alert({ bootbox.alert({
title: responseData.error, title: responseData.error,
message: responseData.error_data message: responseData.error_data
}); });
} }
}); });

View File

@ -58,13 +58,11 @@ data['lang']) }}{% end %}
<div class="row"> <div class="row">
<div class="col-md-6 col-sm-12"> <div class="col-md-6 col-sm-12">
{% if data['new_user'] %} {% if data['new_user'] %}
<form id="user_form" class="forms-sample" method="post" action="/panel/add_user"> <form id="user_form" class="forms-sample">
{% else %} {% else %}
<form id="user_form" class="forms-sample" method="post" action="/panel/edit_user"> <form id="user_form" class="forms-sample">
{% end %} {% end %}
{% raw xsrf_form_html() %}
<input type="hidden" name="id" value="{{ data['user']['user_id'] }}">
<input type="hidden" name="subpage" value="config">
<div class="card"> <div class="card">
@ -85,7 +83,7 @@ data['lang']) }}{% end %}
}}<small class="text-muted ml-1"> - {{ translate('userConfig', 'leaveBlank', data['lang']) }} }}<small class="text-muted ml-1"> - {{ translate('userConfig', 'leaveBlank', data['lang']) }}
</small> </label> </small> </label>
<input type="password" class="form-control" name="password0" id="password0" value="" <input type="password" class="form-control" name="password0" id="password0" value=""
autocomplete="new-password" data-lpignore="true" placeholder="Password"> autocomplete="new-password" data-lpignore="true" placeholder="Password" form="dummy">
<span class="passwords-match" , <span class="passwords-match" ,
data-content="{{ translate('panelConfig', 'match', data['lang']) }}" , data-content="{{ translate('panelConfig', 'match', data['lang']) }}" ,
data-placement="right"></span> data-placement="right"></span>
@ -95,7 +93,7 @@ data['lang']) }}{% end %}
<small class="text-muted ml-1"> - {{ translate('userConfig', 'leaveBlank', data['lang']) <small class="text-muted ml-1"> - {{ translate('userConfig', 'leaveBlank', data['lang'])
}}</small> </label> }}</small> </label>
<input type="password" class="form-control" name="password1" id="password1" value="" <input type="password" class="form-control" name="password1" id="password1" value=""
autocomplete="new-password" data-lpignore="true" placeholder="Repeat Password"> autocomplete="new-password" data-lpignore="true" placeholder="Repeat Password" form="dummy">
<span class="passwords-match" , <span class="passwords-match" ,
data-content="{{ translate('panelConfig', 'match', data['lang']) }}" , data-content="{{ translate('panelConfig', 'match', data['lang']) }}" ,
data-placement="right"></span> data-placement="right"></span>
@ -111,7 +109,7 @@ data['lang']) }}{% end %}
<label class="form-label" for="language">{{ translate('userConfig', 'userLang', data['lang']) <label class="form-label" for="language">{{ translate('userConfig', 'userLang', data['lang'])
}}</label> }}</label>
<select class="form-select form-control form-control-lg select-css" id="language" <select class="form-select form-control form-control-lg select-css" id="language"
name="language" form="user_form"> name="lang" form="user_form">
{% for lang in data['languages'] %} {% for lang in data['languages'] %}
{% if not 'incomplete' in lang %} {% if not 'incomplete' in lang %}
<option value="{{lang}}">{{lang}}</option> <option value="{{lang}}">{{lang}}</option>
@ -182,18 +180,18 @@ data['lang']) }}{% end %}
<td> <td>
{% if role.role_id in data['user']['roles'] %} {% if role.role_id in data['user']['roles'] %}
{% if role.manager == data['exec_user'] or data['superuser'] %} {% if role.manager == data['exec_user'] or data['superuser'] %}
<input type="checkbox" class="form-check-input" <input type="checkbox" class="form-check-input role_check"
id="role_{{ role.role_id }}_membership" name="role_{{ role.role_id }}_membership" id="role_{{ role.role_id }}_membership" name="role_{{ role.role_id }}_membership"
checked="" value="1"> checked="" value="{{role.role_id}}" form="dummy">
{% else %} {% else %}
<input type="checkbox" class="form-check-input" <input type="checkbox" class="form-check-input role_check"
id="role_{{ role.role_id }}_membership" name="role_{{ role.role_id }}_membership" id="role_{{ role.role_id }}_membership" name="role_{{ role.role_id }}_membership"
checked="" value="1" disabled> checked="" value="{{role.role_id}}" disabled form="dummy">
{% end %} {% end %}
{% elif data['superuser'] or role.manager == data['exec_user'] %} {% elif data['superuser'] or role.manager == data['exec_user'] %}
<input type="checkbox" class="form-check-input" <input type="checkbox" class="form-check-input role_check"
id="role_{{ role.role_id }}_membership" name="role_{{ role.role_id }}_membership" id="role_{{ role.role_id }}_membership" name="role_{{ role.role_id }}_membership"
value="1"> value="{{role.role_id}}" form="dummy">
{% end %} {% end %}
</td> </td>
@ -219,7 +217,7 @@ data['lang']) }}{% end %}
<div class="card-body"> <div class="card-body">
<div class="form-group"> <div class="form-group">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover"> <table id="permissions" aria-describedby="User Crafty Permissions" class="table table-hover">
<thead> <thead>
<tr class="rounded"> <tr class="rounded">
<th>{{ translate('userConfig', 'permName', data['lang']) }}</th> <th>{{ translate('userConfig', 'permName', data['lang']) }}</th>
@ -233,16 +231,16 @@ data['lang']) }}{% end %}
<td>{{ permission.name }}</td> <td>{{ permission.name }}</td>
<td> <td>
{% if permission in data['permissions_list'] %} {% if permission in data['permissions_list'] %}
<input type="checkbox" class="form-check-input" id="permission_{{ permission.name }}" <input type="checkbox" class="form-check-input perm-name" id="permission_{{ permission.name }}"
name="permission_{{ permission.name }}" checked="" value="1"> name="permission_{{ permission.name }}" checked="" value="1" data-perm="{{permission.name}}" form="dummy">
{% else %} {% else %}
<input type="checkbox" class="form-check-input" id="permission_{{ permission.name }}" <input type="checkbox" class="form-check-input perm-name" id="permission_{{ permission.name }}"
name="permission_{{ permission.name }}" value="1"> name="permission_{{ permission.name }}" value="1" data-perm="{{permission.name}}" form="dummy">
{% end %} {% end %}
</td> </td>
<td><input type="text" class="form-control" name="quantity_{{ permission.name }}" <td><input type="text" class="form-control" name="quantity_{{ permission.name }}"
id="quantity_{{ permission.name }}" id="quantity_{{ permission.name }}"
value="{{ data['quantity_server'][permission.name] }}"></td> value="{{ data['quantity_server'][permission.name] }}" data-perm="{{permission.name}}" form="dummy"></td>
</tr> </tr>
{% end %} {% end %}
</tbody> </tbody>
@ -287,7 +285,7 @@ data['lang']) }}{% end %}
</div> </div>
<button class="btn btn-success mr-2" onclick="submit_user(event);"><i class="fas fa-save"></i> {{ <button type="submit" class="btn btn-success mr-2"><i class="fas fa-save"></i> {{
translate('panelConfig', 'save', data['lang']) }}</button> translate('panelConfig', 'save', data['lang']) }}</button>
<button type="reset" onclick="location.href='/panel/panel_config'" class="btn btn-light"><i <button type="reset" onclick="location.href='/panel/panel_config'" class="btn btn-light"><i
class="fas fa-undo-alt"></i> {{ translate('panelConfig', 'cancel', data['lang']) }}</button> class="fas fa-undo-alt"></i> {{ translate('panelConfig', 'cancel', data['lang']) }}</button>
@ -363,9 +361,12 @@ data['lang']) }}{% end %}
} }
} }
function validateForm() { function validateForm() {
let password0 = document.getElementById("password0").value let password0 = document.getElementById("password0").value;
let password1 = document.getElementById("password1").value let password1 = document.getElementById("password1").value;
if (password0 != password1) { if (password0 === "" && password1 === "" && userId){
return true
}
else if (password0 != password1) {
$('.passwords-match').popover('show'); $('.passwords-match').popover('show');
$('.popover-body').click(function () { $('.popover-body').click(function () {
$('.passwords-match').popover("hide"); $('.passwords-match').popover("hide");
@ -376,11 +377,103 @@ data['lang']) }}{% end %}
$("#password1").css("outline", "1px solid red"); $("#password1").css("outline", "1px solid red");
return false; return false;
} else { } else {
return true; return password1;
} }
} }
function replacer(key, value) {
if (typeof value == "boolean" || key === "email" || key === "permissions" || key === "roles") {
return value
} else {
console.log(key, value)
return (isNaN(value) ? value : +value);
}
}
const userId = new URLSearchParams(document.location.search).get('id') const userId = new URLSearchParams(document.location.search).get('id')
$("#user_form").on("submit", async function (e) {
e.preventDefault();
let password = validateForm();
if (!password){
return;
}
const token = getCookie("_xsrf")
let userForm = document.getElementById("user_form");
let disabled_flag = false;
let roles = $('.role_check').map(function() {
if ($(this).attr("disabled")){
disabled_flag = true;
}
if ($(this).is(':checked')){
return $(this).val();
}
}).get();
let avail_permissions = $('.perm-name').map(function() {
return $(this).data("perm");
}).get();
permissions = []
for(i=0; i < avail_permissions.length; i++){
permissions.push({"name": avail_permissions[i], "quantity": $(`#quantity_${avail_permissions[i]}`).val(), "enabled": $(`#permission_${avail_permissions[i]}`).is(':checked')})
}
console.log(permissions);
let formData = new FormData(userForm);
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
if (!disabled_flag){
formDataObject.roles = roles;
}
if ($("#permissions").length){
formDataObject.permissions = permissions;
}
if(typeof password === "string"){
formDataObject.password = password;
}
formDataObject.enabled = $("#enabled").is(":checked");
if ($("#superuser").is(":enabled")){
formDataObject.superuser = $("#superuser").is(":checked");
}
formDataObject.hints = $("#hints").is(":checked");
console.log(formDataObject);
//We need to make sure these are sent regardless of whether or not they're checked
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject, replacer);
console.log(formDataJsonString);
if (userId){
url = `/api/v2/users/${userId}`
method = 'PATCH'
}else{
url = `/api/v2/users/`
method = 'POST'
}
let res = await fetch(url, {
method: method,
headers: {
'X-XSRFToken': token
},
body: formDataJsonString,
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.href = "/panel/panel_config";
} else {
if (responseData.hasOwnProperty("error_data")){
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}else{
bootbox.alert(responseData.error
);
}
}
});
$(".delete-user").click(function () { $(".delete-user").click(function () {
var file_to_del = $(this).data("file"); var file_to_del = $(this).data("file");
@ -398,10 +491,26 @@ data['lang']) }}{% end %}
label: '<i class="fas fa-check"></i> {{ translate("serverBackups", "confirm", data['lang']) }}' label: '<i class="fas fa-check"></i> {{ translate("serverBackups", "confirm", data['lang']) }}'
} }
}, },
callback: function (result) { callback: async function (result) {
console.log(result); console.log(result);
if (result === true) { if (result === true) {
location.href = "/panel/remove_user?id=" + userId; const token = getCookie("_xsrf")
let res = await fetch(`/api/v2/users/${userId}`, {
method: "DELETE",
headers: {
'X-XSRFToken': token
},
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.href = "/panel/panel_config";
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error
});
}
} }
} }
}); });

View File

@ -86,17 +86,14 @@
apikey.server_permissions }} apikey.server_permissions }}
{{ translate('apiKeys', 'crafty', data['lang']) }} {{ {{ translate('apiKeys', 'crafty', data['lang']) }} {{
apikey.crafty_permissions }}</td> apikey.crafty_permissions }}</td>
<td> <td><button class="btn btn-danger delete-api-key"
<button class="btn btn-danger delete-api-key"
data-key-id="{{ apikey.token_id }}" data-key-id="{{ apikey.token_id }}"
data-key-name="{{ apikey.name }}">{{ data-key-name="{{ apikey.name }}">{{translate('panelConfig',
translate('panelConfig', 'delete', data['lang']) 'delete', data['lang'])}}</button>
}}</button>
<button class="btn btn-outline-primary get-a-token" <button class="btn btn-outline-primary get-a-token"
data-key-id="{{ apikey.token_id }}" data-key-id="{{ apikey.token_id }}"
data-key-name="{{ apikey.name }}">{{ data-key-name="{{ apikey.name }}">{{translate('apiKeys',
translate('apiKeys', 'getToken', data['lang']) }} 'getToken', data['lang'])}}</button>
</button>
</td> </td>
</tr> </tr>
{% end %} {% end %}
@ -115,10 +112,7 @@
'createNew', data['lang']) }}</h4> 'createNew', data['lang']) }}</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<form id="user_form" class="forms-sample" method="post" <form id="user_api_form" class="forms-sample">
action="/panel/edit_user_apikeys">
{% raw xsrf_form_html() %}
<input type="hidden" name="id" value="{{ data['user']['user_id'] }}">
<div class="form-group"> <div class="form-group">
<label class="form-label" for="username">{{ translate('apiKeys', 'name', <label class="form-label" for="username">{{ translate('apiKeys', 'name',
@ -142,7 +136,7 @@
}}</label> }}</label>
</td> </td>
<td> <td>
<input type="checkbox" class="" <input type="checkbox" class="server_perm"
id="permission_{{ permission.name }}" id="permission_{{ permission.name }}"
name="permission_{{ permission.name }}" value="1"> name="permission_{{ permission.name }}" value="1">
</td> </td>
@ -154,7 +148,7 @@
}}</label> }}</label>
</td> </td>
<td> <td>
<input type="checkbox" class="" <input type="checkbox" class="crafty_perm"
id="permission_{{ permission.name }}" id="permission_{{ permission.name }}"
name="permission_{{ permission.name }}" value="1"> name="permission_{{ permission.name }}" value="1">
</td> </td>
@ -201,56 +195,122 @@
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined; return r ? r[1] : undefined;
} }
const userId = new URLSearchParams(document.location.search).get('id')
$(document).ready(function () {
$("#user_api_form").on("submit", async function (e) {
e.preventDefault();
const token = getCookie("_xsrf")
let apiForm = document.getElementById("user_api_form");
let formData = new FormData(apiForm);
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
//We need to make sure these are sent regardless of whether or not they're checked
formDataObject.disabled_language_files = $('#lang_select').val();
$('#user_api_form input[type="checkbox"]:checked').each(function () {
if ($(this).val() == 'True') {
formDataObject[this.name] = true;
} else {
formDataObject[this.name] = false;
}
});
let server_permissions = $('.server_perm').map(function () {
if (this.checked) {
return "1";
} else {
return "0"
}
}).get();
server_permissions = server_permissions.join("");
let crafty_permissions = $('.crafty_perm').map(function () {
if (this.checked) {
return "1";
} else {
return "0"
}
}).get();
crafty_permissions = crafty_permissions.join("");
console.log(server_permissions);
console.log(crafty_permissions);
console.log(formDataObject);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify({
"name": formDataObject.name,
"server_permissions_mask": server_permissions,
"crafty_permissions_mask": crafty_permissions,
"superuser": $("#superuser").prop('checked'),
});
console.log(formDataJsonString);
let res = await fetch(`/api/v2/users/${userId}/key/`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token
},
body: formDataJsonString,
});
let responseData = await res.json();
if (responseData.status === "ok") {
location.reload();
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
});
});
$(document).ready(function () { $(document).ready(function () {
console.log("ready!"); console.log("ready!");
$('.delete-api-key').click(function () { $('.delete-api-key').click(async function () {
var keyId = $(this).data("key-id"); let keyId = $(this).data("key-id");
var keyName = $(this).data("key-name"); let token = getCookie("_xsrf");
bootbox.confirm({ let res = await fetch(`/api/v2/users/${userId}/key/${keyId}`, {
title: `Remove API key ${keyName}?`, method: 'DELETE',
message: "Do you want to delete this API key? This cannot be undone.", headers: {
buttons: { 'X-XSRFToken': token
cancel: {
label: '<i class="fas fa-times"></i> {{ translate("panelConfig", "cancel", data['lang']) }}'
},
confirm: {
label: '<i class="fas fa-check"></i> {{ translate("serverBackups", "confirm", data['lang']) }}'
}
},
callback: function (result) {
if (result) {
var token = getCookie("_xsrf")
$.ajax({
type: "DELETE",
headers: { 'X-XSRFToken': token },
url: '/panel/remove_apikey?id=' + keyId,
success: function (data) {
location.reload();
},
});
}
}
});
})
$('.get-a-token').click(function () {
var keyId = $(this).data("key-id");
var keyName = $(this).data("key-name");
var token = getCookie("_xsrf")
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/panel/get_token?id=' + keyId,
success: function (data) {
bootbox.alert({
title: `API token for ${keyName}`,
message: `Here is an API token for ${keyName}:\n<pre style="white-space: pre-wrap;color:white;word-break:break-all;background: grey;border-radius: 5px;">${data}</pre>`
});
}, },
}); });
}) let responseData = await res.json();
}); if (responseData.status === "ok") {
location.reload()
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
})
$('.get-a-token').click(async function () {
let keyId = $(this).data("key-id");
let keyName = $(this).data("key-name");
let token = getCookie("_xsrf");
let res = await fetch(`/api/v2/users/${userId}/key/${keyId}`, {
method: 'GET',
headers: {
'X-XSRFToken': token
},
});
let responseData = await res.json();
if (responseData.status === "ok") {
bootbox.alert({
title: `API token for ${keyName}`,
message: `Here is an API token for ${keyName}:\n<pre style="white-space: pre-wrap;color:white;word-break:break-all;background: grey;border-radius: 5px;">${responseData.data}</pre>`
});
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
});
});
</script> </script>

View File

@ -87,7 +87,7 @@
async function send_command_to_server(command) { async function send_command_to_server(command) {
console.log(command) console.log(command)
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
console.log('sending command: ' + command) console.log('sending command: ' + command)
let res = await fetch(`/api/v2/servers/${serverId}/stdin`, { let res = await fetch(`/api/v2/servers/${serverId}/stdin`, {

View File

@ -44,12 +44,7 @@
<div class="col-md-6 col-sm-12"> <div class="col-md-6 col-sm-12">
<br> <br>
<br> <br>
<form class="forms-sample" method="post" action="/panel/server_backup"> <form id="backup-form" class="forms-sample">
{% raw xsrf_form_html() %}
<input type="hidden" name="id" value="{{ data['server_stats']['server_id']['server_id'] }}">
<input type="hidden" name="subpage" value="backup">
{% if data['backing_up'] %} {% if data['backing_up'] %}
<div class="progress" style="height: 15px;"> <div class="progress" style="height: 15px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" id="backup_progress_bar" <div class="progress-bar progress-bar-striped progress-bar-animated" id="backup_progress_bar"
@ -149,8 +144,6 @@
data-server_path="{{ data['server_stats']['server_id']['path']}}" type="button">{{ data-server_path="{{ data['server_stats']['server_id']['path']}}" type="button">{{
translate('serverBackups', 'clickExclude', data['lang']) }}</button> translate('serverBackups', 'clickExclude', data['lang']) }}</button>
</div> </div>
<input type="number" class="form-control" name="changed" id="changed" value="0"
style="visibility: hidden;"></input>
<div class="modal fade" id="dir_select" tabindex="-1" role="dialog" aria-labelledby="dir_select" <div class="modal fade" id="dir_select" tabindex="-1" role="dialog" aria-labelledby="dir_select"
aria-hidden="true"> aria-hidden="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
@ -175,10 +168,8 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" id="modal-cancel" class="btn btn-secondary" data-dismiss="modal">{{ <button type="button" id="modal-cancel" class="btn btn-secondary" data-dismiss="modal"><i class="fa-solid fa-xmark"></i></button>
translate('serverBackups', 'cancel', data['lang']) }}</button> <button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary"><i class="fa-solid fa-thumbs-up"></i></button>
<button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary">{{
translate('serverWizard', 'save', data['lang']) }}</button>
</div> </div>
</div> </div>
</div> </div>
@ -316,66 +307,73 @@
return r ? r[1] : undefined; return r ? r[1] : undefined;
} }
function backup_started() { async function backup_started() {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
document.getElementById('backup_button').style.visibility = 'hidden'; document.getElementById('backup_button').style.visibility = 'hidden';
var dialog = bootbox.dialog({ var dialog = bootbox.dialog({
message: "{{ translate('serverBackups', 'backupTask', data['lang']) }}", message: "{{ translate('serverBackups', 'backupTask', data['lang']) }}",
closeButton: false closeButton: false
}); });
$.ajax({ let res = await fetch(`/api/v2/servers/${server_id}/action/backup_server`, {
type: "POST", method: 'POST',
headers: { 'X-XSRFToken': token }, headers: {
url: `/api/v2/servers/${server_id}/action/backup_server`, 'X-XSRFToken': token
success: function (data) { }
return; });
}, let responseData = await res.json();
}); if (responseData.status === "ok") {
console.log(responseData);
process_tree_response(responseData);
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
return; return;
} }
async function del_backup(filename, id) {
function del_backup(filename, id) { const token = getCookie("_xsrf")
var token = getCookie("_xsrf") let contents = JSON.stringify({"filename": filename})
let res = await fetch(`/api/v2/servers/${id}/backups/backup/`, {
data_to_send = { file_name: filename } method: 'DELETE',
headers: {
console.log('Sending Command to delete backup: ' + filename) 'token': token,
$.ajax({
type: "DELETE",
headers: { 'X-XSRFToken': token },
url: '/ajax/del_backup?server_id=' + id,
data: {
file_path: filename,
id: id
},
success: function (data) {
location.reload();
}, },
body: contents
}); });
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.reload();
}else{
bootbox.alert({"title": responseData.status,
"message": responseData.error})
}
} }
function restore_backup(filename, id) { async function restore_backup(filename, id) {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
let contents = JSON.stringify({"filename": filename})
var dialog = bootbox.dialog({ var dialog = bootbox.dialog({
message: "<i class='fa fa-spin fa-spinner'></i> {{ translate('serverBackups', 'restoring', data['lang']) }}", message: "<i class='fa fa-spin fa-spinner'></i> {{ translate('serverBackups', 'restoring', data['lang']) }}",
closeButton: false closeButton: false
}); });
let res = await fetch(`/api/v2/servers/${id}/backups/backup/`, {
console.log('Sending Command to restore backup: ' + filename) method: 'POST',
$.ajax({ headers: {
type: "POST", 'token': token,
headers: { 'X-XSRFToken': token },
url: '/ajax/restore_backup?server_id=' + id,
data: {
zip_file: filename,
id: id
},
success: function (data) {
setTimeout(function () {
location.href = ('/panel/dashboard');
}, 15000);
}, },
body: contents
}); });
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.href = "/panel/dashboard";
}else{
bootbox.alert({"title": responseData.status,
"message": responseData.error})
}
} }
$("#before-check").on("click", function () { $("#before-check").on("click", function () {
@ -395,7 +393,66 @@
} }
}); });
function replacer(key, value) {
if (key != "backup_before" && key != "backup_after") {
if (typeof value == "boolean" || key === "executable_update_url") {
return value
} else {
return (isNaN(value) ? value : +value);
}
} else {
return value;
}
}
$(document).ready(function () { $(document).ready(function () {
$("#backup-form").on("submit", async function (e) {
e.preventDefault();
const token = getCookie("_xsrf")
let backupForm = document.getElementById("backup-form");
let formData = new FormData(backupForm);
//Remove checks that we don't need in form data.
formData.delete("after-check");
formData.delete("before-check");
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
//We need to make sure these are sent regardless of whether or not they're checked
formDataObject.compress = $("#compress").prop('checked');
formDataObject.shutdown = $("#shutdown").prop('checked');
let excluded = [];
$('input.excluded:checkbox:checked').each(function () {
excluded.push($(this).val());
});
if ($("#root_files_button").hasClass("clicked")){
formDataObject.exclusions = excluded;
}
console.log(excluded);
console.log(formDataObject);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject, replacer);
console.log(formDataJsonString);
let res = await fetch(`/api/v2/servers/${server_id}/backups/`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token
},
body: formDataJsonString,
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.reload();
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
});
try { try {
if ($('#backup_path').val() == '') { if ($('#backup_path').val() == '') {
console.log('true') console.log('true')
@ -457,7 +514,7 @@
console.log(result); console.log(result);
if (result == true) { if (result == true) {
var full_path = backup_path + '/' + file_to_del; var full_path = backup_path + '/' + file_to_del;
del_backup(full_path, server_id); del_backup(file_to_del, server_id);
} }
} }
}); });
@ -505,27 +562,15 @@
return; return;
} else { } else {
document.getElementById('root_files_button').classList.add('clicked'); document.getElementById('root_files_button').classList.add('clicked');
document.getElementById("changed").value = 1;
} }
path = $("#root_files_button").data('server_path') path = $("#root_files_button").data('server_path')
console.log($("#root_files_button").data('server_path')) console.log($("#root_files_button").data('server_path'))
var token = getCookie("_xsrf"); const token = getCookie("_xsrf");
var dialog = bootbox.dialog({ var dialog = bootbox.dialog({
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>', message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>',
closeButton: false closeButton: false
}); });
$.ajax({
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/backup_select?id=' + server_id + '&path=' + path,
});
} else {
bootbox.alert("You must input a path before selecting this button");
}
});
if (webSocket) {
webSocket.on('send_temp_path', function (data) {
setTimeout(function () { setTimeout(function () {
var x = document.querySelector('.bootbox'); var x = document.querySelector('.bootbox');
if (x) { if (x) {
@ -535,13 +580,15 @@
if (x) { if (x) {
x.remove() x.remove()
} }
document.getElementById('main-tree-input').setAttribute('value', data.path) document.getElementById('main-tree-input').setAttribute('value', path)
getTreeView(data.path); getTreeView(path);
show_file_tree(); show_file_tree();
}, 5000); }, 5000);
}); } else {
} bootbox.alert("You must input a path before selecting this button");
}
});
if (webSocket) { if (webSocket) {
webSocket.on('backup_status', function (backup) { webSocket.on('backup_status', function (backup) {
if (backup.percent >= 100) { if (backup.percent >= 100) {
@ -558,68 +605,82 @@
}); });
} }
function getTreeView(path) { function getDirView(event){
path = path let path = event.target.parentElement.getAttribute("data-path");
if (document.getElementById(path).classList.contains('clicked')) {
return;
}else{
getTreeView(path);
}
$.ajax({ }
type: "GET", async function getTreeView(path){
url: '/ajax/get_backup_tree?id=' + server_id + '&path=' + path, console.log(path)
dataType: 'text', const token = getCookie("_xsrf");
success: function (data) { let res = await fetch(`/api/v2/servers/${server_id}/files`, {
console.log("got response:"); method: 'POST',
console.log(data); headers: {
'X-XSRFToken': token
},
body: JSON.stringify({"page": "backups", "path": path}),
});
let responseData = await res.json();
if (responseData.status === "ok") {
console.log(responseData);
process_tree_response(responseData);
dataArr = data.split('\n'); } else {
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
try { bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
function process_tree_response(response) {
let path = response.data.root_path.path;
let text = `<ul class="tree-nested d-block" id="${path}ul">`;
Object.entries(response.data).forEach(([key, value]) => {
if (key === "root_path" || key === "db_stats"){
//continue is not valid in for each. Return acts as a continue.
return;
}
let checked = ""
let dpath = value.path;
let filename = key;
if (value.dir){
if (value.excluded){
checked = "checked"
}
text += `<li class="tree-item" data-path="${dpath}">
\n<div id="${dpath}" data-path="${dpath}" data-name="${filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" class="checkBoxClass excluded" value="${dpath}" ${checked}>
<span id="${dpath}span" class="files-tree-title" data-path="${dpath}" data-name="${filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i>
<strong>${filename}</strong>
</span>
</input></div><li>`
}else{
text += `<li
class="d-block tree-ctx-item tree-file"
data-path="${dpath}"
data-name="${filename}"
onclick=""><input type='checkbox' class="checkBoxClass excluded" name='root_path' value="${dpath}" ${checked}><span style="margin-right: 6px;">
<i class="far fa-file"></i></span></input>${filename}</li>`
}
});
text += `</ul>`;
if(response.data.root_path.top){
try {
document.getElementById('main-tree-div').innerHTML += text; document.getElementById('main-tree-div').innerHTML += text;
document.getElementById('main-tree').parentElement.classList.add("clicked"); document.getElementById('main-tree').parentElement.classList.add("clicked");
} catch { } catch {
document.getElementById('files-tree').innerHTML = text; document.getElementById('files-tree').innerHTML = text;
} }
}else{
try {
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-path', serverDir);
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-name', 'Files');
},
});
}
function getToggleMain(event) {
path = event.target.parentElement.getAttribute('data-path');
document.getElementById("files-tree").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
document.getElementById(path + "span").classList.toggle("tree-caret");
}
function getDirView(event) {
path = event.target.parentElement.getAttribute('data-path');
if (document.getElementById(path).classList.contains('clicked')) {
var toggler = document.getElementById(path + "span");
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "ul").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
}
return;
} else {
$.ajax({
type: "GET",
url: '/ajax/get_backup_dir?id=' + server_id + '&path=' + path,
dataType: 'text',
success: function (data) {
console.log("got response:");
dataArr = data.split('\n');
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
try {
document.getElementById(path + "span").classList.add('tree-caret-down'); document.getElementById(path + "span").classList.add('tree-caret-down');
document.getElementById(path).innerHTML += text; document.getElementById(path).innerHTML += text;
document.getElementById(path).classList.add("clicked"); document.getElementById(path).classList.add("clicked");
@ -627,7 +688,7 @@
console.log("Bad") console.log("Bad")
} }
var toggler = document.getElementById(path); var toggler = document.getElementById(path + "span");
if (toggler.classList.contains('files-tree-title')) { if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "span").addEventListener("click", function caretListener() { document.getElementById(path + "span").addEventListener("click", function caretListener() {
@ -635,10 +696,15 @@
document.getElementById(path + "span").classList.toggle("tree-caret-down"); document.getElementById(path + "span").classList.toggle("tree-caret-down");
}); });
} }
},
});
} }
} }
function getToggleMain(event) {
path = event.target.parentElement.getAttribute('data-path');
document.getElementById("files-tree").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
document.getElementById(path + "span").classList.toggle("tree-caret");
}
function show_file_tree() { function show_file_tree() {
$("#dir_select").modal(); $("#dir_select").modal();
} }

View File

@ -304,7 +304,7 @@
}); });
function deleteServerE(callback) { function deleteServerE(callback) {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
$.ajax({ $.ajax({
type: "DELETE", type: "DELETE",
headers: { 'X-XSRFToken': token }, headers: { 'X-XSRFToken': token },
@ -318,7 +318,7 @@
}); });
} }
function deleteServerFilesE(path, callback) { function deleteServerFilesE(path, callback) {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
$.ajax({ $.ajax({
type: "DELETE", type: "DELETE",
headers: { 'X-XSRFToken': token }, headers: { 'X-XSRFToken': token },
@ -334,7 +334,7 @@
function send_command(serverId, command) { function send_command(serverId, command) {
//<!-- this getCookie function is in base.html--> //<!-- this getCookie function is in base.html-->
var token = getCookie("_xsrf"); const token = getCookie("_xsrf");
if (command == "update_executable") { if (command == "update_executable") {
document.getElementById("update-spinner").style.visibility = "visible"; document.getElementById("update-spinner").style.visibility = "visible";
} }
@ -460,7 +460,7 @@
return; return;
} }
else { else {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
setTimeout(function () { window.location = '/panel/dashboard'; }, 5000); setTimeout(function () { window.location = '/panel/dashboard'; }, 5000);
bootbox.dialog({ bootbox.dialog({
backdrop: true, backdrop: true,
@ -549,7 +549,7 @@
}); });
$("#config_form").on("submit", async function (e) { $("#config_form").on("submit", async function (e) {
e.preventDefault(); e.preventDefault();
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
let configForm = document.getElementById("config_form"); let configForm = document.getElementById("config_form");
let formData = new FormData(configForm); let formData = new FormData(configForm);
@ -576,7 +576,7 @@
}); });
let responseData = await res.json(); let responseData = await res.json();
if (responseData.status === "ok") { if (responseData.status === "ok") {
window.location.reload(); location.reload(true);
} else { } else {
bootbox.alert({ bootbox.alert({

View File

@ -67,7 +67,7 @@
translate('serverFiles', 'download', data['lang']) }}</a> translate('serverFiles', 'download', data['lang']) }}</a>
<a onclick="deleteFileE(event)" href="javascript:void(0)" id="deleteFile" href="#" <a onclick="deleteFileE(event)" href="javascript:void(0)" id="deleteFile" href="#"
style="color: red">{{ translate('serverFiles', 'delete', data['lang']) }}</a> style="color: red">{{ translate('serverFiles', 'delete', data['lang']) }}</a>
<a onclick="deleteDirE(event)" href="javascript:void(0)" id="deleteDir" href="#" style="color: red">{{ <a onclick="deleteFileE(event)" href="javascript:void(0)" id="deleteDir" href="#" style="color: red">{{
translate('serverFiles', 'delete', data['lang']) }}</a> translate('serverFiles', 'delete', data['lang']) }}</a>
<a href="javascript:void(0)" class="closebtn" style="color: var(--info);" <a href="javascript:void(0)" class="closebtn" style="color: var(--info);"
onclick="document.getElementById('files-tree-nav').style.display = 'none';">{{ onclick="document.getElementById('files-tree-nav').style.display = 'none';">{{
@ -398,32 +398,36 @@
}, },
]; ];
let filePath = '', serverFileContent = ''; let path = '', serverFileContent = '';
function clickOnFile(event) { async function clickOnFile(event) {
filePath = event.target.getAttribute('data-path'); const token = getCookie("_xsrf");
$.ajax({ path = event.target.getAttribute('data-path');
type: 'GET', let res = await fetch(`/api/v2/servers/${serverId}/files`, {
url: "/files/get_file?id=" + serverId + "&file_path=" + encodeURIComponent(filePath), method: 'POST',
dataType: 'text', headers: {
success: function (data) { 'X-XSRFToken': token
console.log('Got File Contents From Server');
json = JSON.parse(data)
if (json.error) {
$('#editorParent').toggle(false) // hide
$('#fileError').toggle(true) // show
$('#fileError').text("{{ translate('serverFiles', 'fileReadError', data['lang']) }}: " + json.error) // show error
editor.blur()
} else {
$('#editorParent').toggle(true) // show
$('#fileError').toggle(false) // hide
setFileName(event.target.innerText);
editor.session.setValue(json.content);
serverFileContent = json.content;
setSaveStatus(true);
}
}, },
body: JSON.stringify({ "page": "files", "path": path }),
}); });
let responseData = await res.json();
console.log(responseData)
if (responseData.status === "ok") {
console.log('Got File Contents From Server');
$('#editorParent').toggle(true) // show
$('#fileError').toggle(false) // hide
setFileName(event.target.innerText);
editor.session.setValue(responseData.data);
serverFileContent = responseData.data;
setSaveStatus(true);
}
else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
} }
function setFileName(name) { function setFileName(name) {
@ -577,124 +581,141 @@
} }
async function save() {
function save() {
let text = editor.session.getValue(); let text = editor.session.getValue();
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
$.ajax({ let res = await fetch(`/api/v2/servers/${serverId}/files`, {
type: "PUT", method: 'PATCH',
headers: { 'X-XSRFToken': token }, headers: {
url: "/files/save_file?id=" + serverId, 'X-XSRFToken': token
data: {
file_contents: text,
file_path: filePath
}, },
success: (data) => { body: JSON.stringify({ "path": path, "contents": text }),
serverFileContent = text;
setSaveStatus(true)
}
}); });
let responseData = await res.json();
if (responseData.status === "ok") {
serverFileContent = text;
setSaveStatus(true)
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
} }
function createFile(parent, name, callback) { async function createFile(parent, name, callback) {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
$.ajax({ let res = await fetch(`/api/v2/servers/${serverId}/files/create/`, {
type: "POST", method: 'PUT',
headers: { 'X-XSRFToken': token }, headers: {
url: "/files/create_file?id=" + serverId, 'X-XSRFToken': token
data: {
file_parent: parent,
file_name: name
},
success: function (data) {
console.log("got response:");
callback();
}, },
body: JSON.stringify({ "parent": parent, "name": name, "directory": false }),
}); });
let responseData = await res.json();
if (responseData.status === "ok") {
getTreeView($('#root_dir').data('path'));
setTreeViewContext();
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
} }
function createDir(parent, name, callback) {
var token = getCookie("_xsrf") async function createDir(parent, name, callback) {
$.ajax({ const token = getCookie("_xsrf")
type: "POST", let res = await fetch(`/api/v2/servers/${serverId}/files/create/`, {
headers: { 'X-XSRFToken': token }, method: 'PUT',
url: "/files/create_dir?id=" + serverId, headers: {
data: { 'X-XSRFToken': token
dir_parent: parent,
dir_name: name
},
success: function (data) {
console.log("got response:");
callback();
}, },
body: JSON.stringify({ "parent": parent, "name": name, "directory": true }),
}); });
let responseData = await res.json();
if (responseData.status === "ok") {
getTreeView($('#root_dir').data('path'));
setTreeViewContext();
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
} }
function renameItem(path, name, callback) { async function renameItem(path, name, callback) {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
$.ajax({ let res = await fetch(`/api/v2/servers/${serverId}/files/create/`, {
type: "PATCH", method: 'PATCH',
headers: { 'X-XSRFToken': token }, headers: {
url: "/files/rename_file?id=" + serverId, 'X-XSRFToken': token
data: {
item_path: path,
new_item_name: name
},
success: function (data) {
console.log("got response:");
callback();
}, },
body: JSON.stringify({ "path": path, "new_name": name }),
}); });
let responseData = await res.json();
if (responseData.status === "ok") {
getTreeView($('#root_dir').data('path'));
setTreeViewContext();
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
} }
function deleteFile(path, callback) { async function deleteItem(path, el, callback) {
console.log('Deleting: ' + path) const token = getCookie("_xsrf");
var token = getCookie("_xsrf") let res = await fetch(`/api/v2/servers/${serverId}/files`, {
$.ajax({ method: 'DELETE',
type: "DELETE", headers: {
headers: { 'X-XSRFToken': token }, 'X-XSRFToken': token
url: "/files/del_file?id=" + serverId,
data: {
file_path: path
},
success: function (data) {
console.log("got response:");
callback();
}, },
body: JSON.stringify({ "filename": path }),
}); });
let responseData = await res.json();
if (responseData.status === "ok") {
el = document.getElementById(path + "li");
$(el).remove();
document.getElementById('files-tree-nav').style.display = 'none';
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
} }
function deleteDir(path, callback) { async function unZip(path, callback) {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
$.ajax({ let res = await fetch(`/api/v2/servers/${serverId}/files/zip/`, {
type: "DELETE", method: 'POST',
headers: { 'X-XSRFToken': token }, headers: {
url: "/files/del_dir?id=" + serverId, 'X-XSRFToken': token
data: {
dir_path: path
},
success: function (data) {
console.log("got response:");
callback();
}, },
body: JSON.stringify({ "folder": path }),
}); });
} let responseData = await res.json();
if (responseData.status === "ok") {
getTreeView($('#root_dir').data('path'));
setTreeViewContext();
} else {
function unZip(path, callback) { bootbox.alert({
console.log('path: ', path) title: responseData.status,
var token = getCookie("_xsrf") message: responseData.error
$.ajax({ });
type: "POST", }
headers: { 'X-XSRFToken': token },
url: "/files/unzip_file?id=" + serverId,
data: {
path: path
},
success: function (data) {
window.location.href = "/panel/server_detail?id=" + serverId + "&subpage=files";
},
});
} }
async function sendFile(file, path, serverId, left, i, onProgress) { async function sendFile(file, path, serverId, left, i, onProgress) {
@ -882,36 +903,104 @@
}); });
} }
function getTreeView(event) { function getDirView(event) {
const path = $('#root_dir').data('path');; let path = event.target.parentElement.getAttribute("data-path");
if (document.getElementById(path).classList.contains('clicked')) {
return;
} else {
getTreeView(path);
}
$.ajax({ }
type: "GET", async function getTreeView(path) {
url: "/files/get_tree?id=" + serverId + "&path=" + path, const token = getCookie("_xsrf");
dataType: 'text', let res = await fetch(`/api/v2/servers/${serverId}/files`, {
success: function (data) { method: 'POST',
console.log("got response:"); headers: {
'X-XSRFToken': token
dataArr = data.split('\n');
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
try {
document.getElementById(path).innerHTML += text;
event.target.parentElement.classList.add("clicked");
} catch {
document.getElementById('files-tree').innerHTML = text;
}
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-path', serverDir);
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-name', 'Files');
setTimeout(function () { setTreeViewContext() }, 1000);
}, },
body: JSON.stringify({ "page": "files", "path": path }),
}); });
let responseData = await res.json();
if (responseData.status === "ok") {
console.log(responseData);
process_tree_response(responseData);
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
} }
function process_tree_response(response) {
let path = response.data.root_path.path;
let text = ``;
if (!response.data.root_path.top) {
text = `<ul class="tree-nested d-block" id="${path}ul">`;
}
Object.entries(response.data).forEach(([key, value]) => {
if (key === "root_path" || key === "db_stats") {
//continue is not valid in for each. Return acts as a continue.
return;
}
let checked = ""
let dpath = value.path;
let filename = key;
if (value.dir) {
if (value.excluded) {
checked = "checked"
}
text += `<li class="tree-item" id="${dpath}li" data-path="${dpath}">
\n<div id="${dpath}" data-path="${dpath}" data-name="${filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" class="checkBoxClass d-none file-check" name="root_path" value="${dpath}" ${checked}>
<span id="${dpath}span" class="files-tree-title" data-path="${dpath}" data-name="${filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i>
${filename}
</span>
</input></div></li>`
} else {
text += `<li
class="d-block tree-ctx-item tree-file"
data-path="${dpath}"
data-name="${filename}"
onclick="clickOnFile(event)" id="${dpath}li"><input type='checkbox' class="checkBoxClass d-none file-check" name='root_path' value="${dpath}" ${checked}><span style="margin-right: 6px;">
<i class="far fa-file"></i></span></input>${filename}</li>`
}
});
if (!response.data.root_path.top) {
text += `</ul>`;
}
if (response.data.root_path.top) {
try {
document.getElementById('main-tree-div').innerHTML += text;
document.getElementById('main-tree').parentElement.classList.add("clicked");
} catch {
document.getElementById('files-tree').innerHTML = text;
}
} else {
try {
document.getElementById(path + "span").classList.add('tree-caret-down');
document.getElementById(path).innerHTML += text;
document.getElementById(path).classList.add("clicked");
} catch {
console.log("Bad")
}
var toggler = document.getElementById(path + "span");
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "span").addEventListener("click", function caretListener() {
document.getElementById(path + "ul").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
});
}
}
setTimeout(function () { setTreeViewContext() }, 1000);
}
function getToggleMain(event) { function getToggleMain(event) {
path = event.target.parentElement.getAttribute('data-path'); path = event.target.parentElement.getAttribute('data-path');
document.getElementById("files-tree").classList.toggle("d-block"); document.getElementById("files-tree").classList.toggle("d-block");
@ -919,53 +1008,6 @@
document.getElementById(path + "span").classList.toggle("tree-caret"); document.getElementById(path + "span").classList.toggle("tree-caret");
} }
function getDirView(event) {
let path = event.target.parentElement.getAttribute('data-path');
if (document.getElementById(path).classList.contains('clicked')) {
var toggler = document.getElementById(path + "span");
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "ul").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
}
return;
} else {
$.ajax({
type: "GET",
url: "/files/get_dir?id=" + serverId + "&path=" + path,
dataType: 'text',
success: function (data) {
console.log("got response:");
dataArr = data.split('\n');
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
try {
document.getElementById(path + "span").classList.add('tree-caret-down');
document.getElementById(path).innerHTML += text;
document.getElementById(path).classList.add("clicked");
} catch {
console.log("Bad")
}
setTimeout(function () { setTreeViewContext() }, 1000);
var toggler = document.getElementById(path);
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "span").addEventListener("click", function caretListener() {
document.getElementById(path + "ul").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
});
}
},
});
}
}
function setTreeViewContext() { function setTreeViewContext() {
var treeItems = Array.from(document.getElementsByClassName('tree-ctx-item')); var treeItems = Array.from(document.getElementsByClassName('tree-ctx-item'));
@ -1134,45 +1176,12 @@
}, },
callback: function (result) { callback: function (result) {
if (!result) return; if (!result) return;
deleteFile(path, function () { deleteItem(path);
el = document.getElementById(path + "li");
$(el).remove();
document.getElementById('files-tree-nav').style.display = 'none';
});
} }
}); });
} }
function deleteDirE(event) { getTreeView($('#root_dir').data('path'));
path = event.target.parentElement.getAttribute('data-path');
name = event.target.parentElement.getAttribute('data-name');
bootbox.confirm({
size: "",
title: "{% raw translate('serverFiles', 'deleteItemQuestion', data['lang']) %}",
closeButton: false,
message: "{% raw translate('serverFiles', 'deleteItemQuestionMessage', data['lang']) %}",
buttons: {
confirm: {
label: "{{ translate('serverFiles', 'yesDelete', data['lang']) }}",
className: 'btn-danger'
},
cancel: {
label: "{{ translate('serverFiles', 'noDelete', data['lang']) }}",
className: 'btn-link'
}
},
callback: function (result) {
if (!result) return;
deleteDir(path, function () {
el = document.getElementById(path + "li");
$(el).remove();
document.getElementById('files-tree-nav').style.display = 'none';
});
}
});
}
getTreeView();
setTreeViewContext(); setTreeViewContext();
function setKeyboard(target) { function setKeyboard(target) {

View File

@ -77,8 +77,8 @@
{% block js %} {% block js %}
<script> <script>
// ##### Log Filter Block ##### // ##### Log Filter Block #####
var lines = []; let lines = [];
var words = []; let words = [];
if (localStorage.getItem("words")) { if (localStorage.getItem("words")) {
try { try {
words = JSON.parse(localStorage.getItem("words")); words = JSON.parse(localStorage.getItem("words"));
@ -188,27 +188,40 @@
// Populate logs and filter if present // Populate logs and filter if present
const serverId = new URLSearchParams(document.location.search).get('id') const serverId = new URLSearchParams(document.location.search).get('id')
function get_server_log() { async function get_server_log() {
const token = getCookie("_xsrf")
let colors = true;
if (!$("#stop_scroll").is(':checked')) { if (!$("#stop_scroll").is(':checked')) {
$.ajax({ let res = await fetch(`/api/v2/servers/${serverId}/logs?colors=${colors}`, {
type: 'GET', method: 'GET',
url: '/ajax/server_log?id=' + serverId + '&full=1', headers: {
dataType: 'text', 'X-XSRFToken': token
success: function (data) {
console.log('Got Log From Server')
$('#virt_console').html(data);
scroll();
lines = document.querySelectorAll('.box');
hideFilteredWords();
}, },
}); });
let responseData = await res.json();
let html = ``
if (responseData.status === "ok") {
for (let value of responseData.data) {
html += `<span class='box'>${value}<br /></span>`
}
console.log('Got Log From Server')
$('#virt_console').html(html);
scroll();
lines = document.querySelectorAll('.box');
hideFilteredWords();
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
} }
} }
$(document).ready(function () { $(document).ready(function () {
console.log("ready!"); console.log("ready!");
get_server_log(); get_server_log();
populateWords(); populateWords();
}); });
</script> </script>
{% end %} {% end %}

View File

@ -245,14 +245,12 @@
}else { }else {
return (isNaN(value) ? value : +value); return (isNaN(value) ? value : +value);
} }
} else { } else if (value === "" && key == "start_time"){
if (value === "" && key == "start_time"){
return "00:00"; return "00:00";
}else{ }else{
return value; return value;
} }
} }
}
const serverId = new URLSearchParams(document.location.search).get('id'); const serverId = new URLSearchParams(document.location.search).get('id');
const schId = new URLSearchParams(document.location.search).get('sch_id'); const schId = new URLSearchParams(document.location.search).get('sch_id');
@ -260,7 +258,7 @@
console.log("ready!"); console.log("ready!");
$("#new_schedule_form").on("submit", async function (e) { $("#new_schedule_form").on("submit", async function (e) {
e.preventDefault(); e.preventDefault();
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
let schForm = document.getElementById("new_schedule_form"); let schForm = document.getElementById("new_schedule_form");
let formData = new FormData(schForm); let formData = new FormData(schForm);
@ -305,7 +303,7 @@
$("#schedule_form").on("submit", async function (e) { $("#schedule_form").on("submit", async function (e) {
e.preventDefault(); e.preventDefault();
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
let schForm = document.getElementById("schedule_form"); let schForm = document.getElementById("schedule_form");
let formData = new FormData(schForm); let formData = new FormData(schForm);

View File

@ -47,14 +47,20 @@
<h4 class="card-title"><i class="fas fa-calendar"></i> {{ translate('serverSchedules', <h4 class="card-title"><i class="fas fa-calendar"></i> {{ translate('serverSchedules',
'scheduledTasks', data['lang']) }} </h4> 'scheduledTasks', data['lang']) }} </h4>
{% if data['user_data']['hints'] %} {% if data['user_data']['hints'] %}
<span class="too_small" title="{{ translate('serverSchedules', 'cannotSee', data['lang']) }}" , data-content="{{ translate('serverSchedules', 'cannotSeeOnMobile', data['lang']) }}" , data-placement="bottom"></span> <span class="too_small" title="{{ translate('serverSchedules', 'cannotSee', data['lang']) }}" ,
data-content="{{ translate('serverSchedules', 'cannotSeeOnMobile', data['lang']) }}" ,
data-placement="bottom"></span>
{% end %} {% end %}
<div> <div>
<button onclick="location.href=`/panel/add_schedule?id={{ data['server_stats']['server_id']['server_id'] }}`" class="btn btn-info">{{ translate('serverSchedules', 'create', data['lang']) }}<i class="fas fa-pencil-alt"></i></button> <button
onclick="location.href=`/panel/add_schedule?id={{ data['server_stats']['server_id']['server_id'] }}`"
class="btn btn-info">{{ translate('serverSchedules', 'create', data['lang']) }}<i
class="fas fa-pencil-alt"></i></button>
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<table class="table table-hover d-none d-lg-block responsive-table" id="schedule_table" width="100%" style="table-layout:fixed;"> <table class="table table-hover d-none d-lg-block responsive-table" id="schedule_table"
style="table-layout:fixed;" aria-describedby="Schedule List">
<thead> <thead>
<tr class="rounded"> <tr class="rounded">
<th style="width: 2%; min-width: 10px;">{{ translate('serverSchedules', 'name', data['lang']) }} <th style="width: 2%; min-width: 10px;">{{ translate('serverSchedules', 'name', data['lang']) }}
@ -101,10 +107,14 @@
<p>{{schedule.next_run}}</p> <p>{{schedule.next_run}}</p>
</td> </td>
<td id="{{schedule.enabled}}" class="action"> <td id="{{schedule.enabled}}" class="action">
<input style="width: 10px !important;" type="checkbox" class="schedule-enabled-toggle" data-schedule-id="{{schedule.schedule_id}}" data-schedule-enabled="{{ 'true' if schedule.enabled else 'false' }}"> <input style="width: 10px !important;" type="checkbox" class="schedule-enabled-toggle"
data-schedule-id="{{schedule.schedule_id}}"
data-schedule-enabled="{{ 'true' if schedule.enabled else 'false' }}">
</td> </td>
<td id="{{schedule.action}}" class="action"> <td id="{{schedule.action}}" class="action">
<button onclick="window.location.href='/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{schedule.schedule_id}}'" class="btn btn-info"> <button
onclick="window.location.href='/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{schedule.schedule_id}}'"
class="btn btn-info">
<i class="fas fa-pencil-alt"></i> <i class="fas fa-pencil-alt"></i>
</button> </button>
<br> <br>
@ -118,7 +128,8 @@
</tbody> </tbody>
</table> </table>
<hr /> <hr />
<table class="table table-hover d-block d-lg-none" id="mini_schedule_table" width="100%" style="table-layout:fixed;"> <table class="table table-hover d-block d-lg-none" id="mini_schedule_table"
style="table-layout:fixed;" aria-describedby="Schedule List Mobile">
<thead> <thead>
<tr class="rounded"> <tr class="rounded">
<th style="width: 25%; min-width: 50px;">{{ translate('serverSchedules', 'action', data['lang']) <th style="width: 25%; min-width: 50px;">{{ translate('serverSchedules', 'action', data['lang'])
@ -151,7 +162,8 @@
</td> </td>
</tr> </tr>
<!-- Modal --> <!-- Modal -->
<div class="modal fade" id="task_details_{{schedule.schedule_id}}" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"> <div class="modal fade" id="task_details_{{schedule.schedule_id}}" tabindex="-1" role="dialog"
aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@ -198,14 +210,19 @@
<p>zzzzzzz</p> <p>zzzzzzz</p>
{% end %} {% end %}
</li> </li>
<li id="{{schedule.enabled}}" class="action" style="border-top: .1em solid gray; border-bottom: .1em solid gray"> <li id="{{schedule.enabled}}" class="action"
style="border-top: .1em solid gray; border-bottom: .1em solid gray">
<h4>{{ translate('serverSchedules', 'enabled', data['lang']) }}</h4> <h4>{{ translate('serverSchedules', 'enabled', data['lang']) }}</h4>
<input type="checkbox" class="schedule-enabled-toggle" data-schedule-id="{{schedule.schedule_id}}" data-schedule-enabled="{{ 'true' if schedule.enabled else 'false' }}"> <input type="checkbox" class="schedule-enabled-toggle"
data-schedule-id="{{schedule.schedule_id}}"
data-schedule-enabled="{{ 'true' if schedule.enabled else 'false' }}">
</li> </li>
</ul> </ul>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button onclick="window.location.href='/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{schedule.schedule_id}}'" class="btn btn-info"> <button
onclick="window.location.href='/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{schedule.schedule_id}}'"
class="btn btn-info">
<i class="fas fa-pencil-alt"></i> {{ translate('serverSchedules', 'edit', data['lang']) <i class="fas fa-pencil-alt"></i> {{ translate('serverSchedules', 'edit', data['lang'])
}} }}
</button> </button>
@ -310,7 +327,7 @@
const serverId = new URLSearchParams(document.location.search).get('id') const serverId = new URLSearchParams(document.location.search).get('id')
$(document).ready(function () { $(document).ready(function () {
console.log('ready for JS!') console.log('ready for JS!');
$('#schedule_table').DataTable({ $('#schedule_table').DataTable({
'order': [4, 'asc'], 'order': [4, 'asc'],
} }
@ -393,7 +410,7 @@
}); });
async function del_task(sch_id, id) { async function del_task(sch_id, id) {
var token = getCookie("_xsrf") const token = getCookie("_xsrf")
let res = await fetch(`/api/v2/servers/${id}/tasks/${sch_id}`, { let res = await fetch(`/api/v2/servers/${id}/tasks/${sch_id}`, {
method: 'DELETE', method: 'DELETE',

View File

@ -46,15 +46,15 @@
<div class="input-group"> <div class="input-group">
<div id="virt_console" class="" <div id="virt_console" class=""
style="width: 100%; font-size: .8em; padding: 5px 10px; border: 1px solid var(--outline); background-color:var(--card-banner-bg);height:500px; overflow: scroll;"> style="width: 100%; font-size: .8em; padding: 5px 10px; border: 1px solid var(--outline); background-color:var(--card-banner-bg);height:500px; overflow: scroll;">
</div> </div>
</div> </div>
<br /> <br />
<div style="gap: 0.5rem;" class="input-group flex-wrap"> <div style="gap: 0.5rem;" class="input-group flex-wrap">
<input style="min-width: 10rem;" type="text" class="form-control" id="server_command" <input style="min-width: 10rem;" type="text" class="form-control" id="server_command"
name="server_command" placeholder="{{ translate('serverTerm', 'commandInput', data['lang']) }}" name="server_command" placeholder="{{ translate('serverTerm', 'commandInput', data['lang']) }}"
autofocus=""> autofocus="">
<span class="input-group-btn ml-5"> <span class="input-group-btn ml-5">
<button id="submit" class="btn btn-sm btn-info" type="button">{{ translate('serverTerm', 'sendCommand', <button id="submit" class="btn btn-sm btn-info" type="button">{{ translate('serverTerm', 'sendCommand',
data['lang']) }}</button> data['lang']) }}</button>
@ -63,73 +63,74 @@
{% if data['permissions']['Commands'] in data['user_permissions'] %} {% if data['permissions']['Commands'] in data['user_permissions'] %}
{% if data['importing'] and data['server_stats']['running']%} {% if data['importing'] and data['server_stats']['running']%}
<div id="update_control_buttons" <div id="update_control_buttons"
class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0" class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0"
style="visibility: visible"> style="visibility: visible">
<button onclick="" id="start-btn" style="max-width: 7rem;" <button onclick="" id="start-btn" style="max-width: 7rem;"
class="btn btn-warning m-1 flex-grow-1 disabled"><i class="btn btn-warning m-1 flex-grow-1 disabled"><i
class="fa fa-spinner fa-spin"></i>&nbsp;{{translate('serverTerm', 'installing', data['lang']) }}</button> class="fa fa-spinner fa-spin"></i>&nbsp;{{translate('serverTerm', 'installing', data['lang'])
}}</button>
<button onclick="" id="restart-btn" style="max-width: 7rem;" <button onclick="" id="restart-btn" style="max-width: 7rem;"
class="btn btn-outline-primary m-1 flex-grow-1 disabled">{% raw translate('serverTerm', 'restart', class="btn btn-outline-primary m-1 flex-grow-1 disabled">{% raw translate('serverTerm', 'restart',
data['lang']) %}</button> data['lang']) %}</button>
<button onclick="" id="stop-btn" style="max-width: 7rem;" <button onclick="" id="stop-btn" style="max-width: 7rem;"
class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate('serverTerm', 'stop', data['lang']) class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate('serverTerm', 'stop', data['lang'])
}}</button> }}</button>
</div> </div>
{% elif data['server_stats']['updating']%} {% elif data['server_stats']['updating']%}
<div id="update_control_buttons" <div id="update_control_buttons"
class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0" class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0"
style="visibility: visible"> style="visibility: visible">
<button onclick="" id="start-btn" style="max-width: 7rem;" <button onclick="" id="start-btn" style="max-width: 7rem;"
class="btn btn-warning m-1 flex-grow-1 disabled"><i class="fa fa-spinner fa-spin"></i>&nbsp;{{ class="btn btn-warning m-1 flex-grow-1 disabled"><i class="fa fa-spinner fa-spin"></i>&nbsp;{{
translate('serverTerm', 'updating', data['lang']) }}</button> translate('serverTerm', 'updating', data['lang']) }}</button>
<button onclick="" id="restart-btn" style="max-width: 7rem;" <button onclick="" id="restart-btn" style="max-width: 7rem;"
class="btn btn-outline-primary m-1 flex-grow-1 disabled">{% raw translate('serverTerm', 'restart', class="btn btn-outline-primary m-1 flex-grow-1 disabled">{% raw translate('serverTerm', 'restart',
data['lang']) %}</button> data['lang']) %}</button>
<button onclick="" id="stop-btn" style="max-width: 7rem;" <button onclick="" id="stop-btn" style="max-width: 7rem;"
class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate('serverTerm', 'stop', data['lang']) class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate('serverTerm', 'stop', data['lang'])
}}</button> }}</button>
</div> </div>
{% elif data['waiting_start'] %} {% elif data['waiting_start'] %}
<div id="control_buttons" <div id="control_buttons"
class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0" class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0"
style="visibility: visible"> style="visibility: visible">
<button onclick="" id="start-btn" style="max-width: 7rem; white-space: nowrap;" <button onclick="" id="start-btn" style="max-width: 7rem; white-space: nowrap;"
class="btn btn-secondary m-1 flex-grow-1 disabled" data-toggle="tooltip" class="btn btn-secondary m-1 flex-grow-1 disabled" data-toggle="tooltip"
title="{{ translate('serverTerm', 'delay-explained', data['lang'])}}">{{ translate('serverTerm', title="{{ translate('serverTerm', 'delay-explained', data['lang'])}}">{{ translate('serverTerm',
'starting', data['lang']) }}</button> 'starting', data['lang']) }}</button>
<button onclick="" id="restart-btn" style="max-width: 7rem;" <button onclick="" id="restart-btn" style="max-width: 7rem;"
class="btn btn-outline-primary m-1 flex-grow-1 disabled">{% raw translate('serverTerm', 'restart', class="btn btn-outline-primary m-1 flex-grow-1 disabled">{% raw translate('serverTerm', 'restart',
data['lang']) %}</button> data['lang']) %}</button>
<button onclick="" id="stop-btn" style="max-width: 7rem;" <button onclick="" id="stop-btn" style="max-width: 7rem;"
class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate('serverTerm', 'stop', data['lang']) class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate('serverTerm', 'stop', data['lang'])
}}</button> }}</button>
</div> </div>
{% elif data['importing'] %} {% elif data['importing'] %}
<div id="control_buttons" <div id="control_buttons"
class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0" class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0"
style="visibility: visible"> style="visibility: visible">
<button onclick="" id="start-btn" style="max-width: 12rem; white-space: nowrap;" <button onclick="" id="start-btn" style="max-width: 12rem; white-space: nowrap;"
class="btn btn-secondary m-1 flex-grow-1 disabled"><i class="fa fa-spinner fa-spin"></i> {{ class="btn btn-secondary m-1 flex-grow-1 disabled"><i class="fa fa-spinner fa-spin"></i> {{
translate('serverTerm', 'importing', translate('serverTerm', 'importing',
data['lang']) }}</button> data['lang']) }}</button>
<button onclick="" id="restart-btn" style="max-width: 7rem;" <button onclick="" id="restart-btn" style="max-width: 7rem;"
class="btn btn-outline-primary m-1 flex-grow-1 disabled">{% raw translate('serverTerm', 'restart', class="btn btn-outline-primary m-1 flex-grow-1 disabled">{% raw translate('serverTerm', 'restart',
data['lang']) %}</button> data['lang']) %}</button>
<button onclick="" id="stop-btn" style="max-width: 7rem;" <button onclick="" id="stop-btn" style="max-width: 7rem;"
class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate('serverTerm', 'stop', data['lang']) class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate('serverTerm', 'stop', data['lang'])
}}</button> }}</button>
</div> </div>
{% else %} {% else %}
<div id="control_buttons" <div id="control_buttons"
class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0" class="mt-4 flex-wrap d-flex justify-content-between justify-content-md-center align-items-center px-5 px-md-0"
style="visibility: visible"> style="visibility: visible">
<button onclick="send_command(serverId, 'start_server');" id="start-btn" style="max-width: 7rem;" <button onclick="send_command(serverId, 'start_server');" id="start-btn" style="max-width: 7rem;"
class="btn btn-primary m-1 flex-grow-1">{{ translate('serverTerm', 'start', data['lang']) }}</button> class="btn btn-primary m-1 flex-grow-1">{{ translate('serverTerm', 'start', data['lang']) }}</button>
<button onclick="send_command(serverId, 'restart_server');" id="restart-btn" style="max-width: 7rem;" <button onclick="send_command(serverId, 'restart_server');" id="restart-btn" style="max-width: 7rem;"
class="btn btn-outline-primary m-1 flex-grow-1">{% raw translate('serverTerm', 'restart', data['lang']) class="btn btn-outline-primary m-1 flex-grow-1">{% raw translate('serverTerm', 'restart', data['lang'])
%}</button> %}</button>
<button onclick="send_command(serverId, 'stop_server');" id="stop-btn" style="max-width: 7rem;" <button onclick="send_command(serverId, 'stop_server');" id="stop-btn" style="max-width: 7rem;"
class="btn btn-danger m-1 flex-grow-1">{{ translate('serverTerm', 'stop', data['lang']) }}</button> class="btn btn-danger m-1 flex-grow-1">{{ translate('serverTerm', 'stop', data['lang']) }}</button>
</div> </div>
{% end %} {% end %}
{% end %} {% end %}
@ -150,7 +151,7 @@
/* IE and Edge */ /* IE and Edge */
scrollbar-width: none; scrollbar-width: none;
/* Firefox */ /* Firefox */
font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace; font-family: Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace;
white-space: pre-wrap; white-space: pre-wrap;
} }
</style> </style>
@ -175,7 +176,7 @@
stopBtn.setAttribute('disabled', 'disabled'); stopBtn.setAttribute('disabled', 'disabled');
} }
//<!-- this getCookie function is in base.html--> //<!-- this getCookie function is in base.html-->
var token = getCookie("_xsrf"); const token = getCookie("_xsrf");
$.ajax({ $.ajax({
type: "POST", type: "POST",
@ -195,12 +196,10 @@
document.getElementById('control_buttons').innerHTML = '<button onclick="" id="start-btn" style="max-width: 7rem;" class="btn btn-primary m-1 flex-grow-1">{{ translate("serverTerm", "updating", data["lang"]) }}</button><button onclick="" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1">{% raw translate("serverTerm", "restart", data["lang"]) %}</button><button onclick="" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate("serverTerm", "stop", data["lang"]) }}</button>'; document.getElementById('control_buttons').innerHTML = '<button onclick="" id="start-btn" style="max-width: 7rem;" class="btn btn-primary m-1 flex-grow-1">{{ translate("serverTerm", "updating", data["lang"]) }}</button><button onclick="" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1">{% raw translate("serverTerm", "restart", data["lang"]) %}</button><button onclick="" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate("serverTerm", "stop", data["lang"]) }}</button>';
} }
} }
else { else if (updateButton.server_id == serverId) {
if (updateButton.server_id == serverId) {
window.location.reload() window.location.reload()
document.getElementById('update_control_buttons').innerHTML = '<button onclick="send_command(serverId, "start_server");" id="start-btn" style="max-width: 7rem;" class="btn btn-primary m-1 flex-grow-1">{{ translate("serverTerm", "start", data["lang"]) }}</button><button onclick="send_command(serverId, "restart_server");" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1">{% raw translate("serverTerm", "restart", data["lang"]) %}</button><button onclick="" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate("serverTerm", "stop", data["lang"]) }}</button>'; document.getElementById('update_control_buttons').innerHTML = '<button onclick="send_command(serverId, "start_server");" id="start-btn" style="max-width: 7rem;" class="btn btn-primary m-1 flex-grow-1">{{ translate("serverTerm", "start", data["lang"]) }}</button><button onclick="send_command(serverId, "restart_server");" id="restart-btn" style="max-width: 7rem;" class="btn btn-outline-primary m-1 flex-grow-1">{% raw translate("serverTerm", "restart", data["lang"]) %}</button><button onclick="" id="stop-btn" style="max-width: 7rem;" class="btn btn-danger m-1 flex-grow-1 disabled">{{ translate("serverTerm", "stop", data["lang"]) }}</button>';
} }
}
}); });
} }
// Convert running to lower case (example: 'True' converts to 'true') and // Convert running to lower case (example: 'True' converts to 'true') and
@ -229,17 +228,31 @@
} }
//{% end %} //{% end %}
function get_server_log() { async function get_server_log() {
$.ajax({ const token = getCookie("_xsrf")
type: 'GET', let colors = true;
url: '/ajax/server_log?id=' + serverId, let res = await fetch(`/api/v2/servers/${serverId}/logs?colors=${colors}`, {
dataType: 'text', method: 'GET',
success: function (data) { headers: {
console.log('Got Log From Server') 'X-XSRFToken': token
$('#virt_console').html(data);
scrollConsole();
}, },
}); });
let responseData = await res.json();
let html = ``
if (responseData.status === "ok") {
for (let value of responseData.data) {
html += `<span class='box'>${value}<br /></span>`
}
console.log('Got Log From Server')
$('#virt_console').html(html);
scrollConsole();
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
} }
@ -258,7 +271,7 @@
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security //used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
function getCookie(name) { function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); let r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined; return r ? r[1] : undefined;
} }
@ -293,7 +306,7 @@
}); });
function scrollConsole() { function scrollConsole() {
var logview = $('#virt_console'); let logview = $('#virt_console');
if (logview.length) if (logview.length)
logview.scrollTop(logview[0].scrollHeight - logview.height()); logview.scrollTop(logview[0].scrollHeight - logview.height());
} }
@ -372,7 +385,6 @@
} }
$(document).ready(() => { $(document).ready(() => {
var scrolled = false;
$('#virt_console').on('scroll', chkScroll); $('#virt_console').on('scroll', chkScroll);
$('#to-bottom').on('click', scrollToBottom) $('#to-bottom').on('click', scrollToBottom)
}); });

View File

@ -7,8 +7,8 @@
{% block content %} {% block content %}
<div class="content-wrapper"> <!-- Desktop View -->
<div class="d-none d-sm-block content-wrapper">
<!-- Page Title Header Starts--> <!-- Page Title Header Starts-->
<div class="row page-title-header"> <div class="row page-title-header">
<div class="col-12"> <div class="col-12">
@ -16,35 +16,45 @@
<h4 class="page-title">{{ translate('sidebar', 'documentation', data['lang']) }}</h4> <h4 class="page-title">{{ translate('sidebar', 'documentation', data['lang']) }}</h4>
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row iframe-row">
<div class="col-md-12 grid-margin"> <div class="col-12 iframe-col">
<iframe src="https://docs.craftycontrol.com/" width=100% height=1100px title="crafty's docs"></iframe> <div class="iframe-wrapper">
<iframe title="crafty's docs" src="https://docs.craftycontrol.com/" class="iframe-item"></iframe>
</div>
</div> </div>
</div> </div>
<!-- content-wrapper ends --> </div>
<style> <!-- Mobile View -->
.popover-body { <div class="d-sm-none content-wrapper mobile-content-wrapper">
color: white !important; <iframe title="crafty's docs" src="https://docs.craftycontrol.com/" class="iframe-item"></iframe>
; </div>
} <!-- content-wrapper ends -->
<style>
.iframe-item {
height: 100%;
width: 100%;
border: none;
}
#desc_id { .iframe-wrapper {
-ms-overflow-style: none; height: 100%;
/* for Internet Explorer, Edge */ }
scrollbar-width: none;
/* for Firefox */
overflow-y: scroll;
}
#desc_id::-webkit-scrollbar { .iframe-col {
display: none; height: 100%;
/* for Chrome, Safari, and Opera */ }
}
</style> .iframe-row {
height: 100%;
max-height: calc(100% - 63px);
padding-bottom: 1rem;
}
.mobile-content-wrapper {
padding: 0;
}
</style>
{% end %} {% end %}

View File

@ -156,7 +156,7 @@
if ('serviceWorker' in navigator) { 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) { .then(function (registration) {
console.error('Service Worker Registered'); console.log('Service Worker Registered');
}); });
} }
}); });

View File

@ -97,6 +97,7 @@
{% end %} {% end %}
<div class="accordion" id="accordionServers"> <div class="accordion" id="accordionServers">
{% for server in data['servers'] %} {% for server in data['servers'] %}
{% if server['server_data']['show_status'] %}
<div class="card mb-0"> <div class="card mb-0">
<div class="card-header" id="heading-{{server['server_data']['server_id']}}"> <div class="card-header" id="heading-{{server['server_data']['server_id']}}">
<h2 class="mb-0 container overflow-hidden"> <h2 class="mb-0 container overflow-hidden">
@ -167,6 +168,7 @@
</div> </div>
</div> </div>
{% end %} {% end %}
{% end %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,136 +1,167 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ data['lang_page'] }}" class="default"> <html lang="{{ data['lang_page'] }}" class="default">
<head>
<!-- Required meta tags -->
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
{% block meta %}{% end %}
<title>{% block title %}{{ _('Default') }}{% end %}</title>
<!-- plugins:css -->
<link
rel="stylesheet"
href="/static/assets/vendors/mdi/css/materialdesignicons.min.css"
/>
<link
rel="stylesheet"
href="/static/assets/vendors/flag-icon-css/css/flag-icon.min.css"
/>
<link
rel="stylesheet"
href="/static/assets/vendors/ti-icons/css/themify-icons.css"
/>
<link
rel="stylesheet"
href="/static/assets/vendors/typicons/typicons.css"
/>
<link
rel="stylesheet"
href="/static/assets/vendors/css/vendor.bundle.base.css"
/>
<link
rel="stylesheet"
href="/static/assets/vendors/fontawesome6/css/all.css"
/>
<link rel="manifest" href="/static/assets/crafty.webmanifest" />
<head> <meta name="apple-mobile-web-app-capable" content="yes" />
<!-- Required meta tags --> <meta name="mobile-web-app-capable" content="yes" />
<meta charset="utf-8"> <meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="apple-mobile-web-app-title" content="Crafty" />
{% block meta %}{% end %} <link
<title>{% block title %}{{ _('Default') }}{% end %}</title> rel="apple-touch-icon"
<!-- plugins:css --> href="../static/assets/images/Crafty_4-0.png"
<link rel="stylesheet" href="/static/assets/vendors/mdi/css/materialdesignicons.min.css"> />
<link rel="stylesheet" href="/static/assets/vendors/flag-icon-css/css/flag-icon.min.css"> <!-- endinject -->
<link rel="stylesheet" href="/static/assets/vendors/ti-icons/css/themify-icons.css"> <!-- Plugin css for this page -->
<link rel="stylesheet" href="/static/assets/vendors/typicons/typicons.css"> <!-- End Plugin css for this page -->
<link rel="stylesheet" href="/static/assets/vendors/css/vendor.bundle.base.css"> <!-- Layout styles -->
<link rel="stylesheet" href="/static/assets/vendors/fontawesome6/css/all.css"> <link rel="stylesheet" href="/static/assets/css/dark/style.css" />
<link rel="manifest" href="/static/assets/crafty.webmanifest"> <!-- End Layout styles -->
<link
rel="shortcut icon"
type="image/svg+xml"
href="/static/assets/images/logo_small.svg"
/>
<link rel="alternate icon" href="/static/assets/images/favicon.png" />
</head>
<meta name="apple-mobile-web-app-capable" content="yes"> <body>
<meta name="mobile-web-app-capable" content="yes" /> <div class="container-scroller">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <div class="container-fluid page-body-wrapper full-page-wrapper">
<meta name="apple-mobile-web-app-title" content="Crafty"> <div
<link rel="apple-touch-icon" href="../static/assets/images/Crafty_4-0.png"> class="content-wrapper d-flex align-items-sm-center auth auth-bg-1 theme-one"
<!-- endinject --> >
<!-- Plugin css for this page --> <div class="mx-auto">
<!-- End Plugin css for this page --> <div class="auto-form-wrapper">{% block content %} {% end %}</div>
<!-- Layout styles -->
<link rel="stylesheet" href="/static/assets/css/dark/style.css">
<!-- End Layout styles -->
<link rel="shortcut icon" type="image/svg+xml" href="/static/assets/images/logo_small.svg">
<link rel="alternate icon" href="/static/assets/images/favicon.png" />
</head>
<body>
<div class="container-scroller">
<div class="container-fluid page-body-wrapper full-page-wrapper">
<div class="content-wrapper d-flex align-items-sm-center auth auth-bg-1 theme-one">
<div class="mx-auto">
<div class="auto-form-wrapper">
{% block content %}
{% end %}
</div> </div>
</div> </div>
<!-- content-wrapper ends -->
</div> </div>
<!-- content-wrapper ends --> <!-- page-body-wrapper ends -->
</div> </div>
<!-- page-body-wrapper ends --> <!-- container-scroller -->
</div> <!-- plugins:js -->
<!-- container-scroller --> <script src="/static/assets/vendors/js/vendor.bundle.base.js"></script>
<!-- plugins:js --> <!-- endinject -->
<script src="/static/assets/vendors/js/vendor.bundle.base.js"></script> <!-- inject:js -->
<!-- endinject --> <script src="/static/assets/js/shared/off-canvas.js"></script>
<!-- inject:js --> <script src="/static/assets/js/shared/hoverable-collapse.js"></script>
<script src="/static/assets/js/shared/off-canvas.js"></script> <script src="/static/assets/js/shared/misc.js"></script>
<script src="/static/assets/js/shared/hoverable-collapse.js"></script> <!-- endinject -->
<script src="/static/assets/js/shared/misc.js"></script> <script>
<!-- endinject --> // {% if request.protocol == 'https' %}
<script> let usingWebSockets = true;
// {% if request.protocol == 'https' %} let listenEvents = [];
let usingWebSockets = true;
let listenEvents = []; let pageQueryParams, page;
try { try {
pageQueryParams = 'page_query_params=' + encodeURIComponent(location.search) pageQueryParams =
page = 'page=' + encodeURIComponent(location.pathname) "page_query_params=" + encodeURIComponent(location.search);
var wsInternal = new WebSocket('wss://' + location.host + '/ws?' + page + '&' + pageQueryParams); page = "page=" + encodeURIComponent(location.pathname);
wsInternal.onopen = function () { var wsInternal = new WebSocket(
console.log('opened WebSocket connection:', wsInternal) "wss://" + location.host + "/ws?" + page + "&" + pageQueryParams
}; );
wsInternal.onmessage = function (rawMessage) { wsInternal.onopen = function () {
var message = JSON.parse(rawMessage.data); console.log("opened WebSocket connection:", wsInternal);
};
wsInternal.onmessage = function (rawMessage) {
var message = JSON.parse(rawMessage.data);
console.log('got message: ', message) console.log("got message: ", message);
listenEvents listenEvents
.filter(listenedEvent => listenedEvent.event == message.event) .filter((listenedEvent) => listenedEvent.event == message.event)
.forEach(listenedEvent => listenedEvent.callback(message.data)) .forEach((listenedEvent) => listenedEvent.callback(message.data));
}; };
wsInternal.onerror = function (errorEvent) { wsInternal.onerror = function (errorEvent) {
console.error('WebSocket Error', errorEvent); console.error("WebSocket Error", errorEvent);
}; };
wsInternal.onclose = function (closeEvent) { wsInternal.onclose = function (closeEvent) {
console.log('Closed WebSocket', closeEvent); console.log("Closed WebSocket", closeEvent);
}; };
webSocket = {
on: function (event, callback) {
console.log("registered " + event + " event");
listenEvents.push({ event: event, callback: callback });
},
emit: function (event, data) {
var message = {
event: event,
data: data,
};
webSocket = { wsInternal.send(JSON.stringify(message));
on: function (event, callback) { },
console.log('registered ' + event + ' event'); };
listenEvents.push({ event: event, callback: callback }) } catch (error) {
}, console.error("Error while making websocket helpers", error);
emit: function (event, data) { usingWebSockets = false;
var message = {
event: event,
data: data
}
wsInternal.send(JSON.stringify(message));
}
} }
} catch (error) { // {% else %}
console.error('Error while making websocket helpers', error);
usingWebSockets = false; usingWebSockets = false;
} warn(
// {% else %} "WebSockets are not supported in Crafty if not using the https protocol"
usingWebSockets = false; );
warn('WebSockets are not supported in Crafty if not using the https protocol') var webSocket;
var webSocket; // {% end%}
// {% end%} </script>
{% block js %}
</script> <!-- Custom js for this page -->
{% block js %} <script>
<!-- Custom js for this page --> $(document).ready(function () {
<script> let login_opacity_div = document.getElementById("login_opacity");
$(document).ready(function () { let opacity = login_opacity_div.getAttribute("data-value");
let login_opacity_div = document.getElementById('login_opacity'); document.getElementById("login-form-background").style.background =
let opacity = login_opacity_div.getAttribute('data-value'); "rgb(34, 36, 55, " + opacity / 100 + ")";
document.getElementById('login-form-background').style.background = 'rgb(34, 36, 55, ' + (opacity / 100) + ')'; //Register Service worker for mobile app
//Register Service worker for mobile app if ("serviceWorker" in navigator) {
if ('serviceWorker' in navigator) { navigator.serviceWorker
navigator.serviceWorker.register('/static/assets/js/shared/service-worker.js', {scope: '/'}) .register("/static/assets/js/shared/service-worker.js", {
.then(function (registration) { scope: "/",
console.error('Service Worker Registered'); })
}); .then(function (registration) {
} console.log("Service Worker Registered");
}); });
}
</script> });
<!-- End custom js for this page --> </script>
{% end %} <!-- End custom js for this page -->
{% end %}
</body> </body>
</html>
</html>

View File

@ -38,81 +38,89 @@
<h4>{{ translate('serverWizard', 'newServer', data['lang']) }}</h4> <h4>{{ translate('serverWizard', 'newServer', data['lang']) }}</h4>
<br /> <br />
<form method="post" name="create_server" class="server-wizard" onSubmit="wait_msg()"> <form method="post" id="download_exe" name="create_server" class="server-wizard">
{% if data["server_api"] and data["online"] %} {% if data["server_api"] and data["online"] %}
<fieldset> <fieldset>
{% else %} {% else %}
<fieldset disabled="disabled"> <fieldset disabled="disabled">
<style> <style>
.api-alert{ .api-alert {
position:absolute; position: absolute;
top:-5px; top: -5px;
left:0; left: 0;
font-size: 50px !important; font-size: 50px !important;
color:#fff; color: #fff;
background:rgb(127, 133, 133); background: rgb(127, 133, 133);
opacity:.4; opacity: .4;
width:100%; width: 100%;
height:100%; height: 100%;
z-index: 100; z-index: 100;
} }
.api-alert p {
margin: 0;
position: absolute;
top: 50%;
left: 50%;
text-align: center;
font-size: 20px;
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
</style>
{% end %}
{% raw xsrf_form_html() %}
<div class="form-group">
<label for="server_name">{{ translate('serverWizard', 'serverName', data['lang']) }}</label>
<input type="text" class="form-control" id="server_name" name="server_name" placeholder="{{ translate('serverWizard', 'myNewServer', data['lang']) }}" required>
</div>
<div class="form-group"> .api-alert p {
<div id="accordion-1"> margin: 0;
<div class="card"> position: absolute;
<div class="card-header p-2" id="Role-1"> top: 50%;
<p class="mb-0 p-0" data-toggle="collapse" data-target="#collapseRole-1" aria-expanded="true" aria-controls="collapseRole-1"> left: 50%;
<i class="fas fa-chevron-down"></i> {{ translate('serverWizard', 'addRole', data['lang']) }} text-align: center;
<small style="text-transform: none;"> - {{ translate('serverWizard', 'autoCreate', font-size: 20px;
data['lang']) }}</small> -ms-transform: translate(-50%, -50%);
</p> transform: translate(-50%, -50%);
</div> }
<div id="collapseRole-1" class="collapse" aria-labelledby="Role-1" data-parent=""> </style>
<div class="card-body scroll"> {% end %}
<div class="form-group"> <div class="form-group">
{% for r in data['roles'] %} <label for="server_name">{{ translate('serverWizard', 'serverName', data['lang']) }}</label>
<span class="d-block menu-option"><label><input name="{{ r['role_id'] }}" type="checkbox">&nbsp; <input type="text" class="form-control" id="server_name" name="name"
{{ r['role_name'].capitalize() }}</label></span> placeholder="{{ translate('serverWizard', 'myNewServer', data['lang']) }}" required>
{% end %} </div>
<div class="form-group">
<div id="accordion-1">
<div class="card">
<div class="card-header p-2" id="Role-1">
<p class="mb-0 p-0" data-toggle="collapse" data-target="#collapseRole-1" aria-expanded="true"
aria-controls="collapseRole-1">
<i class="fas fa-chevron-down"></i> {{ translate('serverWizard', 'addRole', data['lang']) }}
<small style="text-transform: none;"> - {{ translate('serverWizard', 'autoCreate',
data['lang']) }}</small>
</p>
</div>
<div id="collapseRole-1" class="collapse" aria-labelledby="Role-1" data-parent="">
<div class="card-body scroll">
<div class="form-group">
{% for r in data['roles'] %}
<span class="d-block menu-option"><label><input name="{{ r['role_id'] }}"
type="checkbox">&nbsp;
{{ r['role_name'].capitalize() }}</label></span>
{% end %}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<button onclick="eula_confirm()" type="button" class="btn btn-primary mr-2">{{ translate('serverWizard',
'buildServer',
data['lang']) }}</button>
<button type="reset" class="btn btn-danger mr-2">{{ translate('serverWizard', 'resetForm', data['lang'])
}}</button>
</fieldset>
{% if not data["server_api"] and data["online"] %}
<div class="api-alert" style="position: absolute; top: -5px; z-index: 100; opacity: .99;">
<p style="color: white !important;"><i class="fas fa-exclamation-triangle"
style="color: red;"></i>&nbsp;{{ translate('error', 'bedrockError', data['lang']) }}<a
style="color: red;" ; href="https://status.craftycontrol.com/status/craftycontrol" target="_blank"
rel="noopener noreferrer">&nbsp;{{ translate('error', 'craftyStatus', data['lang']) }}</a>
&nbsp;{{ translate('error', 'serverJars2', data['lang']) }}</p>
</div> </div>
</div> {% end %}
<button onclick="eula_confirm()" type="button" class="btn btn-primary mr-2">{{ translate('serverWizard', {% if not data["online"] %}
'buildServer', <div class="api-alert" style="position: absolute; top: -5px; z-index: 100; opacity: .99;">
data['lang']) }}</button> <p style="color: white !important;"><i class="fas fa-exclamation-triangle"
<button type="reset" class="btn btn-danger mr-2">{{ translate('serverWizard', 'resetForm', data['lang']) style="color: red;"></i>&nbsp;{{ translate('error', 'noInternet', data['lang']) }}</p>
}}</button> </div>
</fieldset> {% end %}
{% if not data["server_api"] and data["online"] %}
<div class="api-alert" style="position: absolute; top: -5px; z-index: 100; opacity: .99;">
<p style="color: white !important;"><i class="fas fa-exclamation-triangle" style="color: red;"></i>&nbsp;{{ translate('error', 'bedrockError', data['lang']) }}<a style="color: red;"; href="https://status.craftycontrol.com/status/craftycontrol"
target="_blank">&nbsp;{{ translate('error', 'craftyStatus', data['lang']) }}</a>
&nbsp;{{ translate('error', 'serverJars2', data['lang']) }}</p></div>
{% end %}
{% if not data["online"] %}
<div class="api-alert" style="position: absolute; top: -5px; z-index: 100; opacity: .99;">
<p style="color: white !important;"><i class="fas fa-exclamation-triangle" style="color: red;"></i>&nbsp;{{ translate('error', 'noInternet', data['lang']) }}</p></div>
{% end %}
</div> </div>
</form> </form>
</div> </div>
@ -124,36 +132,40 @@
<h4>{{ translate('serverWizard', 'importServer', data['lang']) }}</h4> <h4>{{ translate('serverWizard', 'importServer', data['lang']) }}</h4>
<br /> <br />
<form method="post" class="server-wizard" onSubmit="wait_msg(true)"> <form method="post" id="import-jar" class="server-wizard">
{% raw xsrf_form_html() %}
<input type="hidden" value="import_jar" name="create_type">
<div class="form-group"> <div class="form-group">
<label for="server_name">{{ translate('serverWizard', 'serverName', data['lang']) }}</label> <label for="server_name">{{ translate('serverWizard', 'serverName', data['lang']) }}</label>
<input type="text" class="form-control" id="server_name" name="server_name" value="" placeholder="{{ translate('serverWizard', 'myNewServer', data['lang']) }}" required> <input type="text" class="form-control" id="server_name" name="name" value=""
placeholder="{{ translate('serverWizard', 'myNewServer', data['lang']) }}" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="server">{{ translate('serverWizard', 'serverPath', data['lang']) }} <small>{{ <label for="server">{{ translate('serverWizard', 'serverPath', data['lang']) }} <small>{{
translate('serverWizard', 'absoluteServerPath', data['lang']) }}</small></label> translate('serverWizard', 'absoluteServerPath', data['lang']) }}</small></label>
<input type="text" class="form-control" id="server_path" name="server_path" placeholder="/var/opt/server" required> <input type="text" class="form-control" id="server_path" name="server_path" placeholder="/var/opt/server"
required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="server_jar">{{ translate('serverWizard', 'serverJar', data['lang']) }}</label> <label for="server_jar">{{ translate('serverWizard', 'serverJar', data['lang']) }}</label>
<input type="text" class="form-control" id="server_jar" name="server_jar" value="" placeholder="bedrock_server" required> <input type="text" class="form-control" id="server_jar" name="server_jar" value=""
placeholder="bedrock_server" required>
</div> </div>
<br /> <br />
<h4 class="card-title">{{ translate('serverWizard', 'quickSettings', data['lang']) }} <small style="text-transform: none;"> - {{ translate('serverWizard', 'quickSettingsDescription', <h4 class="card-title">{{ translate('serverWizard', 'quickSettings', data['lang']) }} <small
style="text-transform: none;"> - {{ translate('serverWizard', 'quickSettingsDescription',
data['lang']) }}</small></h4> data['lang']) }}</small></h4>
<hr> <hr>
<div class="form-group"> <div class="form-group">
<label for="port2">{{ translate('serverWizard', 'serverPort', data['lang']) }} <label for="port2">{{ translate('serverWizard', 'serverPort', data['lang']) }}
<small></small></label> <small></small></label>
<input type="number" class="form-control" id="port2" name="port" value="19132" step="1" min="1" max="65535" required> <input type="number" class="form-control" id="port2" name="port" value="19132" step="1" min="1"
max="65535" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<div id="accordion-2"> <div id="accordion-2">
<div class="card"> <div class="card">
<div class="card-header p-2" id="Role-2"> <div class="card-header p-2" id="Role-2">
<p class="mb-0 p-0" data-toggle="collapse" data-target="#collapseRole-2" aria-expanded="true" aria-controls="collapseRole-2"> <p class="mb-0 p-0" data-toggle="collapse" data-target="#collapseRole-2" aria-expanded="true"
aria-controls="collapseRole-2">
<i class="fas fa-chevron-down"></i> {{ translate('serverWizard', 'addRole', data['lang']) }} <i class="fas fa-chevron-down"></i> {{ translate('serverWizard', 'addRole', data['lang']) }}
<small style="text-transform: none;"> - {{ translate('serverWizard', 'autoCreate', <small style="text-transform: none;"> - {{ translate('serverWizard', 'autoCreate',
data['lang']) }}</small> data['lang']) }}</small>
@ -186,19 +198,19 @@
<h4>{{ translate('serverWizard', 'importZip', data['lang']) }}</h4> <h4>{{ translate('serverWizard', 'importZip', data['lang']) }}</h4>
<br /> <br />
<form name="zip" method="post" class="server-wizard" onSubmit="wait_msg(true)"> <form name="zip" id="import-zip" method="post" class="server-wizard">
{% raw xsrf_form_html() %}
<input type="hidden" value="import_zip" name="create_type">
<div class="form-group"> <div class="form-group">
<label for="server_name">{{ translate('serverWizard', 'serverName', data['lang']) }}</label> <label for="server_name">{{ translate('serverWizard', 'serverName', data['lang']) }}</label>
<input type="text" class="form-control" id="server_name" name="server_name" value="" placeholder="{{ translate('serverWizard', 'myNewServer', data['lang']) }}" required> <input type="text" class="form-control" id="server_name" name="name" value=""
placeholder="{{ translate('serverWizard', 'myNewServer', data['lang']) }}" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="server">{{ translate('serverWizard', 'zipPath', data['lang']) }} <small>{{ <label for="server">{{ translate('serverWizard', 'zipPath', data['lang']) }} <small>{{
translate('serverWizard', 'absoluteZipPath', data['lang']) }}</small></label> translate('serverWizard', 'absoluteZipPath', data['lang']) }}</small></label>
<input type="text" class="form-control" id="server_path" name="server_path" placeholder="/var/opt/server.zip" required> <input type="text" class="form-control" id="zip_server_path" name="server_path"
placeholder="/var/opt/server.zip" required>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -210,21 +222,26 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="server_jar">{{ translate('serverWizard', 'serverJar', data['lang']) }}</label> <label for="server_jar">{{ translate('serverWizard', 'serverJar', data['lang']) }}</label>
<input type="text" class="form-control" id="server_jar" name="server_jar" value="" placeholder="bedrock_server" required> <input type="text" class="form-control" id="server_jar" name="server_jar" value=""
placeholder="bedrock_server" required>
</div> </div>
<h4 class="card-title">{{ translate('serverWizard', 'quickSettings', data['lang']) }} <small style="text-transform: none;"> - {{ translate('serverWizard', 'quickSettingsDescription', data['lang']) }}</small> <h4 class="card-title">{{ translate('serverWizard', 'quickSettings', data['lang']) }} <small
style="text-transform: none;"> - {{ translate('serverWizard', 'quickSettingsDescription', data['lang'])
}}</small>
</h4> </h4>
<hr> <hr>
<div class="form-group"> <div class="form-group">
<label for="port3">{{ translate('serverWizard', 'serverPort', data['lang']) }} <label for="port3">{{ translate('serverWizard', 'serverPort', data['lang']) }}
<small></small></label> <small></small></label>
<input type="number" class="form-control" id="port3" name="port" value="19132" step="1" min="1" max="65535" required> <input type="number" class="form-control" id="port3" name="port" value="19132" step="1" min="1"
max="65535" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<div id="accordion-3"> <div id="accordion-3">
<div class="card"> <div class="card">
<div class="card-header p-2" id="Role-3"> <div class="card-header p-2" id="Role-3">
<p class="mb-0 p-0" data-toggle="collapse" data-target="#collapseRole-3" aria-expanded="true" aria-controls="collapseRole-3"> <p class="mb-0 p-0" data-toggle="collapse" data-target="#collapseRole-3" aria-expanded="true"
aria-controls="collapseRole-3">
<i class="fas fa-chevron-down"></i> {{ translate('serverWizard', 'addRole', data['lang']) <i class="fas fa-chevron-down"></i> {{ translate('serverWizard', 'addRole', data['lang'])
}} <small style="text-transform: none;"> - {{ translate('serverWizard', 'autoCreate', }} <small style="text-transform: none;"> - {{ translate('serverWizard', 'autoCreate',
data['lang']) }}</small> data['lang']) }}</small>
@ -248,7 +265,8 @@
<input type="text" class="form-control" id="zip_root_path" name="zip_root_path"> <input type="text" class="form-control" id="zip_root_path" name="zip_root_path">
</div> </div>
</div> </div>
<div class="modal fade" id="dir_select" tabindex="-1" role="dialog" aria-labelledby="dir_select" aria-hidden="true"> <div class="modal fade" id="dir_select" tabindex="-1" role="dialog" aria-labelledby="dir_select"
aria-hidden="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@ -259,8 +277,9 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="tree-ctx-item" id="main-tree-div" data-path="" style="overflow: scroll; max-height:75%;"> <div class="tree-ctx-item" id="main-tree-div" data-path=""
<input type="radio" id="main-tree-input" name="root_path" value="" checked> style="overflow: scroll; max-height:75%;">
<input type="radio" class="root-input" id="main-tree-input" name="root_path" value="" checked>
<span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path=""> <span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path="">
<i class="far fa-folder"></i> <i class="far fa-folder"></i>
<i class="far fa-folder-open"></i> <i class="far fa-folder-open"></i>
@ -278,7 +297,8 @@
</div> </div>
</div> </div>
</div> </div>
<button id="zip_submit" type="submit" title="You must select server root dir first" disabled class="btn btn-primary mr-2">{{ translate('serverWizard', 'importServerButton', data['lang']) <button id="zip_submit" type="submit" title="You must select server root dir first" disabled
class="btn btn-primary mr-2">{{ translate('serverWizard', 'importServerButton', data['lang'])
}}</button> }}</button>
<button type="button" class="btn btn-danger mr-2 tree-reset">{{ translate('serverWizard', 'resetForm', <button type="button" class="btn btn-danger mr-2 tree-reset">{{ translate('serverWizard', 'resetForm',
data['lang']) data['lang'])
@ -295,13 +315,12 @@
<br /> <br />
<p class="card-description"> <p class="card-description">
<form name="zip" method="post" class="server-wizard" onSubmit="wait_msg(true)"> <form name="zip" id="import-upload" method="post" class="server-wizard">
{% raw xsrf_form_html() %}
<input type="hidden" value="import_zip" name="create_type">
<div class="form-group"> <div class="form-group">
<label for="server_name">{{ translate('serverWizard', 'serverName', data['lang']) }}</label> <label for="server_name">{{ translate('serverWizard', 'serverName', data['lang']) }}</label>
<input type="text" class="form-control" id="server_name" name="server_name" value="" placeholder="{{ translate('serverWizard', 'myNewServer', data['lang']) }}" required> <input type="text" class="form-control" id="server_name" name="name" value=""
placeholder="{{ translate('serverWizard', 'myNewServer', data['lang']) }}" required>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -309,10 +328,12 @@
<div id="upload_input" class="input-group"> <div id="upload_input" class="input-group">
<div class="custom-file"> <div class="custom-file">
<input type="file" multiple="false" class="custom-file-input" id="file" name="file" required> <input type="file" multiple="false" class="custom-file-input" id="file" name="file" required>
<label id="fileLabel" class="custom-file-label" for="file">{{ translate('serverWizard', 'labelZipFile', data['lang']) }}</label> <label id="fileLabel" class="custom-file-label" for="file">{{ translate('serverWizard',
'labelZipFile', data['lang']) }}</label>
</div> </div>
<div class="input-group-append"> <div class="input-group-append">
<button type="button" class="btn btn-info upload-button" id="upload-button" onclick="sendFile()" disabled>{{ translate('serverWizard', <button type="button" class="btn btn-info upload-button" id="upload-button" onclick="sendFile()"
disabled>{{ translate('serverWizard',
'uploadButton', data['lang']) }}</button> 'uploadButton', data['lang']) }}</button>
</div> </div>
</div> </div>
@ -329,24 +350,28 @@
<div class="form-group"> <div class="form-group">
<label for="server_jar">{{ translate('serverWizard', 'serverJar', data['lang']) }}</label> <label for="server_jar">{{ translate('serverWizard', 'serverJar', data['lang']) }}</label>
<input type="text" class="form-control" id="server_jar" name="server_jar" value="" placeholder="paper.jar" required> <input type="text" class="form-control" id="server_jar" name="server_jar" value=""
placeholder="paper.jar" required>
</div> </div>
<h4 class="card-title">{{ translate('serverWizard', 'quickSettings', data['lang']) }} <small style="text-transform: none;"> - {{ translate('serverWizard', 'quickSettingsDescription', <h4 class="card-title">{{ translate('serverWizard', 'quickSettings', data['lang']) }} <small
style="text-transform: none;"> - {{ translate('serverWizard', 'quickSettingsDescription',
data['lang']) }}</small></h4> data['lang']) }}</small></h4>
<hr> <hr>
<div class="form-group"> <div class="form-group">
<label for="port3">{{ translate('serverWizard', 'serverPort', data['lang']) }} <small> - {{ <label for="port3">{{ translate('serverWizard', 'serverPort', data['lang']) }} <small> - {{
translate('serverWizard', 'defaultPort', data['lang']) }}</small></label> translate('serverWizard', 'defaultPort', data['lang']) }}</small></label>
<input type="number" class="form-control" id="port4" name="port" value="19132" step="1" min="1" max="65535" required> <input type="number" class="form-control" id="port4" name="port" value="19132" step="1" min="1"
max="65535" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<div id="accordion-3"> <div id="accordion-3">
<div class="card"> <div class="card">
<div class="card-header p-2" id="Role-3"> <div class="card-header p-2" id="Role-3">
<p class="mb-0 p-0" data-toggle="collapse" data-target="#collapseRole-3" aria-expanded="true" aria-controls="collapseRole-3"> <p class="mb-0 p-0" data-toggle="collapse" data-target="#collapseRole-3" aria-expanded="true"
aria-controls="collapseRole-3">
<i class="fas fa-chevron-down"></i> {{ translate('serverWizard', 'addRole', <i class="fas fa-chevron-down"></i> {{ translate('serverWizard', 'addRole',
data['lang']) data['lang'])
}} <small style="text-transform: none;"> - {{ translate('serverWizard', 'autoCreate', }} <small style="text-transform: none;"> - {{ translate('serverWizard', 'autoCreate',
@ -357,7 +382,8 @@
<div class="card-body scroll"> <div class="card-body scroll">
<div class="form-group"> <div class="form-group">
{% for r in data['roles'] %} {% for r in data['roles'] %}
<span class="d-block menu-option"><label><input name="{{ r['role_id'] }}" type="checkbox">&nbsp; <span class="d-block menu-option"><label><input name="{{ r['role_id'] }}"
type="checkbox">&nbsp;
{{ r['role_name'].capitalize() }}</label></span> {{ r['role_name'].capitalize() }}</label></span>
{% end %} {% end %}
</div> </div>
@ -371,7 +397,8 @@
<input type="text" class="form-control" id="zip_root_path" name="zip_root_path"> <input type="text" class="form-control" id="zip_root_path" name="zip_root_path">
</div> </div>
</div> </div>
<div class="modal fade" id="dir_upload_select" tabindex="-1" role="dialog" aria-labelledby="dir_select" aria-hidden="true"> <div class="modal fade" id="dir_upload_select" tabindex="-1" role="dialog" aria-labelledby="dir_select"
aria-hidden="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@ -382,8 +409,10 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="tree-ctx-item" id="main-tree-div-upload" data-path="" style="overflow: scroll; max-height:75%;"> <div class="tree-ctx-item" id="main-tree-div-upload" data-path=""
<input type="radio" id="main-tree-input-upload" name="root_path" value="" checked> style="overflow: scroll; max-height:75%;">
<input type="radio" class="root-input" id="main-tree-input-upload" name="root_path" value=""
checked>
<span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path=""> <span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path="">
<i class="far fa-folder"></i> <i class="far fa-folder"></i>
<i class="far fa-folder-open"></i> <i class="far fa-folder-open"></i>
@ -401,7 +430,8 @@
</div> </div>
</div> </div>
</div> </div>
<button id="upload_submit" type="submit" title="You must select server root dir first" disabled class="btn btn-primary mr-2">{{ translate('serverWizard', 'importServerButton', data['lang']) <button id="upload_submit" type="submit" title="You must select server root dir first" disabled
class="btn btn-primary mr-2">{{ translate('serverWizard', 'importServerButton', data['lang'])
}}</button> }}</button>
<button type="button" class="btn btn-danger mr-2 tree-reset">{{ translate('serverWizard', 'resetForm', <button type="button" class="btn btn-danger mr-2 tree-reset">{{ translate('serverWizard', 'resetForm',
data['lang']) data['lang'])
@ -545,7 +575,7 @@
xmlHttpRequest.addEventListener('load', (event) => { xmlHttpRequest.addEventListener('load', (event) => {
if (event.target.responseText == 'success') { if (event.target.responseText == 'success') {
console.log('Upload for file', file.name, 'was successful!') 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%;"><span id="file-uploaded" style="color: gray;">' + fileName + '</span> 🔒</div>'; 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>`;
document.getElementById("lower_half").style.visibility = "visible"; document.getElementById("lower_half").style.visibility = "visible";
} }
else { else {
@ -567,24 +597,20 @@
xmlHttpRequest.send(file); xmlHttpRequest.send(file);
} }
document.getElementById("root_upload_button").addEventListener("click", function () { document.getElementById("root_upload_button").addEventListener("click", function (event) {
if (file) { if (file) {
upload = true; upload = true;
if (document.getElementById('root_upload_button').classList.contains('clicked')) { if (document.getElementById('root_upload_button').classList.contains('clicked')) {
document.getElementById('main-tree-div-upload').innerHTML = '<input type="radio" id="main-tree-input-upload" name="root_path" value="" checked><span id="main-tree-upload" class="files-tree-title tree-caret-down root-dir" data-path=""><i class="far fa-folder"></i><i class="far fa-folder-open"></i>{{ translate("serverFiles", "files", data["lang"]) }}</span></input>' document.getElementById('main-tree-div-upload').innerHTML = '<input type="radio" class="root-input" id="main-tree-input-upload" name="root_path" value="" checked><span id="main-tree-upload" class="files-tree-title tree-caret-down root-dir"><i class="far fa-folder"></i><i class="far fa-folder-open"></i>{{ translate("serverFiles", "files", data["lang"]) }}</span></input>'
} else { } else {
document.getElementById('root_upload_button').classList.add('clicked') document.getElementById('root_upload_button').classList.add('clicked')
} }
var token = getCookie("_xsrf"); const token = getCookie("_xsrf");
var dialog = bootbox.dialog({ var dialog = bootbox.dialog({
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>', message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>',
closeButton: false closeButton: false
}); });
$.ajax({ getDirView();
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/unzip_server?id=-1&file=' + encodeURIComponent(file.name),
});
} else { } else {
bootbox.alert("You must input a path before selecting this button"); bootbox.alert("You must input a path before selecting this button");
} }
@ -606,7 +632,7 @@
}, },
callback: function (result) { callback: function (result) {
if (result == true) { if (result == true) {
document.create_server.submit(); $("#download_exe").submit();
} }
else { else {
return; return;
@ -626,25 +652,19 @@
$(".tree-reset").on("click", function () { $(".tree-reset").on("click", function () {
location.href = "/server/bedrock_step1"; location.href = "/server/bedrock_step1";
}); });
document.getElementById("root_files_button").addEventListener("click", function () { document.getElementById("root_files_button").addEventListener("click", function (event) {
if (document.forms["zip"]["server_path"].value != "") { if (document.forms["zip"]["server_path"].value != "") {
if (document.getElementById('root_files_button').classList.contains('clicked')) { if (document.getElementById('root_files_button').classList.contains('clicked')) {
document.getElementById('main-tree-div').innerHTML = '<input type="radio" id="main-tree-input" name="root_path" value="" checked><span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path=""><i class="far fa-folder"></i><i class="far fa-folder-open"></i>{{ translate('serverFiles', 'files', data['lang']) }}</span></input>' show_file_tree();
return;
} else { } else {
document.getElementById('root_files_button').classList.add('clicked') document.getElementById('root_files_button').classList.add('clicked')
} }
path = document.forms["zip"]["server_path"].value;
console.log(document.forms["zip"]["server_path"].value)
var token = getCookie("_xsrf");
var dialog = bootbox.dialog({ var dialog = bootbox.dialog({
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>', message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>',
closeButton: false closeButton: false
}); });
$.ajax({ getDirView();
type: "POST",
headers: { 'X-XSRFToken': token },
url: '/ajax/unzip_server?id=-1&path=' + encodeURIComponent(path),
});
} else { } else {
bootbox.alert("You must input a path before selecting this button"); bootbox.alert("You must input a path before selecting this button");
} }
@ -671,134 +691,6 @@
}); });
} }
function show_file_tree() {
if (upload) {
$("#dir_upload_select").modal();
} else {
$("#dir_select").modal();
}
}
function getTreeView(path) {
const styles = window.getComputedStyle(document.getElementById("lower_half"));
//If this value is still hidden we know the user is executing a zip import and not an upload
if (styles.visibility === "hidden") {
document.getElementById('zip_submit').disabled = false;
} else {
document.getElementById('upload_submit').disabled = false;
}
path = path
$.ajax({
type: "GET",
url: '/ajax/get_zip_tree?id=-1&path=' + path,
dataType: 'text',
success: function (data) {
console.log("got response:");
console.log(data);
dataArr = data.split('\n');
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
if (styles.visibility === "hidden") {
try {
document.getElementById('main-tree-div').innerHTML += text;
document.getElementById('main-tree').parentElement.classList.add("clicked");
} catch {
document.getElementById('files-tree').innerHTML = text;
}
} else {
try {
document.getElementById('main-tree-div-upload').innerHTML += text;
document.getElementById('main-tree-upload').parentElement.classList.add("clicked");
} catch {
document.getElementById('files-tree').innerHTML = text;
}
}
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-path', serverDir);
document.getElementsByClassName('files-tree-title')[0].setAttribute('data-name', 'Files');
},
});
}
function getToggleMain(event) {
path = event.target.parentElement.getAttribute('data-path');
document.getElementById("files-tree").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
document.getElementById(path + "span").classList.toggle("tree-caret");
}
function getDirView(event) {
path = event.target.parentElement.getAttribute('data-path');
if (document.getElementById(path).classList.contains('clicked')) {
var toggler = document.getElementById(path + "span");
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "ul").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
}
return;
} else {
$.ajax({
type: "GET",
url: '/ajax/get_zip_dir?id=-1&path=' + path,
dataType: 'text',
success: function (data) {
console.log("got response:");
dataArr = data.split('\n');
serverDir = dataArr.shift(); // Remove & return first element (server directory)
text = dataArr.join('\n');
try {
document.getElementById(path + "span").classList.add('tree-caret-down');
document.getElementById(path).innerHTML += text;
document.getElementById(path).classList.add("clicked");
} catch {
console.log("Bad")
}
var toggler = document.getElementById(path);
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "span").addEventListener("click", function caretListener() {
document.getElementById(path + "ul").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
});
}
},
});
}
}
if (webSocket) {
webSocket.on('send_temp_path', function (data) {
setTimeout(function () {
var x = document.querySelector('.bootbox');
if (x) {
x.remove()
}
var x = document.querySelector('.modal-backdrop');
if (x) {
x.remove()
}
document.getElementById('main-tree-input').setAttribute('value', data.path)
document.getElementById('main-tree-input-upload').setAttribute('value', data.path)
getTreeView(data.path);
show_file_tree();
$("#root_files_button").attr("disabled", "disabled");
$("#root_upload_button").attr("disabled", "disabled");
}, 5000);
});
}
$('#file').change(function () { $('#file').change(function () {
console.log("File changed"); console.log("File changed");
if ($('#file').val()) { if ($('#file').val()) {
@ -807,6 +699,187 @@
console.log("File changed good"); console.log("File changed good");
} }
}); });
function replacer(key, value) {
if (key === "roles") {
return value
}
if (key != "ignored_exits") {
if (typeof value == "boolean" || key === "host" || key === "version") {
return value
} else {
return (isNaN(value) ? value : +value);
}
} else {
return value;
}
}
function calcRoles() {
let role_ids = $('.roles').map(function () {
if ($(this).is(':checked')) {
return $(this).val();
}
}).get();
console.log(role_ids)
return role_ids
}
async function send_server(data) {
let token = getCookie("_xsrf")
console.log(token)
let res = await fetch(`/api/v2/servers/`, {
method: 'POST',
headers: {
'X-XSRFToken': token
},
body: data,
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.href = '/panel/dashboard';
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
}
$(document).ready(function () {
$("#download_exe").on("submit", async function (e) {
wait_msg();
e.preventDefault();
let jarForm = document.getElementById("download_exe");
let formData = new FormData(jarForm);
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
console.log(formDataObject);
let send_data = {
"name": formDataObject.name,
"roles": calcRoles(),
"monitoring_type": "minecraft_bedrock",
"minecraft_bedrock_monitoring_data": {
"host": "127.0.0.1",
"port": 19132
},
"create_type": "minecraft_bedrock",
"minecraft_bedrock_create_data": {
"create_type": "download_exe",
"download_exe_create_data": {
//agree to eula since we confirmed before calling this function
"agree_to_eula": true,
}
}
}
console.log(send_data);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(send_data, replacer);
console.log(formDataJsonString);
send_server(formDataJsonString);
});
$("#import-jar").on("submit", async function (e) {
wait_msg(true);
e.preventDefault();
let jarForm = document.getElementById("import-jar");
let formData = new FormData(jarForm);
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
console.log(formDataObject);
let send_data = {
"name": formDataObject.name,
"roles": calcRoles(),
"monitoring_type": "minecraft_bedrock",
"minecraft_bedrock_monitoring_data": {
"host": "127.0.0.1",
"port": formDataObject.port
},
"create_type": "minecraft_bedrock",
"minecraft_bedrock_create_data": {
"create_type": "import_server",
"import_server_create_data": {
"existing_server_path": formDataObject.server_path,
"executable": formDataObject.server_jar,
}
}
}
console.log(send_data);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(send_data, replacer);
send_server(formDataJsonString);
});
$("#import-zip").on("submit", async function (e) {
wait_msg(true);
e.preventDefault();
let jarForm = document.getElementById("import-zip");
let formData = new FormData(jarForm);
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
console.log(formDataObject);
let send_data = {
"name": formDataObject.name,
"roles": calcRoles(),
"monitoring_type": "minecraft_bedrock",
"minecraft_bedrock_monitoring_data": {
"host": "127.0.0.1",
"port": formDataObject.port
},
"create_type": "minecraft_bedrock",
"minecraft_bedrock_create_data": {
"create_type": "import_server",
"import_server_create_data": {
"existing_server_path": formDataObject.root_path,
"executable": formDataObject.server_jar,
}
}
}
console.log(send_data);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(send_data, replacer);
send_server(formDataJsonString);
});
$("#import-upload").on("submit", async function (e) {
wait_msg(true);
e.preventDefault();
let jarForm = document.getElementById("import-upload");
let formData = new FormData(jarForm);
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
console.log(formDataObject);
let send_data = {
"name": formDataObject.name,
"roles": calcRoles(),
"monitoring_type": "minecraft_bedrock",
"minecraft_bedrock_monitoring_data": {
"host": "127.0.0.1",
"port": formDataObject.port
},
"create_type": "minecraft_bedrock",
"minecraft_bedrock_create_data": {
"create_type": "import_server",
"import_server_create_data": {
"existing_server_path": formDataObject.root_path,
"executable": formDataObject.server_jar,
}
}
}
console.log(send_data);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(send_data, replacer);
send_server(formDataJsonString);
});
});
</script> </script>
<script type="text/javascript" src="../../static/assets/js/shared/root-dir.js"></script>
{% end %} {% end %}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
# Generated by database migrator
import peewee
def migrate(migrator, database, **kwargs):
migrator.add_columns("users", cleared_notifs=peewee.CharField(default=""))
"""
Write your migrations here.
"""
def rollback(migrator, database, **kwargs):
migrator.drop_columns("users", ["cleared_notifs"])
"""
Write your rollback migrations here.
"""

View File

@ -171,7 +171,7 @@ if __name__ == "__main__":
Console.info("Remote change complete.") Console.info("Remote change complete.")
import3 = Import3(helper, controller) import3 = Import3(helper, controller)
tasks_manager = TasksManager(helper, controller) tasks_manager = TasksManager(helper, controller, file_helper)
tasks_manager.start_webserver() tasks_manager.start_webserver()
def signal_handler(signum, _frame): def signal_handler(signum, _frame):

View File

@ -5,7 +5,7 @@ bleach==4.1
cached_property==1.5.2 cached_property==1.5.2
colorama==0.4 colorama==0.4
croniter==1.3.5 croniter==1.3.5
cryptography==41.0.1 cryptography==41.0.3
libgravatar==1.0.0 libgravatar==1.0.0
peewee==3.13 peewee==3.13
pexpect==4.8 pexpect==4.8
@ -15,7 +15,7 @@ pyjwt==2.4.0
PyYAML==6.0.1 PyYAML==6.0.1
requests==2.31 requests==2.31
termcolor==1.1 termcolor==1.1
tornado==6.3.2 tornado==6.3.3
tzlocal==4.0 tzlocal==4.0
jsonschema==4.5.1 jsonschema==4.5.1
orjson==3.8.12 orjson==3.8.12

View File

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