"""
-
- 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
diff --git a/app/classes/web/base_handler.py b/app/classes/web/base_handler.py
index e772d633..33fe9936 100644
--- a/app/classes/web/base_handler.py
+++ b/app/classes/web/base_handler.py
@@ -8,6 +8,7 @@ import tornado.web
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.models.users import ApiKeys
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.translation import Translation
from app.classes.models.management import DatabaseShortcuts
@@ -24,15 +25,22 @@ class BaseHandler(tornado.web.RequestHandler):
helper: Helpers
controller: Controller
translator: Translation
+ file_helper: FileHelpers
# noinspection PyAttributeOutsideInit
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.controller = controller
self.tasks_manager = tasks_manager
self.translator = translator
+ self.file_helper = file_helper
def set_default_headers(self) -> None:
"""
diff --git a/app/classes/web/file_handler.py b/app/classes/web/file_handler.py
deleted file mode 100644
index e2d07476..00000000
--- a/app/classes/web/file_handler.py
+++ /dev/null
@@ -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
diff --git a/app/classes/web/panel_handler.py b/app/classes/web/panel_handler.py
index ff55fd26..9c4dfb5b 100644
--- a/app/classes/web/panel_handler.py
+++ b/app/classes/web/panel_handler.py
@@ -1534,40 +1534,8 @@ class PanelHandler(BaseHandler):
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":
- 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"
@@ -1649,606 +1617,3 @@ class PanelHandler(BaseHandler):
utc_offset=(time.timezone * -1 / 60 / 60),
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,
- )
diff --git a/app/classes/web/routes/api/api_handlers.py b/app/classes/web/routes/api/api_handlers.py
index 82a13168..cc8145da 100644
--- a/app/classes/web/routes/api/api_handlers.py
+++ b/app/classes/web/routes/api/api_handlers.py
@@ -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 (
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 (
ApiServersServerTasksTaskChildrenHandler,
)
@@ -44,8 +55,22 @@ from app.classes.web.routes.api.users.user.index import ApiUsersUserIndexHandler
from app.classes.web.routes.api.users.user.permissions import (
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.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):
@@ -61,12 +86,52 @@ def api_handlers(handler_args):
ApiAuthInvalidateTokensHandler,
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
(
r"/api/v2/users/?",
ApiUsersIndexHandler,
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]+)/?",
ApiUsersUserIndexHandler,
@@ -113,11 +178,41 @@ def api_handlers(handler_args):
ApiServersIndexHandler,
handler_args,
),
+ (
+ r"/api/v2/crafty/exeCache/?",
+ ApiCraftyExeCacheIndexHandler,
+ handler_args,
+ ),
(
r"/api/v2/servers/([0-9]+)/?",
ApiServersServerIndexHandler,
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/?",
ApiServersServerTasksIndexHandler,
diff --git a/app/classes/web/routes/api/crafty/announcements/index.py b/app/classes/web/routes/api/crafty/announcements/index.py
new file mode 100644
index 00000000..409aceed
--- /dev/null
+++ b/app/classes/web/routes/api/crafty/announcements/index.py
@@ -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": {},
+ },
+ )
diff --git a/app/classes/web/routes/api/crafty/clogs/index.py b/app/classes/web/routes/api/crafty/clogs/index.py
new file mode 100644
index 00000000..97a24a34
--- /dev/null
+++ b/app/classes/web/routes/api/crafty/clogs/index.py
@@ -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
diff --git a/app/classes/web/routes/api/crafty/config/index.py b/app/classes/web/routes/api/crafty/config/index.py
new file mode 100644
index 00000000..a2bff723
--- /dev/null
+++ b/app/classes/web/routes/api/crafty/config/index.py
@@ -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"})
diff --git a/app/classes/web/routes/api/crafty/config/server_dir.py b/app/classes/web/routes/api/crafty/config/server_dir.py
new file mode 100644
index 00000000..4e41be14
--- /dev/null
+++ b/app/classes/web/routes/api/crafty/config/server_dir.py
@@ -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"},
+ )
diff --git a/app/classes/web/routes/api/crafty/exe_cache.py b/app/classes/web/routes/api/crafty/exe_cache.py
new file mode 100644
index 00000000..a779195d
--- /dev/null
+++ b/app/classes/web/routes/api/crafty/exe_cache.py
@@ -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(),
+ },
+ )
diff --git a/app/classes/web/routes/api/crafty/imports/index.py b/app/classes/web/routes/api/crafty/imports/index.py
new file mode 100644
index 00000000..643a0aa4
--- /dev/null
+++ b/app/classes/web/routes/api/crafty/imports/index.py
@@ -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})
diff --git a/app/classes/web/routes/api/roles/index.py b/app/classes/web/routes/api/roles/index.py
index 150bff0c..0d46e11b 100644
--- a/app/classes/web/routes/api/roles/index.py
+++ b/app/classes/web/routes/api/roles/index.py
@@ -28,9 +28,39 @@ create_role_schema = {
"required": ["server_id", "permissions"],
},
},
+ "manager": {"type": ["integer", "null"]},
},
- "required": ["name"],
"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:
- 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:
return self.finish_json(
400,
@@ -98,6 +131,9 @@ class ApiRolesIndexHandler(BaseApiHandler):
)
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
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"}
)
- role_id = self.controller.roles.add_role_advanced(
- role_name, servers, user["user_id"]
- )
+ role_id = self.controller.roles.add_role_advanced(role_name, servers, manager)
self.controller.management.add_to_audit_log(
user["user_id"],
diff --git a/app/classes/web/routes/api/roles/role/index.py b/app/classes/web/routes/api/roles/role/index.py
index 20354722..0dd7d6c8 100644
--- a/app/classes/web/routes/api/roles/role/index.py
+++ b/app/classes/web/routes/api/roles/role/index.py
@@ -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:
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:
return self.finish_json(404, {"status": "error", "error": "ROLE_NOT_FOUND"})
diff --git a/app/classes/web/routes/api/servers/index.py b/app/classes/web/routes/api/servers/index.py
index edfec8fc..d468f979 100644
--- a/app/classes/web/routes/api/servers/index.py
+++ b/app/classes/web/routes/api/servers/index.py
@@ -24,6 +24,7 @@ new_server_schema = {
"examples": ["My Server"],
"minLength": 2,
},
+ "roles": {"title": "Roles to add", "type": "array", "examples": [1, 2, 3]},
"stop_command": {
"title": "Stop command",
"description": '"" means the default for the server creation type.',
@@ -133,8 +134,13 @@ new_server_schema = {
"mem_min",
"mem_max",
"server_properties_port",
- "agree_to_eula",
+ "category",
],
+ "category": {
+ "title": "Jar Category",
+ "type": "string",
+ "examples": ["modded", "vanilla"],
+ },
"properties": {
"type": {
"title": "Server JAR Type",
@@ -185,7 +191,6 @@ new_server_schema = {
"mem_min",
"mem_max",
"server_properties_port",
- "agree_to_eula",
],
"properties": {
"existing_server_path": {
@@ -240,7 +245,6 @@ new_server_schema = {
"mem_min",
"mem_max",
"server_properties_port",
- "agree_to_eula",
],
"properties": {
"zip_path": {
@@ -336,12 +340,24 @@ new_server_schema = {
"title": "Creation type",
"type": "string",
"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": {
"title": "Import server data",
"type": "object",
- "required": ["existing_server_path", "command"],
+ "required": ["existing_server_path", "executable"],
"properties": {
"existing_server_path": {
"title": "Server path",
@@ -350,6 +366,14 @@ new_server_schema = {
"examples": ["/var/opt/server"],
"minLength": 1,
},
+ "executable": {
+ "title": "Executable File",
+ "description": "File Crafty should execute"
+ "on server launch",
+ "type": "string",
+ "examples": ["bedrock_server.exe"],
+ "minlength": 1,
+ },
"command": {
"title": "Command",
"type": "string",
@@ -371,6 +395,14 @@ new_server_schema = {
"examples": ["/var/opt/server.zip"],
"minLength": 1,
},
+ "executable": {
+ "title": "Executable File",
+ "description": "File Crafty should execute"
+ "on server launch",
+ "type": "string",
+ "examples": ["bedrock_server.exe"],
+ "minlength": 1,
+ },
"zip_root": {
"title": "Server root directory",
"description": "The server root in the ZIP archive",
@@ -394,7 +426,9 @@ new_server_schema = {
"allOf": [
{
"if": {
- "properties": {"create_type": {"const": "import_exec"}}
+ "properties": {
+ "create_type": {"const": "import_server"}
+ }
},
"then": {"required": ["import_server_create_data"]},
},
@@ -404,6 +438,16 @@ new_server_schema = {
},
"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": [
{"required": ["import_server_create_data"]},
{"required": ["import_zip_create_data"]},
+ {"required": ["download_exe_create_data"]},
],
},
],
@@ -651,7 +696,6 @@ class ApiServersIndexHandler(BaseApiHandler):
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
-
try:
validate(data, new_server_schema)
except ValidationError as e:
diff --git a/app/classes/web/routes/api/servers/server/action.py b/app/classes/web/routes/api/servers/server/action.py
index e5b3ae23..153b889d 100644
--- a/app/classes/web/routes/api/servers/server/action.py
+++ b/app/classes/web/routes/api/servers/server/action.py
@@ -31,6 +31,8 @@ class ApiServersServerActionHandler(BaseApiHandler):
if action == "clone_server":
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(
auth_data[4]["user_id"], server_id, self.get_remote_ip(), action
@@ -41,6 +43,11 @@ class ApiServersServerActionHandler(BaseApiHandler):
{"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 is_name_used(name):
return Servers.select().where(Servers.server_name == name).exists()
diff --git a/app/classes/web/routes/api/servers/server/backups/backup/index.py b/app/classes/web/routes/api/servers/server/backups/backup/index.py
new file mode 100644
index 00000000..20a9ded0
--- /dev/null
+++ b/app/classes/web/routes/api/servers/server/backups/backup/index.py
@@ -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"})
diff --git a/app/classes/web/routes/api/servers/server/backups/index.py b/app/classes/web/routes/api/servers/server/backups/index.py
new file mode 100644
index 00000000..9e47bcfc
--- /dev/null
+++ b/app/classes/web/routes/api/servers/server/backups/index.py
@@ -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"})
diff --git a/app/classes/web/routes/api/servers/server/files.py b/app/classes/web/routes/api/servers/server/files.py
new file mode 100644
index 00000000..9ed720ac
--- /dev/null
+++ b/app/classes/web/routes/api/servers/server/files.py
@@ -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"})
diff --git a/app/classes/web/routes/api/servers/server/logs.py b/app/classes/web/routes/api/servers/server/logs.py
index 641a1163..94a8a71b 100644
--- a/app/classes/web/routes/api/servers/server/logs.py
+++ b/app/classes/web/routes/api/servers/server/logs.py
@@ -74,6 +74,6 @@ class ApiServersServerLogsHandler(BaseApiHandler):
if use_html:
for line in lines:
- self.write(f"{line} ")
- else:
- self.finish_json(200, {"status": "ok", "data": lines})
+ line = f"{line} "
+
+ self.finish_json(200, {"status": "ok", "data": lines})
diff --git a/app/classes/web/routes/api/users/index.py b/app/classes/web/routes/api/users/index.py
index a1f849ef..f7341d38 100644
--- a/app/classes/web/routes/api/users/index.py
+++ b/app/classes/web/routes/api/users/index.py
@@ -93,10 +93,17 @@ class ApiUsersIndexHandler(BaseApiHandler):
"error_data": str(e),
},
)
-
username = data["username"]
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"]
email = data.get("email", "default@example.com")
enabled = data.get("enabled", True)
diff --git a/app/classes/web/routes/api/users/user/api.py b/app/classes/web/routes/api/users/user/api.py
new file mode 100644
index 00000000..1c7635f2
--- /dev/null
+++ b/app/classes/web/routes/api/users/user/api.py
@@ -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}},
+ )
diff --git a/app/classes/web/routes/api/users/user/index.py b/app/classes/web/routes/api/users/user/index.py
index 47d8dd68..73f17f9f 100644
--- a/app/classes/web/routes/api/users/user/index.py
+++ b/app/classes/web/routes/api/users/user/index.py
@@ -166,7 +166,13 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
return self.finish_json(
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(
400, {"status": "error", "error": "USER_EXISTS"}
)
@@ -210,13 +216,13 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
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)
+ 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:
roles: t.Set[str] = set(data.pop("roles"))
@@ -236,6 +242,12 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
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:
permissions: t.List[UsersController.ApiPermissionDict] = data.pop(
"permissions"
@@ -246,7 +258,7 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
limit_role_creation = 0
for permission in permissions:
- self.controller.crafty_perms.set_permission(
+ permissions_mask = self.controller.crafty_perms.set_permission(
permissions_mask,
EnumPermissionsCrafty.__members__[permission["name"]],
"1" if permission["enabled"] else "0",
diff --git a/app/classes/web/server_handler.py b/app/classes/web/server_handler.py
index eae3ce0c..69864049 100644
--- a/app/classes/web/server_handler.py
+++ b/app/classes/web/server_handler.py
@@ -1,14 +1,10 @@
import json
import logging
-import os
-import time
import tornado.web
import tornado.escape
-import bleach
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
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.web.base_handler import BaseHandler
@@ -174,441 +170,3 @@ class ServerHandler(BaseHandler):
data=page_data,
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": ""
- " 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")
-
- try:
- self.render(
- template,
- data=page_data,
- translate=self.translator.translate,
- )
- except RuntimeError:
- self.redirect("/panel/dashboard")
diff --git a/app/classes/web/tornado_handler.py b/app/classes/web/tornado_handler.py
index d2b047d7..c10184dc 100644
--- a/app/classes/web/tornado_handler.py
+++ b/app/classes/web/tornado_handler.py
@@ -15,13 +15,11 @@ from app.classes.models.management import HelpersManagement
from app.classes.shared.console import Console
from app.classes.shared.helpers import Helpers
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.panel_handler import PanelHandler
from app.classes.web.default_handler import DefaultHandler
from app.classes.web.routes.api.api_handlers import api_handlers
from app.classes.web.server_handler import ServerHandler
-from app.classes.web.ajax_handler import AjaxHandler
from app.classes.web.api_handler import (
ServersStats,
NodeStats,
@@ -48,13 +46,14 @@ class Webserver:
controller: Controller
helper: Helpers
- def __init__(self, helper, controller, tasks_manager):
+ def __init__(self, helper, controller, tasks_manager, file_helper):
self.ioloop = None
self.http_server = None
self.https_server = None
self.helper = helper
self.controller = controller
self.tasks_manager = tasks_manager
+ self.file_helper = file_helper
self._asyncio_patch()
@staticmethod
@@ -146,13 +145,12 @@ class Webserver:
"controller": self.controller,
"tasks_manager": self.tasks_manager,
"translator": self.helper.translation,
+ "file_helper": self.file_helper,
}
handlers = [
(r"/", DefaultHandler, handler_args),
(r"/panel/(.*)", PanelHandler, handler_args),
(r"/server/(.*)", ServerHandler, handler_args),
- (r"/ajax/(.*)", AjaxHandler, handler_args),
- (r"/files/(.*)", FileHandler, handler_args),
(r"/ws", SocketHandler, handler_args),
(r"/upload", UploadHandler, handler_args),
(r"/status", StatusHandler, handler_args),
diff --git a/app/classes/web/upload_handler.py b/app/classes/web/upload_handler.py
index adce3ab9..88510a80 100644
--- a/app/classes/web/upload_handler.py
+++ b/app/classes/web/upload_handler.py
@@ -25,11 +25,13 @@ class UploadHandler(BaseHandler):
controller: Controller = None,
tasks_manager=None,
translator=None,
+ file_helper=None,
):
self.helper = helper
self.controller = controller
self.tasks_manager = tasks_manager
self.translator = translator
+ self.file_helper = file_helper
def prepare(self):
# Class & Function Defination
diff --git a/app/classes/web/websocket_handler.py b/app/classes/web/websocket_handler.py
index 78b33951..a2e7f5a4 100644
--- a/app/classes/web/websocket_handler.py
+++ b/app/classes/web/websocket_handler.py
@@ -18,12 +18,18 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
io_loop = None
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.controller = controller
self.tasks_manager = tasks_manager
self.translator = translator
+ self.file_helper = file_helper
self.io_loop = tornado.ioloop.IOLoop.current()
def get_remote_ip(self):
diff --git a/app/config/version.json b/app/config/version.json
index 49cae729..6c1274e0 100644
--- a/app/config/version.json
+++ b/app/config/version.json
@@ -1,5 +1,5 @@
{
"major": 4,
- "minor": 1,
- "sub": 4
+ "minor": 2,
+ "sub": 0
}
diff --git a/app/frontend/static/assets/js/shared/root-dir.js b/app/frontend/static/assets/js/shared/root-dir.js
new file mode 100644
index 00000000..6882b577
--- /dev/null
+++ b/app/frontend/static/assets/js/shared/root-dir.js
@@ -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 = `
`;
+ 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 += `