diff --git a/.gitignore b/.gitignore index 132a2a26..d3b153ad 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ app/config/ docker/* !docker/docker-compose.yml lang_sort_log.txt +lang_sort.txt diff --git a/.gitlab/scripts/sort.py b/.gitlab/scripts/sort.py new file mode 100644 index 00000000..c78885a6 --- /dev/null +++ b/.gitlab/scripts/sort.py @@ -0,0 +1,72 @@ +import json +import os + + +def get_missing_keys_and_values(obj1, obj2, path=None): + if path is None: + path = [] + + missing_keys_and_values = {} + + if isinstance(obj1, dict) and isinstance(obj2, dict): + for key in obj1: + if key not in obj2: + missing_keys_and_values[key] = obj1[key] + elif isinstance(obj1[key], (dict, list)) and isinstance( + obj2[key], (dict, list) + ): + sub_missing = get_missing_keys_and_values( + obj1[key], obj2[key], path + [key] + ) + if sub_missing: + missing_keys_and_values[key] = sub_missing + + return missing_keys_and_values + + +def main(): + project_dir = os.getcwd() + os.chdir("../../app/translations") # Change the working directory + dir_path = os.getcwd() # Get the current working directory + + en_en_path = os.path.join(dir_path, "en_EN.json") + + if not os.path.isfile(en_en_path): + print( + f"The file en_EN.json does not exist in {dir_path}. Ensure you have the right directory, Exiting." + ) + return + + result = {} # JSON object to store missing keys and values + + for root, _, files in os.walk(dir_path): + for file in files: + if ( + "_incomplete" not in file + and file != "en_EN.json" + and file.endswith(".json") + ): + file_path = os.path.join(root, file) + + with open(file_path, "r", encoding="utf-8") as current_file: + current_data = json.load(current_file) + + with open(en_en_path, "r", encoding="utf-8") as en_en_file: + en_en_data = json.load(en_en_file) + + missing_keys_and_values = get_missing_keys_and_values( + en_en_data, current_data + ) + if missing_keys_and_values: + result[file] = missing_keys_and_values + + # Write the JSON object to lang_sort.txt + with open( + os.path.join(project_dir, "lang_sort.txt"), + "w", + encoding="utf-8", + ) as output_file: + json.dump(result, output_file, indent=4) + + +main() diff --git a/CHANGELOG.md b/CHANGELOG.md index 7054c1b1..1c0dab4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,36 @@ # Changelog +## --- [4.2.2] - 2023/12/13 +### New features +- Loading Screen for Crafty during startup ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/668)) +### Refactor +- Remove deprecated API V1 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/670)) +- Tidy up main.py to be more comprehensive ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/668)) +- Force random password on first run. Stop using common default password ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/672) | [Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/673)) +### Bug fixes +- Remove webhook `custom` option from webook provider list as it's not currently an option ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/664)) +- Bump cryptography for CVE-2023-49083 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/680)) +- Fix bug where su cannot edit general user password ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/676)) +- Fix bug where no file error on import root dir ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/677)) +- Fix Unban button failing to pardon users ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/671)) +- Fix stack in API error handling ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/674)) +- Fix bug where you cannot select "do not monitor mounts" from `config.json` ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/678)) +- Fix support log 'x' button still downloading logs ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/679)) +- Fix bug where servers are created without bu dir ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/682)) +### Tweaks +- Homogenize Panel logos/branding ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/666)) +- Retain previous tab when revisiting server details page (#272)([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/667)) +- Add server name tag in panel header (#272)([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/667)) +- Setup logging for panel authentication attempts ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/669)) +- Update minimum password length from 6 to 8, and unrestrict maximum password length ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/669)) +- Give better feedback when backup delete fails ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/681)) +- Add user queue debug logging ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/683)) +### Lang +- Update `de_DE, en_EN, fr_FR, lol_EN, lv_LV, nl_BE, pl_PL, zh_CN` translations for `4.2.2` ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/684)) +- Mark `es_ES` as incomplete ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/684)) +- Mark `he_IL` as active ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/684)) +- pl_PL Minor fixes ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/675)) +

+ ## --- [4.2.1] - 2023/11/01 ### Bug fixes - Fix logic issue with `get_files` API permissions check ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/654)) diff --git a/README.md b/README.md index a805eca7..0a94b6cb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![Crafty Logo](app/frontend/static/assets/images/logo_long.svg)](https://craftycontrol.com) -# Crafty Controller 4.2.1 +# Crafty Controller 4.2.2 > Python based Control Panel for your Minecraft Server ## What is Crafty Controller? diff --git a/app/classes/controllers/servers_controller.py b/app/classes/controllers/servers_controller.py index c0bae7b0..88923194 100644 --- a/app/classes/controllers/servers_controller.py +++ b/app/classes/controllers/servers_controller.py @@ -22,6 +22,7 @@ from app.classes.models.server_permissions import ( PermissionsServers, EnumPermissionsServer, ) +from app.classes.shared.websocket_manager import WebSocketManager logger = logging.getLogger(__name__) @@ -36,6 +37,8 @@ class ServersController(metaclass=Singleton): self.management_helper = management_helper self.servers_list = [] self.stats = Stats(self.helper, self) + self.web_sock = WebSocketManager() + self.server_subpage = {} # ********************************************************************************** # Generic Servers Methods @@ -169,8 +172,15 @@ class ServersController(metaclass=Singleton): def init_all_servers(self): servers = self.get_all_defined_servers() self.failed_servers = [] - for server in servers: + self.web_sock.broadcast_to_admins( + "update", + {"section": "server", "server": server["server_name"]}, + ) + self.web_sock.broadcast_to_non_admins( + "update", + {"section": "init"}, + ) server_id = server.get("server_id") # if we have already initialized this server, let's skip it. diff --git a/app/classes/controllers/users_controller.py b/app/classes/controllers/users_controller.py index ed53ad61..87cc513c 100644 --- a/app/classes/controllers/users_controller.py +++ b/app/classes/controllers/users_controller.py @@ -45,8 +45,7 @@ class UsersController: }, "password": { "type": "string", - "maxLength": 20, - "minLength": 6, + "minLength": 8, "examples": ["crafty"], "title": "Password", }, diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 62ce8819..9c63a323 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -80,6 +80,7 @@ class Helpers: self.translation = Translation(self) self.update_available = False self.ignored_names = ["crafty_managed.txt", "db_stats"] + self.crafty_starting = False @staticmethod def auto_installer_fix(ex): @@ -361,6 +362,42 @@ class Helpers: return result_of_check == 0 + def create_pass(self): + # Maximum length of password needed + max_len = 64 + + # Declare string of the character that we need in our password + digits = string.digits + locase = string.ascii_lowercase + upcase = string.ascii_uppercase + symbols = "!@#$%^&*" # Reducing to avoid issues with ([]{}<>,'`) etc + + # Combine all the character strings above to form one string + combo = digits + upcase + locase + symbols + + # Randomly select at least one character from each character set above + rand_digit = secrets.choice(digits) + rand_upper = secrets.choice(upcase) + rand_lower = secrets.choice(locase) + rand_symbol = secrets.choice(symbols) + + # Combine the character randomly selected above + temp_pass = rand_digit + rand_upper + rand_lower + rand_symbol + + # Fill the rest of the password length by selecting randomly char list + for _ in range(max_len - 4): + temp_pass += secrets.choice(combo) + + # Shuffle the temporary password to prevent predictable patterns + temp_pass_list = list(temp_pass) + secrets.SystemRandom().shuffle(temp_pass_list) + + # Form the password by concatenating the characters + password = "".join(temp_pass_list) + + # Return completed password + return password + @staticmethod def cmdparse(cmd_in): # Parse a string into arguments diff --git a/app/classes/shared/main_controller.py b/app/classes/shared/main_controller.py index 23586696..ae58d988 100644 --- a/app/classes/shared/main_controller.py +++ b/app/classes/shared/main_controller.py @@ -78,6 +78,37 @@ class Controller: self.first_login = False self.cached_login = self.management.get_login_image() self.support_scheduler.start() + try: + with open( + os.path.join(os.path.curdir, "logs", "auth_tracker.log"), + "r", + encoding="utf-8", + ) as f: + self.auth_tracker = json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + self.auth_tracker = {} + + def log_attempt(self, remote_ip, username): + remote = self.auth_tracker.get(str(remote_ip), None) + if remote: + remote["names"].append(username) + remote["attempts"] += 1 + remote["times"].append(datetime.now().strftime("%d/%m/%Y %H:%M:%S")) + self.auth_tracker[str(remote_ip)] = remote + else: + self.auth_tracker[str(remote_ip)] = { + "names": [username], + "attempts": 1, + "times": [datetime.now().strftime("%d/%m/%Y %H:%M:%S")], + } + + def write_auth_tracker(self): + with open( + os.path.join(os.path.curdir, "logs", "auth_tracker.log"), + "w", + encoding="utf-8", + ) as f: + json.dump(self.auth_tracker, f, indent=4) @staticmethod def check_system_user(): @@ -498,6 +529,10 @@ class Controller: server_host=monitoring_host, server_type=monitoring_type, ) + self.management.set_backup_config( + new_server_id, + backup_path, + ) if data["create_type"] == "minecraft_java": if root_create_data["create_type"] == "download_jar": # modded update urls from server jars will only update the installer diff --git a/app/classes/shared/main_models.py b/app/classes/shared/main_models.py index 4bfca52c..c166b7fb 100644 --- a/app/classes/shared/main_models.py +++ b/app/classes/shared/main_models.py @@ -14,13 +14,17 @@ class DatabaseBuilder: self.management_helper = management_helper self.users_helper = users_helper - def default_settings(self): + def default_settings(self, password="crafty"): logger.info("Fresh Install Detected - Creating Default Settings") Console.info("Fresh Install Detected - Creating Default Settings") default_data = self.helper.find_default_password() - + if password not in default_data: + Console.help( + "No default password found. Using password created " + "by Crafty. Find it in app/config/default-creds.txt" + ) username = default_data.get("username", "admin") - password = default_data.get("password", "crafty") + password = default_data.get("password", password) self.users_helper.add_user( username=username, diff --git a/app/classes/shared/tasks.py b/app/classes/shared/tasks.py index 0402c587..a5ea32ac 100644 --- a/app/classes/shared/tasks.py +++ b/app/classes/shared/tasks.py @@ -23,6 +23,7 @@ from app.classes.web.tornado_handler import Webserver from app.classes.shared.websocket_manager import WebSocketManager logger = logging.getLogger("apscheduler") +command_log = logging.getLogger("cmd_queue") scheduler_intervals = { "seconds", "minutes", @@ -94,7 +95,15 @@ class TasksManager: def command_watcher(self): while True: # select any commands waiting to be processed + command_log.debug( + "Queue currently has " + f"{self.controller.management.command_queue.qsize()} queued commands." + ) if not self.controller.management.command_queue.empty(): + command_log.info( + "Current queued commands: " + f"{list(self.controller.management.command_queue.queue)}" + ) cmd = self.controller.management.command_queue.get() try: svr = self.controller.servers.get_server_instance_by_id( @@ -201,6 +210,13 @@ class TasksManager: id="update_watcher", start_date=datetime.datetime.now(), ) + self.scheduler.add_job( + self.controller.write_auth_tracker, + "interval", + minutes=5, + id="auth_tracker_write", + start_date=datetime.datetime.now(), + ) # self.scheduler.add_job( # self.scheduler.print_jobs, "interval", seconds=10, id="-1" # ) diff --git a/app/classes/shared/websocket_manager.py b/app/classes/shared/websocket_manager.py index f48adef8..7cda296d 100644 --- a/app/classes/shared/websocket_manager.py +++ b/app/classes/shared/websocket_manager.py @@ -37,7 +37,15 @@ class WebSocketManager(metaclass=Singleton): def broadcast_to_admins(self, event_type: str, data): def filter_fn(client): - if client.get_user_id in HelperUsers.get_super_user_list(): + if str(client.get_user_id()) in str(HelperUsers.get_super_user_list()): + return True + return False + + self.broadcast_with_fn(filter_fn, event_type, data) + + def broadcast_to_non_admins(self, event_type: str, data): + def filter_fn(client): + if str(client.get_user_id()) not in str(HelperUsers.get_super_user_list()): return True return False diff --git a/app/classes/web/api_handler.py b/app/classes/web/api_handler.py deleted file mode 100644 index 34b09ee8..00000000 --- a/app/classes/web/api_handler.py +++ /dev/null @@ -1,446 +0,0 @@ -from datetime import datetime -import logging -import re - -from app.classes.controllers.crafty_perms_controller import EnumPermissionsCrafty -from app.classes.controllers.server_perms_controller import EnumPermissionsServer -from app.classes.web.base_handler import BaseHandler -from app.classes.models.management import DatabaseShortcuts - -logger = logging.getLogger(__name__) -bearer_pattern = re.compile(r"^Bearer", flags=re.IGNORECASE) - - -class ApiHandler(BaseHandler): - def return_response(self, status: int, data: dict): - # Define a standardized response - self.set_status(status) - self.write(data) - - def check_xsrf_cookie(self): - # Disable CSRF protection on API routes - pass - - def access_denied(self, user, reason=""): - if reason: - reason = " because " + reason - logger.info( - "User %s from IP %s was denied access to the API route " - + self.request.path - + reason, - user, - self.get_remote_ip(), - ) - self.finish( - self.return_response( - 403, - { - "error": "ACCESS_DENIED", - "info": "You were denied access to the requested resource", - }, - ) - ) - - def authenticate_user(self) -> bool: - self.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, - "Server_Creation": EnumPermissionsCrafty.SERVER_CREATION, - "User_Config": EnumPermissionsCrafty.USER_CONFIG, - "Roles_Config": EnumPermissionsCrafty.ROLES_CONFIG, - } - try: - logger.debug("Searching for specified token") - - api_token = self.get_argument("token", "") - self.api_token = api_token - if api_token is None and self.request.headers.get("Authorization"): - api_token = bearer_pattern.sub( - "", self.request.headers.get("Authorization") - ) - elif api_token is None: - api_token = self.get_cookie("token") - user_data = self.controller.users.get_user_by_api_token(api_token) - - logger.debug("Checking results") - if user_data: - # Login successful! Check perms - logger.info(f"User {user_data['username']} has authenticated to API") - - return True # This is to set the "authenticated" - logging.debug("Auth unsuccessful") - self.access_denied("unknown", "the user provided an invalid token") - return False - except Exception as e: - logger.warning("An error occured while authenticating an API user: %s", e) - self.finish( - self.return_response( - 403, - { - "error": "ACCESS_DENIED", - "info": "An error occured while authenticating the user", - }, - ) - ) - return False - - -class ServersStats(ApiHandler): - def get(self): - """Get details about all servers""" - authenticated = self.authenticate_user() - user_obj = self.controller.users.get_user_by_api_token(self.api_token) - if not authenticated: - return - if user_obj["superuser"]: - raw_stats = self.controller.servers.get_all_servers_stats() - else: - raw_stats = self.controller.servers.get_authorized_servers_stats( - user_obj["user_id"] - ) - stats = [] - for rs in raw_stats: - s = {} - for k, v in rs["server_data"].items(): - if isinstance(v, datetime): - s[k] = v.timestamp() - else: - s[k] = v - stats.append(s) - - # Get server stats - # TODO Check perms - self.finish(self.write({"servers": stats})) - - -class NodeStats(ApiHandler): - def get(self): - """Get stats for particular node""" - authenticated = self.authenticate_user() - if not authenticated: - return - - # Get node stats - node_stats = self.controller.servers.stats.get_node_stats() - self.return_response(200, {"code": node_stats["node_stats"]}) - - -class SendCommand(ApiHandler): - def post(self): - user = self.authenticate_user() - - user_obj = self.controller.users.get_user_by_api_token(self.api_token) - - if user is None: - self.access_denied("unknown") - return - server_id = self.get_argument("id") - - if ( - not user_obj["user_id"] - in self.controller.server_perms.get_server_user_list(server_id) - and not user_obj["superuser"] - ): - self.access_denied("unknown") - return - - if not self.permissions[ - "Commands" - ] in self.controller.server_perms.get_api_key_permissions_list( - self.controller.users.get_api_key_by_token(self.api_token), server_id - ): - self.access_denied(user) - return - - command = self.get_argument("command", default=None, strip=True) - server_id = self.get_argument("id") - if command: - server = self.controller.servers.get_server_instance_by_id(server_id) - if server.check_running: - server.send_command(command) - self.return_response(200, {"run": True}) - else: - self.return_response(200, {"error": "SER_NOT_RUNNING"}) - else: - self.return_response(200, {"error": "NO_COMMAND"}) - - -class ServerBackup(ApiHandler): - def post(self): - user = self.authenticate_user() - - user_obj = self.controller.users.get_user_by_api_token(self.api_token) - - if user is None: - self.access_denied("unknown") - return - server_id = self.get_argument("id") - - if ( - not user_obj["user_id"] - in self.controller.server_perms.get_server_user_list(server_id) - and not user_obj["superuser"] - ): - self.access_denied("unknown") - return - - if not self.permissions[ - "Backup" - ] in self.controller.server_perms.get_api_key_permissions_list( - self.controller.users.get_api_key_by_token(self.api_token), server_id - ): - self.access_denied(user) - return - - server = self.controller.servers.get_server_instance_by_id(server_id) - - server.backup_server() - - self.return_response(200, {"code": "SER_BAK_CALLED"}) - - -class StartServer(ApiHandler): - def post(self): - user = self.authenticate_user() - remote_ip = self.get_remote_ip() - - user_obj = self.controller.users.get_user_by_api_token(self.api_token) - - if user is None: - self.access_denied("unknown") - return - server_id = self.get_argument("id") - - if ( - not user_obj["user_id"] - in self.controller.server_perms.get_server_user_list(server_id) - and not user_obj["superuser"] - ): - self.access_denied("unknown") - return - if not self.permissions[ - "Commands" - ] in self.controller.server_perms.get_api_key_permissions_list( - self.controller.users.get_api_key_by_token(self.api_token), server_id - ): - self.access_denied("unknown") - return - - server = self.controller.servers.get_server_instance_by_id(server_id) - - if not server.check_running(): - self.controller.management.send_command( - user_obj["user_id"], server_id, remote_ip, "start_server" - ) - self.return_response(200, {"code": "SER_START_CALLED"}) - else: - self.return_response(500, {"error": "SER_RUNNING"}) - - -class StopServer(ApiHandler): - def post(self): - user = self.authenticate_user() - remote_ip = self.get_remote_ip() - - user_obj = self.controller.users.get_user_by_api_token(self.api_token) - - if user is None: - self.access_denied("unknown") - return - server_id = self.get_argument("id") - - if ( - not user_obj["user_id"] - in self.controller.server_perms.get_server_user_list(server_id) - and not user_obj["superuser"] - ): - self.access_denied("unknown") - - if not self.permissions[ - "Commands" - ] in self.controller.server_perms.get_api_key_permissions_list( - self.controller.users.get_api_key_by_token(self.api_token), server_id - ): - self.access_denied(user) - return - - server = self.controller.servers.get_server_instance_by_id(server_id) - - if server.check_running(): - self.controller.management.send_command( - user, server_id, remote_ip, "stop_server" - ) - - self.return_response(200, {"code": "SER_STOP_CALLED"}) - else: - self.return_response(500, {"error": "SER_NOT_RUNNING"}) - - -class RestartServer(ApiHandler): - def post(self): - user = self.authenticate_user() - remote_ip = self.get_remote_ip() - user_obj = self.controller.users.get_user_by_api_token(self.api_token) - - if user is None: - self.access_denied("unknown") - return - server_id = self.get_argument("id") - - if not user_obj["user_id"] in self.controller.server_perms.get_server_user_list( - server_id - ): - self.access_denied("unknown") - - if not self.permissions[ - "Commands" - ] in self.controller.server_perms.get_api_key_permissions_list( - self.controller.users.get_api_key_by_token(self.api_token), server_id - ): - self.access_denied(user) - - self.controller.management.send_command( - user, server_id, remote_ip, "restart_server" - ) - self.return_response(200, {"code": "SER_RESTART_CALLED"}) - - -class CreateUser(ApiHandler): - def post(self): - user = self.authenticate_user() - user_obj = self.controller.users.get_user_by_api_token(self.api_token) - - user_perms = self.controller.crafty_perms.get_crafty_permissions_list( - user_obj["user_id"] - ) - if ( - not self.permissions["User_Config"] in user_perms - and not user_obj["superuser"] - ): - self.access_denied("unknown") - return - - if user is None: - self.access_denied("unknown") - return - - if not self.permissions[ - "User_Config" - ] in self.controller.crafty_perms.get_api_key_permissions_list( - self.controller.users.get_api_key_by_token(self.api_token) - ): - self.access_denied(user) - return - - new_username = self.get_argument("username").lower() - new_pass = self.get_argument("password") - manager = int(user_obj["user_id"]) - - if new_username: - self.controller.users.add_user( - new_username, manager, new_pass, "default@example.com", True, False - ) - - self.return_response( - 200, - { - "code": "COMPLETE", - "username": new_username, - "password": new_pass, - }, - ) - else: - self.return_response( - 500, - { - "error": "MISSING_PARAMS", - "info": "Some paramaters failed validation", - }, - ) - - -class DeleteUser(ApiHandler): - def post(self): - user = self.authenticate_user() - - user_obj = self.controller.users.get_user_by_api_token(self.api_token) - - user_perms = self.controller.crafty_perms.get_crafty_permissions_list( - user_obj["user_id"] - ) - - if ( - not self.permissions["User_Config"] in user_perms - and not user_obj["superuser"] - ): - self.access_denied("unknown") - return - - if user is None: - self.access_denied("unknown") - return - - if not self.permissions[ - "User_Config" - ] in self.controller.crafty_perms.get_api_key_permissions_list( - self.controller.users.get_api_key_by_token(self.api_token) - ): - self.access_denied(user) - return - - user_id = self.get_argument("user_id", None, True) - user_to_del = self.controller.users.get_user_by_id(user_id) - - if user_to_del["superuser"]: - self.return_response( - 500, - {"error": "NOT_ALLOWED", "info": "You cannot delete a super user"}, - ) - else: - if user_id: - self.controller.users.remove_user(user_id) - self.return_response(200, {"code": "COMPLETED"}) - - -class ListServers(ApiHandler): - def get(self): - user = self.authenticate_user() - user_obj = self.controller.users.get_user_by_api_token(self.api_token) - - if user is None: - self.access_denied("unknown") - return - - if self.api_token is None: - self.access_denied("unknown") - return - - if user_obj["superuser"]: - servers = self.controller.servers.get_all_defined_servers() - servers = [str(i) for i in servers] - else: - servers = self.controller.servers.get_authorized_servers( - user_obj["user_id"] - ) - page_servers = [] - for server in servers: - if server not in page_servers: - page_servers.append( - DatabaseShortcuts.get_data_obj(server.server_object) - ) - servers = page_servers - servers = [str(i) for i in servers] - - self.return_response( - 200, - { - "code": "COMPLETED", - "servers": servers, - }, - ) diff --git a/app/classes/web/base_handler.py b/app/classes/web/base_handler.py index 2504bc13..d8181b94 100644 --- a/app/classes/web/base_handler.py +++ b/app/classes/web/base_handler.py @@ -14,6 +14,7 @@ from app.classes.shared.translation import Translation from app.classes.shared.main_models import DatabaseShortcuts logger = logging.getLogger(__name__) +auth_log = logging.getLogger("auth") bearer_pattern = re.compile(r"^Bearer ", flags=re.IGNORECASE) @@ -231,9 +232,16 @@ class BaseHandler(tornado.web.RequestHandler): user, ) logging.debug("Auth unsuccessful") + auth_log.error( + f"Authentication attempted from {self.get_remote_ip()}. Invalid token" + ) self.access_denied(None, "the user provided an invalid token") return None except Exception as auth_exception: + auth_log.error( + f"Authentication attempted from {self.get_remote_ip()}." + f" Error: {auth_exception}" + ) logger.debug( "An error occured while authenticating an API user:", exc_info=auth_exception, diff --git a/app/classes/web/panel_handler.py b/app/classes/web/panel_handler.py index e1d21f03..8ac827c3 100644 --- a/app/classes/web/panel_handler.py +++ b/app/classes/web/panel_handler.py @@ -210,6 +210,8 @@ class PanelHandler(BaseHandler): error = self.get_argument("error", "WTF Error!") template = "panel/denied.html" + if self.helper.crafty_starting: + page = "loading" now = time.time() formatted_time = str( @@ -243,9 +245,13 @@ class PanelHandler(BaseHandler): for r in exec_user["roles"]: role = self.controller.roles.get_role(r) exec_user_role.add(role["role_name"]) - defined_servers = self.controller.servers.get_authorized_servers( - exec_user["user_id"] - ) + # get_auth_servers will throw an exception if run while Crafty is starting + if not self.helper.crafty_starting: + defined_servers = self.controller.servers.get_authorized_servers( + exec_user["user_id"] + ) + else: + defined_servers = [] user_order = self.controller.users.get_user_by_id(exec_user["user_id"]) user_order = user_order["server_order"].split(",") @@ -481,6 +487,12 @@ class PanelHandler(BaseHandler): subpage = nh3.clean(self.get_argument("subpage", "")) server_id = self.check_server_id() + # load page the user was on last + server_subpage = self.controller.servers.server_subpage.get(server_id, "") + if subpage == "" and server_subpage != "": + subpage = self.controller.servers.server_subpage.get(server_id, "") + else: + self.controller.servers.server_subpage[server_id] = subpage if server_id is None: return if not self.failed_server: @@ -1609,7 +1621,8 @@ class PanelHandler(BaseHandler): logs_thread.start() self.redirect("/panel/dashboard") return - + if self.helper.crafty_starting: + template = "panel/loading.html" self.render( template, data=page_data, diff --git a/app/classes/web/public_handler.py b/app/classes/web/public_handler.py index b7d1be9b..57e6ddd8 100644 --- a/app/classes/web/public_handler.py +++ b/app/classes/web/public_handler.py @@ -6,6 +6,7 @@ from app.classes.models.users import HelperUsers from app.classes.web.base_handler import BaseHandler logger = logging.getLogger(__name__) +auth_log = logging.getLogger("auth") class PublicHandler(BaseHandler): @@ -96,18 +97,27 @@ class PublicHandler(BaseHandler): page_data["query"] = self.request.query if page == "login": + auth_log.info( + f"User attempting to authenticate from {self.get_remote_ip()}" + ) next_page = "/login" if self.request.query: next_page = "/login?" + self.request.query entered_username = nh3.clean(self.get_argument("username")) - entered_password = nh3.clean(self.get_argument("password")) + entered_password = self.get_argument("password") # pylint: disable=no-member try: user_id = HelperUsers.get_user_id_by_name(entered_username.lower()) user_data = HelperUsers.get_user_model(user_id) except: + self.controller.log_attempt(self.get_remote_ip(), entered_username) + auth_log.error( + f"User attempted to log into {entered_username}." + f" Authentication failed from remote IP {self.get_remote_ip()}" + " Users does not exist." + ) error_msg = "Incorrect username or password. Please try again." # self.clear_cookie("user") # self.clear_cookie("user_data") @@ -120,6 +130,12 @@ class PublicHandler(BaseHandler): # if we don't have a user if not user_data: + auth_log.error( + f"User attempted to log into {entered_username}. Authentication" + f" failed from remote IP {self.get_remote_ip()}" + " User does not exist." + ) + self.controller.log_attempt(self.get_remote_ip(), entered_username) error_msg = "Incorrect username or password. Please try again." # self.clear_cookie("user") # self.clear_cookie("user_data") @@ -132,6 +148,12 @@ class PublicHandler(BaseHandler): # if they are disabled if not user_data.enabled: + auth_log.error( + f"User attempted to log into {entered_username}. " + f"Authentication failed from remote IP {self.get_remote_ip()}." + " User account disabled" + ) + self.controller.log_attempt(self.get_remote_ip(), entered_username) error_msg = ( "User account disabled. Please contact " "your system administrator for more info." @@ -159,7 +181,11 @@ class PublicHandler(BaseHandler): user_data.last_ip = self.get_remote_ip() user_data.last_login = Helpers.get_time_as_string() user_data.save() - + auth_log.info( + f"{entered_username} successfully" + " authenticated and logged" + f" into panel from remote IP {self.get_remote_ip()}" + ) # log this login self.controller.management.add_to_audit_log( user_data.user_id, "Logged in", 0, self.get_remote_ip() @@ -172,6 +198,11 @@ class PublicHandler(BaseHandler): self.redirect(next_page) else: + auth_log.error( + f"User attempted to log into {entered_username}." + f" Authentication failed from remote IP {self.get_remote_ip()}" + ) + self.controller.log_attempt(self.get_remote_ip(), entered_username) # self.clear_cookie("user") # self.clear_cookie("user_data") self.clear_cookie("token") diff --git a/app/classes/web/routes/api/auth/login.py b/app/classes/web/routes/api/auth/login.py index 84ae2815..b91b295d 100644 --- a/app/classes/web/routes/api/auth/login.py +++ b/app/classes/web/routes/api/auth/login.py @@ -7,7 +7,7 @@ from app.classes.shared.helpers import Helpers from app.classes.web.base_api_handler import BaseApiHandler logger = logging.getLogger(__name__) - +auth_log = logging.getLogger("auth") login_schema = { "type": "object", "properties": { @@ -29,6 +29,10 @@ class ApiAuthLoginHandler(BaseApiHandler): try: data = json.loads(self.request.body) except json.decoder.JSONDecodeError as e: + logger.error( + "Invalid JSON schema for API" + f" login attempt from {self.get_remote_ip()}" + ) return self.finish_json( 400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)} ) @@ -36,6 +40,10 @@ class ApiAuthLoginHandler(BaseApiHandler): try: validate(data, login_schema) except ValidationError as e: + logger.error( + "Invalid JSON schema for API" + f" login attempt from {self.get_remote_ip()}" + ) return self.finish_json( 400, { @@ -52,12 +60,23 @@ class ApiAuthLoginHandler(BaseApiHandler): user_data = Users.get_or_none(Users.username == username) if user_data is None: + self.controller.log_attempt(self.get_remote_ip(), username) + auth_log.error( + f"User attempted to log into {username}." + " Authentication failed from remote IP" + f" {self.get_remote_ip()}. User not found" + ) return self.finish_json( 401, {"status": "error", "error": "INCORRECT_CREDENTIALS", "token": None}, ) if not user_data.enabled: + auth_log.error( + f"User attempted to log into {username}." + " Authentication failed from remote" + f" IP {self.get_remote_ip()} account disabled" + ) self.finish_json( 403, {"status": "error", "error": "ACCOUNT_DISABLED", "token": None} ) @@ -67,6 +86,11 @@ class ApiAuthLoginHandler(BaseApiHandler): # Valid Login if login_result: + auth_log.info( + f"{username} successfully" + " authenticated and logged" + f" into panel from remote IP {self.get_remote_ip()}" + ) logger.info(f"User: {user_data} Logged in from IP: {self.get_remote_ip()}") # record this login diff --git a/app/classes/web/routes/api/crafty/imports/index.py b/app/classes/web/routes/api/crafty/imports/index.py index e6c8c548..2aca2fa9 100644 --- a/app/classes/web/routes/api/crafty/imports/index.py +++ b/app/classes/web/routes/api/crafty/imports/index.py @@ -7,6 +7,7 @@ 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 +from app.classes.web.websocket_handler import WebSocketManager logger = logging.getLogger(__name__) files_get_schema = { @@ -73,7 +74,7 @@ class ApiImportFilesIndexHandler(BaseApiHandler): else: if user_id: user_lang = self.controller.users.get_user_lang_by_id(user_id) - self.helper.websocket_helper.broadcast_user( + WebSocketManager().broadcast_user( user_id, "send_start_error", { @@ -85,7 +86,7 @@ class ApiImportFilesIndexHandler(BaseApiHandler): 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( + WebSocketManager().broadcast_user( user_id, "send_start_error", { diff --git a/app/classes/web/routes/api/roles/index.py b/app/classes/web/routes/api/roles/index.py index 0d46e11b..b0c773a7 100644 --- a/app/classes/web/routes/api/roles/index.py +++ b/app/classes/web/routes/api/roles/index.py @@ -110,7 +110,7 @@ class ApiRolesIndexHandler(BaseApiHandler): try: data = orjson.loads(self.request.body) - except orjson.decoder.JSONDecodeError as e: + except orjson.JSONDecodeError as e: return self.finish_json( 400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)} ) 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 index b92e1e9f..9a4ecc30 100644 --- a/app/classes/web/routes/api/servers/server/backups/backup/index.py +++ b/app/classes/web/routes/api/servers/server/backups/backup/index.py @@ -72,9 +72,9 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler): FileHelpers.del_file( os.path.join(backup_conf["backup_path"], data["filename"]) ) - except Exception: + except Exception as e: return self.finish_json( - 400, {"status": "error", "error": "NO BACKUP FOUND"} + 400, {"status": "error", "error": f"DELETE FAILED with error {e}"} ) self.controller.management.add_to_audit_log( auth_data[4]["user_id"], diff --git a/app/classes/web/routes/api/users/user/index.py b/app/classes/web/routes/api/users/user/index.py index d416e800..1b7f6f91 100644 --- a/app/classes/web/routes/api/users/user/index.py +++ b/app/classes/web/routes/api/users/user/index.py @@ -215,7 +215,7 @@ class ApiUsersUserIndexHandler(BaseApiHandler): 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): + if str(user["user_id"]) != str(user_obj.manager) and not user["superuser"]: # TODO: edit your own password return self.finish_json( 400, {"status": "error", "error": "INVALID_PASSWORD_MODIFY"} diff --git a/app/classes/web/tornado_handler.py b/app/classes/web/tornado_handler.py index 621c930a..f5501d31 100644 --- a/app/classes/web/tornado_handler.py +++ b/app/classes/web/tornado_handler.py @@ -22,18 +22,6 @@ from app.classes.web.default_handler import DefaultHandler from app.classes.web.routes.api.api_handlers import api_handlers from app.classes.web.routes.metrics.metrics_handlers import metrics_handlers from app.classes.web.server_handler import ServerHandler -from app.classes.web.api_handler import ( - ServersStats, - NodeStats, - ServerBackup, - StartServer, - StopServer, - RestartServer, - CreateUser, - DeleteUser, - ListServers, - SendCommand, -) from app.classes.web.websocket_handler import WebSocketHandler from app.classes.web.static_handler import CustomStaticHandler from app.classes.web.upload_handler import UploadHandler @@ -162,17 +150,6 @@ class Webserver: (r"/ws", WebSocketHandler, handler_args), (r"/upload", UploadHandler, handler_args), (r"/status", StatusHandler, handler_args), - # API Routes V1 - (r"/api/v1/stats/servers", ServersStats, handler_args), - (r"/api/v1/stats/node", NodeStats, handler_args), - (r"/api/v1/server/send_command", SendCommand, handler_args), - (r"/api/v1/server/backup", ServerBackup, handler_args), - (r"/api/v1/server/start", StartServer, handler_args), - (r"/api/v1/server/stop", StopServer, handler_args), - (r"/api/v1/server/restart", RestartServer, handler_args), - (r"/api/v1/list_servers", ListServers, handler_args), - (r"/api/v1/users/create_user", CreateUser, handler_args), - (r"/api/v1/users/delete_user", DeleteUser, handler_args), # API Routes V2 *api_handlers(handler_args), # API Routes OpenMetrics diff --git a/app/config/logging.json b/app/config/logging.json index 99de60e4..fd1173eb 100644 --- a/app/config/logging.json +++ b/app/config/logging.json @@ -10,16 +10,20 @@ }, "schedule": { "format": "%(asctime)s - [Schedules] - %(levelname)s - %(message)s" + }, + "auth": { + "format": "%(asctime)s - [AUTH] - %(levelname)s - %(message)s" + }, + "cmd_queue": { + "format": "%(asctime)s - [CMD_QUEUE] - %(levelname)s - %(message)s" } }, - "handlers": { "console": { "class": "logging.StreamHandler", "formatter": "commander", "stream": "ext://sys.stdout" }, - "main_file_handler": { "class": "logging.handlers.RotatingFileHandler", "formatter": "commander", @@ -50,24 +54,60 @@ "maxBytes": 10485760, "backupCount": 20, "encoding": "utf8" + }, + "auth_file_handler": { + "class": "logging.handlers.RotatingFileHandler", + "formatter": "auth", + "filename": "logs/auth.log", + "maxBytes": 10485760, + "backupCount": 20, + "encoding": "utf8" + }, + "cmd_queue_file_handler": { + "class": "logging.handlers.RotatingFileHandler", + "formatter": "auth", + "filename": "logs/cmd_queue.log", + "maxBytes": 10485760, + "backupCount": 20, + "encoding": "utf8" } }, - "loggers": { "": { "level": "INFO", - "handlers": ["main_file_handler", "session_file_handler"], + "handlers": [ + "main_file_handler", + "session_file_handler" + ], "propagate": false }, "tornado.access": { "level": "INFO", - "handlers": ["tornado_access_file_handler"], + "handlers": [ + "tornado_access_file_handler" + ], "propagate": false }, "apscheduler": { "level": "INFO", - "handlers": ["schedule_file_handler"], + "handlers": [ + "schedule_file_handler" + ], + "propagate": false + }, + "auth": { + "level": "INFO", + "handlers": [ + "auth_file_handler" + ], + "propagate": false + }, + "cmd_queue": { + "level": "INFO", + "handlers": [ + "cmd_queue_file_handler" + ], "propagate": false } } -} +} \ No newline at end of file diff --git a/app/config/version.json b/app/config/version.json index 47d57814..e629f1ba 100644 --- a/app/config/version.json +++ b/app/config/version.json @@ -1,5 +1,5 @@ { "major": 4, "minor": 2, - "sub": 1 + "sub": 2 } diff --git a/app/frontend/static/assets/images/Crafty_4-0.png b/app/frontend/static/assets/images/Crafty_4-0.png index d873eedc..0671d421 100644 Binary files a/app/frontend/static/assets/images/Crafty_4-0.png and b/app/frontend/static/assets/images/Crafty_4-0.png differ diff --git a/app/frontend/static/assets/images/Crafty_4-0_Logo_square.ico b/app/frontend/static/assets/images/Crafty_4-0_Logo_square.ico index 68095bf6..7b354581 100644 Binary files a/app/frontend/static/assets/images/Crafty_4-0_Logo_square.ico and b/app/frontend/static/assets/images/Crafty_4-0_Logo_square.ico differ diff --git a/app/frontend/static/assets/images/crafty-logo-square-1024.png b/app/frontend/static/assets/images/crafty-logo-square-1024.png index 65dd0671..cec34236 100644 Binary files a/app/frontend/static/assets/images/crafty-logo-square-1024.png and b/app/frontend/static/assets/images/crafty-logo-square-1024.png differ diff --git a/app/frontend/static/assets/images/crafty-logo-square-96.png b/app/frontend/static/assets/images/crafty-logo-square-96.png index 6928e039..8c3a34b2 100644 Binary files a/app/frontend/static/assets/images/crafty-logo-square-96.png and b/app/frontend/static/assets/images/crafty-logo-square-96.png differ diff --git a/app/frontend/static/assets/images/crafty-logo-square.svg b/app/frontend/static/assets/images/crafty-logo-square.svg new file mode 100644 index 00000000..c0a686d3 --- /dev/null +++ b/app/frontend/static/assets/images/crafty-logo-square.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/frontend/templates/base.html b/app/frontend/templates/base.html index 40ee757b..48c6ee95 100755 --- a/app/frontend/templates/base.html +++ b/app/frontend/templates/base.html @@ -82,6 +82,9 @@ +     + + {% include notify.html %} @@ -381,18 +384,21 @@ if (x) { x.remove() } - bootbox.alert({ + bootbox.confirm({ title: "{{ translate('notify', 'downloadLogs', data['lang']) }}", message: "{{ translate('notify', 'finishedPreparing', data['lang']) }}", buttons: { - ok: { + confirm: { label: 'Download', className: 'btn-info' } }, - callback: function () { - console.log("in callback") - location.href = "/panel/download_support_package"; + callback: function (result) { + if (result){ + location.href = "/panel/download_support_package"; + } else { + bootbox.close(); + } } }); }); @@ -602,4 +608,4 @@ - \ No newline at end of file + diff --git a/app/frontend/templates/panel/config_json.html b/app/frontend/templates/panel/config_json.html index 5b1f4df8..5736b907 100644 --- a/app/frontend/templates/panel/config_json.html +++ b/app/frontend/templates/panel/config_json.html @@ -170,7 +170,7 @@ {% block js %} +{% end %} diff --git a/app/frontend/templates/panel/parts/details_stats.html b/app/frontend/templates/panel/parts/details_stats.html index 47fa501d..ad190bc3 100644 --- a/app/frontend/templates/panel/parts/details_stats.html +++ b/app/frontend/templates/panel/parts/details_stats.html @@ -248,12 +248,38 @@ $("#player-body").html(text); } - + //used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security + function getCookie(name) { + var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); + return r ? r[1] : undefined; + } + const token = getCookie("_xsrf") $(window).ready(function () { console.log("ready!"); //if (webSocket) { webSocket.on('update_server_details', update_server_details); + add_server_name(); //} }); + async function add_server_name(){ + let res = await fetch(`/api/v2/servers/${serverId}`, { + method: 'GET', + headers: { + 'X-XSRFToken': token + }, + }); + let responseData = await res.json(); + if (responseData.status === "ok") { + console.log(responseData) + $("#server-name-nav").html(`${responseData.data['server_name']}`) + $("#server-name-nav").show(); + } else { + + bootbox.alert({ + title: responseData.error, + message: responseData.error_data + }); + } + } \ No newline at end of file diff --git a/app/frontend/templates/panel/parts/server_players.html b/app/frontend/templates/panel/parts/server_players.html index df059e95..ca81f39e 100644 --- a/app/frontend/templates/panel/parts/server_players.html +++ b/app/frontend/templates/panel/parts/server_players.html @@ -69,7 +69,7 @@ Banned on {{ player['banned_on'] }} Banned by : {{ player['source'] }}
Reason : {{ player['reason'] }} - + {% end %} diff --git a/app/frontend/templates/panel/server_backup.html b/app/frontend/templates/panel/server_backup.html index d79e710e..3606d4af 100644 --- a/app/frontend/templates/panel/server_backup.html +++ b/app/frontend/templates/panel/server_backup.html @@ -428,6 +428,7 @@ if ($("#root_files_button").hasClass("clicked")){ formDataObject.exclusions = excluded; } + delete formDataObject.root_path console.log(excluded); console.log(formDataObject); // Format the plain form data as JSON @@ -650,10 +651,10 @@ let checked = "" let dpath = value.path; let filename = key; + if (value.excluded){ + checked = "checked" + } if (value.dir){ - if (value.excluded){ - checked = "checked" - } text += `
  • \n
    diff --git a/app/frontend/templates/panel/server_webhook_edit.html b/app/frontend/templates/panel/server_webhook_edit.html index e7087771..fd556833 100644 --- a/app/frontend/templates/panel/server_webhook_edit.html +++ b/app/frontend/templates/panel/server_webhook_edit.html @@ -43,8 +43,10 @@
    {% end %} - + {% if data['new_webhook'] == False %} + {% end %} {% for type in data['providers'] %} {% if type != data['webhook']['webhook_type'] %} diff --git a/app/translations/de_DE.json b/app/translations/de_DE.json index ac18990d..11311bb0 100644 --- a/app/translations/de_DE.json +++ b/app/translations/de_DE.json @@ -111,6 +111,7 @@ "starting": "Verzögerter Start", "status": "Status", "stop": "Stoppen", + "storage": "Speicher", "version": "Version", "welcome": "Willkommen bei Crafty Controller" }, @@ -591,6 +592,15 @@ "newServer": "Neuen Server erstellen", "servers": "Server" }, + "startup": { + "almost": "Nur noch einen Moment, fast geschafft", + "internals": "Crafty's interne Komponneten initialisieren und starten", + "internet": "Verbindung zum Internet überprüfen", + "server": "initialisieren ", + "serverInit": "Server initialisieren", + "starting": "Crafty startet...", + "tasks": "Zeitplan-Aufgaben werden geladen" + }, "userConfig": { "apiKey": "API Schlüssel", "auth": "Autorisiert? ", diff --git a/app/translations/en_EN.json b/app/translations/en_EN.json index 6db96dfc..4db8be71 100644 --- a/app/translations/en_EN.json +++ b/app/translations/en_EN.json @@ -111,6 +111,7 @@ "starting": "Delayed-Start", "status": "Status", "stop": "Stop", + "storage": "Storage", "version": "Version", "welcome": "Welcome to Crafty Controller" }, @@ -590,6 +591,15 @@ "newServer": "Create New Server", "servers": "Servers" }, + "startup": { + "almost": "Finishing up. Hang on tight...", + "internals": "Configuring and starting Crafty's internal components", + "internet": "Checking for internet connection", + "server": "Initializing ", + "serverInit": "Initializing Servers", + "starting": "Crafty Is Starting...", + "tasks": "Starting Tasks Scheduler" + }, "userConfig": { "apiKey": "API Keys", "auth": "Authorized? ", diff --git a/app/translations/es_ES.json b/app/translations/es_ES_incomplete.json similarity index 100% rename from app/translations/es_ES.json rename to app/translations/es_ES_incomplete.json diff --git a/app/translations/fr_FR.json b/app/translations/fr_FR.json index 33ea5225..112de289 100644 --- a/app/translations/fr_FR.json +++ b/app/translations/fr_FR.json @@ -111,6 +111,7 @@ "starting": "Démarrage retardé", "status": "Statut", "stop": "Arrêter", + "storage": "Stockage", "version": "Version", "welcome": "Bienvenue sur Crafty Controller" }, @@ -591,6 +592,15 @@ "newServer": "Créer un Nouveau Serveur", "servers": "Serveurs" }, + "startup": { + "almost": "Finalisation. Patienter ...", + "internals": "Configuration et Démarrage des composants internes de Crafty", + "internet": "Vérification de la connexion à Internet", + "server": "Initialisation ", + "serverInit": "Initialisation des Serveurs", + "starting": "Crafty Démarre ...", + "tasks": "Démarrage du planificateur de tâches" + }, "userConfig": { "apiKey": "Clés API", "auth": "Authorisé ? ", diff --git a/app/translations/he_IL_incomplete.json b/app/translations/he_IL.json similarity index 81% rename from app/translations/he_IL_incomplete.json rename to app/translations/he_IL.json index 2c8e0139..73e7032a 100644 --- a/app/translations/he_IL_incomplete.json +++ b/app/translations/he_IL.json @@ -53,6 +53,20 @@ "translationTitle": "שפת התרגום", "translator": "מתרגמים" }, + "customLogin": { + "apply": "החל", + "backgroundUpload": "העלאת רקע", + "customLoginPage": "התאמת דף הכניסה", + "delete": "מחק", + "labelLoginImage": "בחר את רקע כניסתך", + "loginBackground": "תמונת רקע לכניסה", + "loginImage": "העלה תמונת רקע למסך הכניסה.", + "loginOpacity": "בחר את שקיפות חלון הכניסה", + "pageTitle": "דף כניסה מותאם אישית", + "preview": "תצוגה מקדימה", + "select": "בחר", + "selectImage": "בחר תמונה" + }, "dashboard": { "actions": "פעולות", "allServers": "כל השרתים", @@ -75,6 +89,7 @@ "dashboard": "פאנל", "delay-explained": "השירות/סוכן התחיל לאחרונה והוא מעכב את הדלקת שרת המיינקראפט", "host": "אחסון", + "installing": "מתקין...", "kill": "כיבוי מידי", "killing": "...מכבה מידית", "lastBackup": "אחרון:", @@ -96,6 +111,7 @@ "starting": "התחלה בעיכוב", "status": "סטאטוס", "stop": "עצור", + "storage": "שטח אחסון", "version": "גרסה", "welcome": "ברוכים הבאים ל-פאנל קראפטי" }, @@ -164,20 +180,33 @@ } }, "error": { + "agree": "מסכים", + "bedrockError": "הורדות Bedrock אינן זמינות. אנא בדוק", + "cancel": "בטל", "contact": "בבקשה צרו קשר עם תמיכת פאנל קראפטי באמצעות דיסקורד", + "craftyStatus": "דף המצב של Crafty", + "cronFormat": "זוהה פורמט Cron לא תקין", "embarassing": "אוי, טוב, זה מביך.", "error": "שגיאה!", "eulaAgree": "אתם מסכימים?", "eulaMsg": "עליכם להסכים להסכם הרישיון למשתמש הקצה. עותק של הסכם הרישיון למשתמש הקצה של מוג'אנג מקושר תחת הודעה זו.", "eulaTitle": "להסכים להסכם רישיון משתמש קצה של מוג'אנג", + "fileError": "סוג הקובץ חייב להיות תמונה.", "fileTooLarge": "העלאה נכשלה. העלאת הקובץ גדולה מדי. פנה למנהל המערכת לקבלת סיוע.", "hereIsTheError": "הנה השגיאה", + "installerJava": "נכשל בהתקנת {} : התקנות של שרת Forge דורשות Java. זוהה ש-Java אינו מותקן. אנא התקן את Java ואז התקן את השרת.", "internet": "גילינו שלמכונה(מחשב) שמריצה את קראפטי אין חיבור לאינטרנט. חיבורי לקוחות לשרת עשויים להיות מוגבלים.", + "migration": "אחסון השרת הראשי של Crafty מועבר למיקום חדש. כל הפעלות השרתים נעצרו במהלך זמן זה. אנא המתן בזמן שאנו מסיימים את המעבר הזה", "no-file": "נראה שאיננו מצליחים לאתר את הקובץ המבוקש. בדוק שוב את הנתיב. האם ל-קראפטי יש הרשאות מתאימות?", + "noInternet": "Crafty נתקל בבעיות גישה לאינטרנט. יצירת שרתים הושבתה. אנא בדוק את חיבור האינטרנט שלך ורענן את הדף הזה.", "noJava": "השרת {} לא הצליח להתחיל עם קוד השגיאה: גילינו ש-Java אינו מותקן. אנא התקינו את Java ואז הפעילו את השרת.", "not-downloaded": "לא הצלחנו למצוא את קובץ ההפעלה שלך. האם זה סיים להוריד? האם ההרשאות מוגדרות בשביל הפעלה?", "portReminder": "זיהינו שזו הפעם הראשונה ש-{} מופעל. הקפידו להעביר את היציאה {} דרך הנתב/חומת האש שלכם כדי להפוך אותה לנגישה מרחוק מהאינטרנט.", + "privMsg": "וה", + "serverJars1": "API של צנצנות השרת אינו נגיש. אנא בדוק", + "serverJars2": "למידע מעודכן ביותר.", "start-error": "השרת {} לא הצליח להתחיל עם קוד שגיאה: {}", + "superError": "חובה להיות משתמש על כדי לבצע פעולה זו.", "terribleFailure": "איזה כישלון נורא!" }, "footer": { @@ -189,7 +218,8 @@ "forgotPassword": "שכחתי סיסמה", "login": "התחברות", "password": "סיסמה", - "username": "שם משתמש" + "username": "שם משתמש", + "viewStatus": "צפה בדף המצב הציבורי" }, "notify": { "activityLog": "יומני פעילות", @@ -201,24 +231,38 @@ "preparingLogs": "אנא המתינו בזמן שאנו מכינים את היומנים שלכם... נשלח הודעה כשהם יהיו מוכנים. זה עשוי להימשך זמן מה לפריסות שרתים גדולות.", "supportLogs": "יומני תמיכה" }, + "offline": { + "offline": "מנותק", + "pleaseConnect": "אנא חבר לאינטרנט כדי להשתמש ב-Crafty." + }, "panelConfig": { "adminControls": "בקרות מנהל", "allowedServers": "שרתים מורשים", + "apply": "החל", "assignedRoles": "תפקידים שהוקצו", "cancel": "ביטול", "clearComms": "ניקוי פקודות שלא בוצעו", + "custom": "התאמת Crafty", "delete": "מחיקה", "edit": "עריכה", + "enableLang": "אפשר כל השפות", "enabled": "מופעל", + "globalExplain": "היכן Crafty שומר את כל קבצי השרת שלך. (נוסיף את הנתיב עם /servers/[uuid של השרת])", + "globalServer": "תיקיית שרתים גלובלית", + "json": "Config.json", + "match": "הסיסמאות חייבות להתאים", "newRole": "הוספת תפקיד חדש", "newUser": "הוספת משתמש חדש", + "noMounts": "אל תציג נקודות עגינה בלוח המחוונים", "pageTitle": "הגדרת פאנל", "role": "תפקיד", "roleUsers": "תפקידי משתמשים", "roles": "תפקידים", "save": "שמירה", + "select": "בחר", "superConfirm": "המשיכו רק אם אתם רוצים שלמשתמש זה תהיה גישה להכל (כל חשבונות המשתמש, השרתים, הגדרות הפאנל וכו'). הם יכולים אפילו למחוק את זכויות משתמש העל שלך.", "superConfirmTitle": "להפעיל משתמש-על? האם אתם בטוחים?", + "title": "תצורת Crafty", "user": "משתמש", "users": "משתמשים" }, @@ -242,14 +286,17 @@ "roleTitle": "הגדרות תפקידים", "roleUserName": "שם משתמש", "roleUsers": "תפקידי המשתמשים: ", + "selectManager": "בחר מנהל לתפקיד זה", "serverAccess": "?גישה", "serverName": "שם שרת", "serversDesc": "לשרתים מותר לגשת לתפקיד זה" }, "serverBackups": { + "after": "הרץ פקודה לאחר הגיבוי", "backupAtMidnight": "גיבוי אוטומטי בחצות?", "backupNow": "!גיבוי עכשיו", "backupTask": "החלה משימת גיבוי.", + "before": "הרץ פקודה לפני הגיבוי", "cancel": "לבטל", "clickExclude": "לחצו כדי לבחור מה לא יהיה בגיבוי", "compress": "דחוס גיבוי", @@ -289,6 +336,8 @@ "deleteServerQuestionMessage": "האם אתם בטוחים שברצונכם למחוק את השרת הזה? אחרי זה אין דרך חזרה...", "exeUpdateURL": "כתובת ה-URL של עדכון השרת הניתן להפעלה", "exeUpdateURLDesc": "כתובת אתר להורדה ישירה לקבלת עדכונים.", + "ignoredExits": "קודי יציאה של קריסה שלא ישמעו", + "ignoredExitsExplain": "קודי יציאה שגילוי הקריסות של Crafty צריך להתעלם מהם כמו 'עצירה' רגילה (מופרדים בפסיקים)", "javaNoChange": "לא למחוק", "javaVersion": "כן למחוק את גרסאת ה-Java המותקנת כרגע", "javaVersionDesc": "אם אתה מתכוון לעקוף את Java, ודא שנתיב ה-Java הנוכחי שלך ב'פקודה לביצוע' עטוף במרכאות (משתנה ברירת המחדל 'java' לא נכלל)", @@ -319,7 +368,13 @@ "serverPortDesc": "קראפטי צריך פורט בשביל להתחבר לנתונים סטטיסטיים", "serverStopCommand": "פקודת עצירת שרת", "serverStopCommandDesc": "פקודה לשלוח את התוכנית כדי לעצור אותה", + "showStatus": "הצג בדף המצב הציבורי", + "shutdownTimeout": "זמן קצוב לכיבוי", + "statsHint1": "הפורט שבו השרת שלך פועל צריך להיות כאן. זה רק איך Crafty פותח חיבור לשרת שלך לצורך סטטיסטיקות.", + "statsHint2": "זה לא משנה את פורט השרת שלך. עדיין צריך לשנות את הפורט בקובץ התצורה של השרת שלך.", "stopBeforeDeleting": "בבקשה לעצור את השרת לפני מחיקתו", + "timeoutExplain1": "כמה זמן Crafty יחכה לשרת שלך להיכבות לאחר ביצוע ה", + "timeoutExplain2": "פקודה לפני שהוא יכריח את התהליך לרדת.", "update": "עדכנו את קובץ ההפעלה", "yesDelete": "כן, למחוק", "yesDeleteFiles": "כן, מחק קבצים" @@ -345,8 +400,12 @@ "backup": "גיבוי", "config": "הגדרות", "files": "קבצים", + "filter": "סינון יומנים", + "filterList": "מילים מסוננות", "logs": "לוג", + "metrics": "מדדים", "playerControls": "ניהול שחקנים", + "reset": "אפס גלילה", "schedule": "לוח זמנים", "serverDetails": "פרטי שרת", "terminal": "מסוף פקודות" @@ -383,6 +442,11 @@ "waitUpload": "אנא המתן בזמן שאנו מעלים את הקבצים שלך... זה עשוי לקחת זמן מה.", "yesDelete": "כן, אני מבין.ה את ההשלכות" }, + "serverMetrics": { + "resetZoom": "אפס זום", + "zoomHint1": "כדי להגדיל על הגרף החזק את מקש ה-shift והשתמש בגלגלת הגלילה שלך.", + "zoomHint2": "חלופית, החזק את מקש ה-shift ולחץ וגרור את האזור שברצונך להגדיל עליו." + }, "serverPlayerManagement": { "bannedPlayers": "שחקנים בבאן", "loadingBannedPlayers": "טוען שחקנים שהם בבאן", @@ -410,18 +474,36 @@ "parent-explain": "איזה לוח זמנים צריך להפעיל את זה?", "reaction": "תגובה", "restart": "הפעלה מחדש", + "select": "בחר בסיסי / Cron / תגובת שרשרת", "start": "הדלקת שרת", "stop": "כיבוי שרת", "time": "זמן", "time-explain": "באיזו שעה אתה רוצה שהלוח שלך יתבצע?" }, "serverSchedules": { + "action": "פעולה", "areYouSure": "למחוק משימה מתוזמנת?", "cancel": "לבטל", "cannotSee": "לא רואים הכל?", "cannotSeeOnMobile": "נסה ללחוץ על משימה מתוזמנת לפרטים מלאים.", + "child": "ילד של לוח זמנים עם מזהה ", + "close": "סגור", + "command": "פקודה", "confirm": "אישור", - "confirmDelete": "האם ברצונך למחוק את המשימה המתוזמנת הזו? אי אפשר לבטל את זה." + "confirmDelete": "האם ברצונך למחוק את המשימה המתוזמנת הזו? אי אפשר לבטל את זה.", + "create": "צור לוח זמנים חדש", + "cron": "מחרוזת Cron", + "delete": "מחק", + "details": "פרטי לוח זמנים", + "edit": "ערוך", + "enabled": "מופעל", + "every": "כל", + "interval": "מרווח", + "name": "שם", + "nextRun": "הריצה הבאה", + "no": "לא", + "scheduledTasks": "משימות מתוזמנות", + "yes": "כן" }, "serverStats": { "cpuUsage": "שימוש במעבד", @@ -444,6 +526,8 @@ "commandInput": "הקלידו את הפקודה שלכם", "delay-explained": "השירות/סוכן התחיל לאחרונה והוא מעכב את הדלקת שרת המיינקראפט", "downloading": "...מוריד", + "importing": "מייבא...", + "installing": "מתקין...", "restart": "הפעלה מחדש", "sendCommand": "שליחת פקודה", "start": "התחלה", @@ -468,6 +552,7 @@ "importServerButton": "ייבוא שרת!", "importZip": "ייבוא מקובץ Zip", "importing": "מייבא שרת...", + "labelZipFile": "בחר את קובץ ה-Zip שלך", "maxMem": "מקסימום זיכרון", "minMem": "מינימום זיכרון", "myNewServer": "השרת החדש שלי", @@ -478,6 +563,7 @@ "save": "שמור", "selectRole": "בחר תפקידים", "selectRoot": "בחר ארכיון שורש Dir", + "selectServer": "בחר שרת", "selectType": "בחר סוג", "selectVersion": "בחר גרסה", "selectZipDir": "בחר את הספרייה בארכיון שממנו אתה רוצה שנפתח קבצים", @@ -485,9 +571,13 @@ "serverName": "שם השרת", "serverPath": "נתיב שרת", "serverPort": "פורט שרת", + "serverSelect": "בחירת שרת", "serverType": "סוג השרת", + "serverUpload": "העלה שרת מכווץ", "serverVersion": "גרסת השרת", "sizeInGB": "גודל ב-GB", + "uploadButton": "העלה", + "uploadZip": "העלה קובץ Zip לייבוא שרת", "zipPath": "נתיב שרת" }, "sidebar": { @@ -495,10 +585,20 @@ "credits": "קרדיט", "dashboard": "פאנל", "documentation": "ויקיפדייה", + "inApp": "מסמכים באפליקציה", "navigation": "ניווט", "newServer": "צור שרת חדש", "servers": "שרתים" }, + "startup": { + "almost": "מסיימים. תחזיקו חזק...", + "internals": "הגדרה והפעלה של הרכיבים הפנימיים של Crafty", + "internet": "בודק את חיבור האינטרנט", + "server": "אתחול ", + "serverInit": "מפעיל שרתים", + "starting": "מתחילים בהפעלת מערכת Crafty...", + "tasks": "מתזמן את מחולל המשימות" + }, "userConfig": { "apiKey": "API מפתחות", "auth": "מורשה? ", @@ -519,6 +619,7 @@ "lastLogin": "כניסה אחרונה: ", "lastUpdate": "עדכון אחרון: ", "leaveBlank": "כדי לערוך משתמש מבלי לשנות סיסמה השאר אותו ריק.", + "manager": "מנהל", "member": "חבר?", "notExist": "אתה לא יכול למחוק משהו שלא קיים!", "pageTitle": "ערוך משתמש", @@ -527,6 +628,7 @@ "permName": "שם הרשאה", "repeat": "חזור על הסיסמה", "roleName": "שם התפקיד", + "selectManager": "בחר מנהל למשתמש", "super": "משתמש על", "userLang": "שפת משתמש", "userName": "שם משתמש", @@ -534,6 +636,30 @@ "userRoles": "תפקידי משתמש", "userRolesDesc": "תפקידים שמשתמש זה חבר בהם", "userSettings": "הגדרות משתמש", + "userTheme": "ערכת נושא UI", "uses": "מספר השימושים המותרים (-1==ללא הגבלה)" + }, + "webhooks": { + "areYouSureDel": "האם אתה בטוח שברצונך למחוק את ה-Webhook הזה?", + "areYouSureRun": "האם אתה בטוח שברצונך לבדוק את ה-Webhook הזה?", + "backup_server": "גיבוי השרת הושלם", + "bot_name": "שם הבוט", + "color": "בחר גוון צבע", + "crash_detected": "השרת קרס", + "edit": "ערוך", + "enabled": "מופעל", + "jar_update": "השרת הביצועי עודכן", + "kill": "השרת נסגר", + "name": "שם", + "new": "Webhook חדש", + "run": "הרץ Webhook לבדיקה", + "send_command": "פקודת שרת התקבלה", + "start_server": "השרת הופעל", + "stop_server": "השרת נעצר", + "trigger": "גרם", + "type": "סוג ה-Webhook", + "url": "URL של ה-Webhook", + "webhook_body": "גוף ה-Webhook", + "webhooks": "Webhooks" } } diff --git a/app/translations/lol_EN.json b/app/translations/lol_EN.json index 8c2436cb..a2368a84 100644 --- a/app/translations/lol_EN.json +++ b/app/translations/lol_EN.json @@ -111,6 +111,7 @@ "starting": "I WAITZ B4 I START", "status": "STATUZ", "stop": "STAHP PLZ", + "storage": "STASH BOX", "version": "VERZHUN", "welcome": "WELCOM 2 CWAFTY CONTROLLR" }, @@ -591,6 +592,15 @@ "newServer": "CONSTWUCT A SERVR", "servers": "SERVRS" }, + "startup": { + "almost": "ALMOST DUN. HOLD ON TO YER WHISKERS...", + "internals": "SETTIN' UP AN' STARTIN' CWAFTY'S INSIDE BITZ", + "internet": "LOOKIN' FOR OUTER SPACE TALKY", + "server": "WAKIN' UPZ ", + "serverInit": "MAKIN' SERVERZ READY", + "starting": "CWAFTY WAKING UP...", + "tasks": "STARTIN' TASK SCHEDULERZ" + }, "userConfig": { "apiKey": "API KEYS", "auth": "PERMISHUN TO ACESS? ", diff --git a/app/translations/lv_LV.json b/app/translations/lv_LV.json index 15ecf869..95d2ffae 100644 --- a/app/translations/lv_LV.json +++ b/app/translations/lv_LV.json @@ -112,6 +112,7 @@ "starting": "Aizkavēts-Starts", "status": "Statuss", "stop": "Apturēt", + "storage": "Glabātuve", "version": "Versija", "welcome": "Esiet sveicināts Crafty Controller" }, @@ -592,6 +593,15 @@ "newServer": "Izveidot Jaunu Serveri", "servers": "Serveri" }, + "startup": { + "almost": "Pabeidz. Vēl tik nedaudz...", + "internals": "Konfigurē un Startē Crafty iekšējās komponenetes", + "internet": "Pārbauda interneta savienojumu", + "server": "Inicializē ", + "serverInit": "Inicializē Serverus", + "starting": "Crafty Startējas...", + "tasks": "Sāknē Notikumu Plānotāju" + }, "userConfig": { "apiKey": "API Atslēgas", "auth": "Authorizēts? ", diff --git a/app/translations/nl_BE.json b/app/translations/nl_BE.json index 81d455ed..bed0f894 100644 --- a/app/translations/nl_BE.json +++ b/app/translations/nl_BE.json @@ -111,6 +111,7 @@ "starting": "Vertraagde start", "status": "Toestand", "stop": "Stoppen", + "storage": "Opslagruimte", "version": "Versie", "welcome": "Welkom bij Crafty Controller " }, @@ -591,6 +592,15 @@ "newServer": "Nieuwe server maken", "servers": "Servers" }, + "startup": { + "almost": "De laatste hand leggen. Houd je vast...", + "internals": "Crafty's interne componenten configureren en starten", + "internet": "Controleren op internetverbinding", + "server": "Initialiseren ", + "serverInit": "Servers initialiseren", + "starting": "Crafty wordt gestart...", + "tasks": "Start takenplanner" + }, "userConfig": { "apiKey": "API Sleutels", "auth": "Bevoegd? ", diff --git a/app/translations/pl_PL.json b/app/translations/pl_PL.json index 828ec0b1..2b9ce56d 100644 --- a/app/translations/pl_PL.json +++ b/app/translations/pl_PL.json @@ -111,6 +111,7 @@ "starting": "Opóźniony-Start", "status": "Status", "stop": "Zatrzymaj", + "storage": "Przestrzeń dyskowa", "version": "Wersja", "welcome": "Witamy w Crafty Controller" }, @@ -326,8 +327,8 @@ "bePatientDeleteFiles": "Poczekaj, aż usuniemy twój serwer i jego pliki. Strona za chwilę się zamknie.", "bePatientUpdate": "Poczekaj kiedy my aktualizujemy twój serwer. Pobieranie zależy od prędkości twojego internetu.
    Strona się odświeży za chwile.", "cancel": "Anuluj", - "crashTime": "Crash wyszedł poza limit czasu", - "crashTimeDesc": "How long should we wait before we consider your server as crashed?", + "crashTime": "Crash serwera wyszedł poza limit czasu", + "crashTimeDesc": "Jak długo powinniśmy poczekać zanim uznać serwer za zcrashowany?", "deleteFilesQuestion": "Usuń pliki serwera z maszyny?", "deleteFilesQuestionMessage": "Czy chcesz aby Crafty usunął wszystkie pliki tego serwera?

    To zawiera backupy.", "deleteServer": "Usuń serwer", @@ -403,7 +404,7 @@ "filterList": "Filtrowane słowa", "logs": "Logi", "metrics": "Statystyki", - "playerControls": "Player Management", + "playerControls": "Zarządzanie użytkownikami", "reset": "Resetuj Scrolla", "schedule": "Harmonogram", "serverDetails": "Detale serwera", @@ -421,7 +422,7 @@ "deleteItemQuestion": "Czy jesteś pewien że chcesz usunąć \" + name + \"?", "deleteItemQuestionMessage": "Usuwasz \\\"\" + path + \"\\\"!

    Ta akcja jest nieodwracalna i zostanie usunięta na zawsze!", "download": "Pobierz", - "editingFile": "Edytuję plik", + "editingFile": "Edytuj plik", "error": "Error while getting files", "fileReadError": "Error odczytu pliku", "files": "Pliki", @@ -432,7 +433,7 @@ "rename": "Zmień nazwę", "renameItemQuestion": "Jaka ma być nowa nazwa?", "save": "Zapisz", - "size": "Włącz zmienianie rozmiaru edytora", + "size": "Włącz rozszerzanie i zmniejszanie edytora", "stayHere": "NIE WYCHODŹ Z TEJ STRONY!", "unsupportedLanguage": "Uwaga: To nie jest wspierany typ pliku", "unzip": "Rozpakuj", @@ -545,7 +546,7 @@ "buildServer": "Zbuduj serwer!", "clickRoot": "Kilknij tutaj aby zaznaczyć główną ścieżkę", "close": "Zamknij", - "defaultPort": "25565 podstawowy", + "defaultPort": "Domyślnie 25565", "downloading": "Pobieranie serwera...", "explainRoot": "Proszę, kliknij przycisk poniżej aby zaznaczyć główną ścieżkę w tym archiwum", "importServer": "Importuj egzystujący serwer", @@ -590,6 +591,15 @@ "newServer": "Stwórz nowy serwer", "servers": "Serwery" }, + "startup": { + "almost": "Prawie gotowe! Jeszcze tylko chwilka...", + "internals": "Konfigurowanie i włączanie backendu...", + "internet": "Sprawdzam połączenie z internetem", + "server": "Włączanie ", + "serverInit": "Ładuje serwery...", + "starting": "Włączam Craftiego...", + "tasks": "Włączanie harmonogramu..." + }, "userConfig": { "apiKey": "Klucze API", "auth": "Autoryzacja? ", @@ -640,12 +650,12 @@ "edit": "Edytuj", "enabled": "Włączony", "jar_update": "Plik startowy zaktualizowany", - "kill": "Serwer zatrzymany", + "kill": "Serwer został zabity", "name": "Nazwa", "new": "Nowy Webhook", "newWebhook": "Nowy Webhook", "no-webhook": "Nie posiadasz aktualnie żadnych Webhooków dla tego serwera. Aby dodać webhook kliknij na", - "run": "Włącz Webhook", + "run": "Przetestuj Webhook", "send_command": "Komenda serwera otrzymana!", "start_server": "Serwer włączony", "stop_server": "Serwer wyłączony", diff --git a/app/translations/zh_CN.json b/app/translations/zh_CN.json index e905a406..b8689b2a 100644 --- a/app/translations/zh_CN.json +++ b/app/translations/zh_CN.json @@ -111,6 +111,7 @@ "starting": "延迟启动", "status": "状态", "stop": "停止", + "storage": "存储", "version": "版本", "welcome": "欢迎来到 Crafty Controller" }, @@ -591,6 +592,15 @@ "newServer": "创建新服务器", "servers": "服务器" }, + "startup": { + "almost": "即将完成。请稍候……", + "internals": "正在配置并启动 Crafty 的内部组件", + "internet": "正在检查网络连接", + "server": "正在初始化 ", + "serverInit": "正在初始化服务器", + "starting": "Crafty 正在启动……", + "tasks": "正在启动任务计划器" + }, "userConfig": { "apiKey": "API 密钥", "auth": "已授权?", diff --git a/docker/unraid.xml b/docker/unraid.xml index c1b106fb..8c036ba2 100644 --- a/docker/unraid.xml +++ b/docker/unraid.xml @@ -21,7 +21,7 @@ https://wiki.craftycontrol.com/uploads/en/crafty%204%20dashboard%20with%20one%20server.jpeg https://wiki.craftycontrol.com/uploads/en/crafty%204%20server%20setup%20details.png - Crafty 4 is the next iteration of our Minecraft Server Wrapper / Controller / Launcher. [br]Boasting a clean new look, rebuilt from the ground up. [br] [br] Crafty 4 brings a whole host of new features such as Bedrock support. [br] With SteamCMD support on the way![br] **Default login Credentrails are username: "admin" password: "crafty". ** [br]Crafty 4 is the successor of Crafty Controller. [br]For official support join the Discord server https://discord.gg/9VJPhCE [br] For migration from 3.x please refer to the documentation: https://wiki.craftycontrol.com/en/4/ + Crafty 4 is the next iteration of our Minecraft Server Wrapper / Controller / Launcher. [br]Boasting a clean new look, rebuilt from the ground up. [br] [br] Crafty 4 brings a whole host of new features such as Bedrock support. [br] With SteamCMD support on the way![br] **Default login Credentrails are stored in your Crafty Configuration location in the file default-creds.txt ** [br]Crafty 4 is the successor of Crafty Controller. [br]For official support join the Discord server https://discord.gg/9VJPhCE [br] For migration from 3.x please refer to the documentation: https://wiki.craftycontrol.com/en/4/ GameServers: Other: https://[IP]:[PORT:8443]/ diff --git a/main.py b/main.py index 143dfb4f..75997247 100644 --- a/main.py +++ b/main.py @@ -20,6 +20,18 @@ from app.classes.shared.websocket_manager import WebSocketManager console = Console() helper = Helpers() +# Get the path our application is running on. +if getattr(sys, "frozen", False): + APPLICATION_PATH = os.path.dirname(sys.executable) + RUNNING_MODE = "Frozen/executable" +else: + try: + app_full_path = os.path.realpath(__file__) + APPLICATION_PATH = os.path.dirname(app_full_path) + RUNNING_MODE = "Non-interactive (e.g. 'python main.py')" + except NameError: + APPLICATION_PATH = os.getcwd() + RUNNING_MODE = "Interactive" if helper.check_root(): Console.critical( "Root detected. Root/Admin access denied. " @@ -39,6 +51,7 @@ if not (sys.version_info.major == 3 and sys.version_info.minor >= 9): time.sleep(3) Console.info("Crafty stopped. Exiting...") sys.exit(0) + # pylint: disable=wrong-import-position try: from app.classes.models.base_model import database_proxy @@ -51,7 +64,180 @@ except ModuleNotFoundError as err: helper.auto_installer_fix(err) +def internet_check(): + """ + This checks to see if the Crafty host is connected to the + internet. This will show a warning in the console if no interwebs. + """ + print() + logger.info("Checking Internet. This may take a minute.") + Console.info("Checking Internet. This may take a minute.") + + if not helper.check_internet(): + logger.warning( + "We have detected the machine running Crafty has no " + "connection to the internet. Client connections to " + "the server may be limited." + ) + Console.warning( + "We have detected the machine running Crafty has no " + "connection to the internet. Client connections to " + "the server may be limited." + ) + + +def controller_setup(): + """ + Method sets up the software controllers. + This also sets the application path as well as the + master server dir (if not set). + + This also clears the support logs status. + """ + if not controller.check_system_user(): + controller.add_system_user() + + master_server_dir = controller.management.get_master_server_dir() + if master_server_dir == "": + logger.debug("Could not find master server path. Setting default") + controller.set_master_server_dir( + os.path.join(controller.project_root, "servers") + ) + else: + helper.servers_dir = master_server_dir + + logger.info(f"Execution Mode: {RUNNING_MODE}") + logger.info(f"Application path: '{APPLICATION_PATH}'") + Console.info(f"Execution Mode: {RUNNING_MODE}") + Console.info(f"Application path: '{APPLICATION_PATH}'") + + controller.clear_support_status() + + +def tasks_starter(): + """ + Method starts stats recording, app scheduler, and + serverjars/steamCMD cache refreshers + """ + # start stats logging + tasks_manager.start_stats_recording() + + # once the controller is up and stats are logging, we can kick off + # the scheduler officially + tasks_manager.start_scheduler() + + # refresh our cache and schedule for every 12 hoursour cache refresh + # for serverjars.com + tasks_manager.serverjar_cache_refresher() + + +def signal_handler(signum, _frame): + """ + Method handles sigterm and shuts the app down. + """ + if not args.daemon: + print() # for newline after prompt + signame = signal.Signals(signum).name + logger.info(f"Recieved signal {signame} [{signum}], stopping Crafty...") + Console.info(f"Recieved signal {signame} [{signum}], stopping Crafty...") + tasks_manager._main_graceful_exit() + crafty_prompt.universal_exit() + + +def do_cleanup(): + """ + Checks Crafty's temporary directory and clears it out on boot. + """ + try: + logger.info("Removing old temp dirs") + FileHelpers.del_dirs(os.path.join(controller.project_root, "temp")) + except: + logger.info("Did not find old temp dir.") + os.mkdir(os.path.join(controller.project_root, "temp")) + + +def do_version_check(): + """ + Checks for remote version differences. + + Prints in terminal with differences if true. + + Also sets helper variable to update available when pages + are served. + """ + + # Check if new version available + remote_ver = helper.check_remote_version() + if remote_ver: + notice = f""" + A new version of Crafty is available! + {'/' * 37} + New version available: {remote_ver} + Current version: {pkg_version.parse(helper.get_version_string())} + {'/' * 37} + """ + Console.yellow(notice) + + crafty_prompt.prompt = f"Crafty Controller v{helper.get_version_string()} > " + + +def setup_starter(): + """ + This method starts our setup threads. + (tasks scheduler, internet checks, controller setups) + + Once our threads complete we will set our startup + variable to false and send a reload to any clients waiting. + + + """ + if not args.daemon: + time.sleep(0.01) # Wait for the prompt to start + print() # Make a newline after the prompt so logs are on an empty line + else: + time.sleep(0.01) # Wait for the daemon info message + + Console.info("Setting up Crafty's internal components...") + # Start the setup threads + web_sock.broadcast("update", {"section": "tasks"}) + time.sleep(2) + tasks_starter_thread.start() + web_sock.broadcast("update", {"section": "internet"}) + time.sleep(2) + internet_check_thread.start() + web_sock.broadcast( + "update", + {"section": "internals"}, + ) + time.sleep(2) + controller_setup_thread.start() + + # Wait for the setup threads to finish + web_sock.broadcast( + "update", + {"section": "almost"}, + ) + tasks_starter_thread.join() + internet_check_thread.join() + controller_setup_thread.join() + helper.crafty_starting = False + web_sock.broadcast("send_start_reload", "") + do_version_check() + Console.info("Crafty has fully started and is now ready for use!") + + do_cleanup() + + if not args.daemon: + # Put the prompt under the cursor + crafty_prompt.print_prompt() + + def do_intro(): + """ + Runs the Crafty Controller Terminal Intro with information about the software + This method checks for a "settings file" or config.json. If it does not find + one it will create one. + """ logger.info("***** Crafty Controller Started *****") version = helper.get_version_string() @@ -72,7 +258,22 @@ def do_intro(): def setup_logging(debug=True): - logging_config_file = os.path.join(os.path.curdir, "app", "config", "logging.json") + """ + This method sets up our logging for Crafty. It takes + one optional (defaulted to True) parameter which + determines whether or not the logging level is "debug" or verbose. + """ + logging_config_file = os.path.join( + APPLICATION_PATH, "app", "config", "logging.json" + ) + if not helper.check_file_exists( + os.path.join(APPLICATION_PATH, "logs", "auth_tracker.log") + ): + open( + os.path.join(APPLICATION_PATH, "logs", "auth_tracker.log"), + "a", + encoding="utf-8", + ).close() if os.path.exists(logging_config_file): # open our logging config file @@ -109,11 +310,11 @@ if __name__ == "__main__": ) args = parser.parse_args() - helper.ensure_logging_setup() - + helper.crafty_starting = True + # Init WebSocket Manager Here + web_sock = WebSocketManager() setup_logging(debug=args.verbose) - if args.verbose: Console.level = "debug" @@ -125,20 +326,18 @@ if __name__ == "__main__": # print our pretty start message do_intro() - # our session file, helps prevent multiple controller agents on the same machine. helper.create_session_file(ignore=args.ignore) - # start the database database = peewee.SqliteDatabase( helper.db_path, pragmas={"journal_mode": "wal", "cache_size": -1024 * 10} ) database_proxy.initialize(database) - migration_manager = MigrationManager(database, helper) migration_manager.up() # Automatically runs migrations - # do our installer stuff + # init classes + # now the tables are created, we can load the tasks_manager and server controller user_helper = HelperUsers(database, helper) management_helper = HelpersManagement(database, helper) installer = DatabaseBuilder(database, helper, user_helper, management_helper) @@ -152,7 +351,19 @@ if __name__ == "__main__": f"through your router/firewall if you would like to be able " f"to access Crafty remotely." ) - installer.default_settings() + PASSWORD = helper.create_pass() + installer.default_settings(PASSWORD) + with open( + os.path.join(APPLICATION_PATH, "app", "config", "default-creds.txt"), + "w", + encoding="utf-8", + ) as cred_file: + cred_file.write( + json.dumps({"username": "admin", "password": PASSWORD}, indent=4) + ) + os.chmod( + os.path.join(APPLICATION_PATH, "app", "config", "default-creds.txt"), 0o600 + ) else: Console.debug("Existing install detected") Console.info("Checking for reset secret flag") @@ -163,153 +374,54 @@ if __name__ == "__main__": helper.set_setting("reset_secrets_on_next_boot", False) else: Console.info("No flag found. Secrets are staying") + + # now we've initialized our database for fresh install we + # can finishing initializing our controllers/modules file_helper = FileHelpers(helper) import_helper = ImportHelpers(helper, file_helper) - # Init WebSocket Manager Here - WebSocketManager() - # now the tables are created, we can load the tasks_manager and server controller controller = Controller(database, helper, file_helper, import_helper) + controller.set_project_root(APPLICATION_PATH) + tasks_manager = TasksManager(helper, controller, file_helper) + import3 = Import3(helper, controller) + + # Check to see if client config.json version is different than the + # Master config.json in helpers.py Console.info("Checking for remote changes to config.json") controller.get_config_diff() Console.info("Remote change complete.") - import3 = Import3(helper, controller) - tasks_manager = TasksManager(helper, controller, file_helper) + # startup the web server tasks_manager.start_webserver() - def signal_handler(signum, _frame): - if not args.daemon: - print() # for newline after prompt - signame = signal.Signals(signum).name - logger.info(f"Recieved signal {signame} [{signum}], stopping Crafty...") - Console.info(f"Recieved signal {signame} [{signum}], stopping Crafty...") - tasks_manager._main_graceful_exit() - crafty_prompt.universal_exit() - signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGINT, signal_handler) # init servers logger.info("Initializing all servers defined") Console.info("Initializing all servers defined") + web_sock.broadcast( + "update", + {"section": "serverInit"}, + ) controller.servers.init_all_servers() - def tasks_starter(): - # start stats logging - tasks_manager.start_stats_recording() - - # once the controller is up and stats are logging, we can kick off - # the scheduler officially - tasks_manager.start_scheduler() - - # refresh our cache and schedule for every 12 hoursour cache refresh - # for serverjars.com - tasks_manager.serverjar_cache_refresher() - + # start up our tasks handler in tasks.py tasks_starter_thread = Thread(target=tasks_starter, name="tasks_starter") - def internet_check(): - print() - logger.info("Checking Internet. This may take a minute.") - Console.info("Checking Internet. This may take a minute.") - - if not helper.check_internet(): - logger.warning( - "We have detected the machine running Crafty has no " - "connection to the internet. Client connections to " - "the server may be limited." - ) - Console.warning( - "We have detected the machine running Crafty has no " - "connection to the internet. Client connections to " - "the server may be limited." - ) - + # check to see if instance has internet internet_check_thread = Thread(target=internet_check, name="internet_check") - def controller_setup(): - if not controller.check_system_user(): - controller.add_system_user() - - if getattr(sys, "frozen", False): - application_path = os.path.dirname(sys.executable) - running_mode = "Frozen/executable" - else: - try: - app_full_path = os.path.realpath(__file__) - application_path = os.path.dirname(app_full_path) - running_mode = "Non-interactive (e.g. 'python main.py')" - except NameError: - application_path = os.getcwd() - running_mode = "Interactive" - - controller.set_project_root(application_path) - master_server_dir = controller.management.get_master_server_dir() - if master_server_dir == "": - logger.debug("Could not find master server path. Setting default") - controller.set_master_server_dir( - os.path.join(controller.project_root, "servers") - ) - else: - helper.servers_dir = master_server_dir - - Console.debug(f"Execution Mode: {running_mode}") - Console.debug(f"Application path : '{application_path}'") - - controller.clear_support_status() - + # start the Crafty console. crafty_prompt = MainPrompt( helper, tasks_manager, migration_manager, controller, import3 ) + # set up all controllers controller_setup_thread = Thread(target=controller_setup, name="controller_setup") - def setup_starter(): - if not args.daemon: - time.sleep(0.01) # Wait for the prompt to start - print() # Make a newline after the prompt so logs are on an empty line - else: - time.sleep(0.01) # Wait for the daemon info message + setup_starter_thread = Thread(target=setup_starter, name="setup_starter") - Console.info("Setting up Crafty's internal components...") - - # Start the setup threads - tasks_starter_thread.start() - internet_check_thread.start() - controller_setup_thread.start() - - # Wait for the setup threads to finish - tasks_starter_thread.join() - internet_check_thread.join() - controller_setup_thread.join() - - Console.info("Crafty has fully started and is now ready for use!") - - # Check if new version available - remote_ver = helper.check_remote_version() - if remote_ver: - notice = f""" - A new version of Crafty is available! - {'/' * 37} - New version available: {remote_ver} - Current version: {pkg_version.parse(helper.get_version_string())} - {'/' * 37} - """ - Console.yellow(notice) - - crafty_prompt.prompt = f"Crafty Controller v{helper.get_version_string()} > " - try: - logger.info("Removing old temp dirs") - FileHelpers.del_dirs(os.path.join(controller.project_root, "temp")) - except: - logger.info("Did not find old temp dir.") - os.mkdir(os.path.join(controller.project_root, "temp")) - - if not args.daemon: - # Put the prompt under the cursor - crafty_prompt.print_prompt() - - Thread(target=setup_starter, name="setup_starter").start() + setup_starter_thread.start() if not args.daemon: # Start the Crafty prompt diff --git a/requirements.txt b/requirements.txt index 1e9feb0f..7b1adcfc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,13 +4,13 @@ argon2-cffi==23.1.0 cached_property==1.5.2 colorama==0.4.6 croniter==1.4.1 -cryptography==41.0.4 +cryptography==41.0.7 libgravatar==1.0.4 nh3==0.2.14 packaging==23.2 peewee==3.13 psutil==5.9.5 -pyOpenSSL==23.2.0 +pyOpenSSL==23.3.0 pyjwt==2.8.0 PyYAML==6.0.1 requests==2.31.0 diff --git a/sonar-project.properties b/sonar-project.properties index 210aa767..938b710f 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,7 +3,7 @@ sonar.organization=crafty-controller # This is the name and version displayed in the SonarCloud UI. sonar.projectName=Crafty 4 -sonar.projectVersion=4.2.1 +sonar.projectVersion=4.2.2 sonar.python.version=3.9, 3.10, 3.11 sonar.exclusions=app/migrations/**, app/frontend/static/assets/vendors/**