diff --git a/CHANGELOG.md b/CHANGELOG.md index eb186b78..59cbd693 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,16 @@ ## --- [4.0.20] - 2022/TBD ### New features - Add option to run command before backup. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/536)) +- Make Config.json editable from panel. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/532)) +- Managed config.json refector (See MR for details). ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/485)) ### Bug fixes - Fix local java server imports. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/529)) - Fix Schedule Restore | Add Backup Config Preservation. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/533)) - Rework `/public` Route. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/538)) ### Tweaks +- Hide stats DB directory from files tree. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/530)) +- Make it so file tree doesn't reload on upload/delete. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/541)) +- Add upload completed feedback to file upload. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/541)) - Added further login screen customisation settings. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/531)) - Set backup filename to use same time as schedule. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/534)) - Move Schedules to from DB to Queue Datatype. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/535)) diff --git a/app/classes/controllers/management_controller.py b/app/classes/controllers/management_controller.py index dc0f5422..2811dce4 100644 --- a/app/classes/controllers/management_controller.py +++ b/app/classes/controllers/management_controller.py @@ -46,6 +46,14 @@ class ManagementController: def get_crafty_api_key(): return HelpersManagement.get_secret_api_key() + @staticmethod + def set_cookie_secret(key): + HelpersManagement.set_cookie_secret(key) + + @staticmethod + def add_crafty_row(): + HelpersManagement.create_crafty_row() + # ********************************************************************************** # Commands Methods # ********************************************************************************** diff --git a/app/classes/models/management.py b/app/classes/models/management.py index bb183eef..c2b5afde 100644 --- a/app/classes/models/management.py +++ b/app/classes/models/management.py @@ -43,6 +43,7 @@ class AuditLog(BaseModel): # ********************************************************************************** class CraftySettings(BaseModel): secret_api_key = CharField(default="") + cookie_secret = CharField(default="") login_photo = CharField(default="login_1.jpg") login_opacity = IntegerField(default=100) @@ -204,9 +205,22 @@ class HelpersManagement: else: return + @staticmethod + def create_crafty_row(): + CraftySettings.insert( + { + CraftySettings.secret_api_key: "", + CraftySettings.cookie_secret: "", + CraftySettings.login_photo: "login_1.jpg", + CraftySettings.login_opacity: 100, + } + ).execute() + @staticmethod def set_secret_api_key(key): - CraftySettings.insert(secret_api_key=key).execute() + CraftySettings.update({CraftySettings.secret_api_key: key}).where( + CraftySettings.id == 1 + ).execute() @staticmethod def get_secret_api_key(): @@ -215,6 +229,19 @@ class HelpersManagement: ) return settings[0].secret_api_key + @staticmethod + def get_cookie_secret(): + settings = CraftySettings.select(CraftySettings.cookie_secret).where( + CraftySettings.id == 1 + ) + return settings[0].cookie_secret + + @staticmethod + def set_cookie_secret(key): + CraftySettings.update({CraftySettings.cookie_secret: key}).where( + CraftySettings.id == 1 + ).execute() + # ********************************************************************************** # Config Methods # ********************************************************************************** diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index c5238ae8..92226425 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -78,6 +78,7 @@ class Helpers: self.websocket_helper = WebSocketHelper(self) self.translation = Translation(self) self.update_available = False + self.ignored_names = ["crafty_managed.txt", "db_stats"] @staticmethod def auto_installer_fix(ex): @@ -376,6 +377,64 @@ class Helpers: return default_return + def set_settings(self, data): + try: + with open(self.settings_file, "w", encoding="utf-8") as f: + json.dump(data, f, indent=4) + + except Exception as e: + logger.critical( + f"Config File Error: Unable to read {self.settings_file} due to {e}" + ) + Console.critical( + f"Config File Error: Unable to read {self.settings_file} due to {e}" + ) + return False + + return True + + @staticmethod + def get_master_config(): + # Make changes for users' local config.json files here. As of 4.0.20 + # Config.json was removed from the repo to make it easier for users + # To make non-breaking changes to the file. + return { + "http_port": 8000, + "https_port": 8443, + "language": "en_EN", + "cookie_expire": 30, + "show_errors": True, + "history_max_age": 7, + "stats_update_frequency": 30, + "delete_default_json": False, + "show_contribute_link": True, + "virtual_terminal_lines": 70, + "max_log_lines": 700, + "max_audit_entries": 300, + "disabled_language_files": [], + "stream_size_GB": 1, + "keywords": ["help", "chunk"], + "allow_nsfw_profile_pictures": False, + "enable_user_self_delete": False, + "reset_secrets_on_next_boot": False, + } + + def get_all_settings(self): + try: + with open(self.settings_file, "r", encoding="utf-8") as f: + data = json.load(f) + + except Exception as e: + data = {} + logger.critical( + f"Config File Error: Unable to read {self.settings_file} due to {e}" + ) + Console.critical( + f"Config File Error: Unable to read {self.settings_file} due to {e}" + ) + + return data + @staticmethod def is_subdir(server_path, root_dir): server_path = os.path.realpath(server_path) @@ -947,8 +1006,7 @@ class Helpers: return data - @staticmethod - def generate_tree(folder, output=""): + def generate_tree(self, folder, output=""): dir_list = [] unsorted_files = [] file_list = os.listdir(folder) @@ -965,18 +1023,22 @@ class Helpers: rel = os.path.join(folder, raw_filename) dpath = os.path.join(folder, filename) if os.path.isdir(rel): - output += f"""
  • - \n
    - - - - {filename} - -
  • - \n""" + if filename not in self.ignored_names: + output += f"""
  • + \n
    + + + + {filename} + +
  • + \n""" else: - if filename != "crafty_managed.txt": - output += f"""
  • {filename}
  • """ return output - @staticmethod - def generate_dir(folder, output=""): + def generate_dir(self, folder, output=""): dir_list = [] unsorted_files = [] @@ -1004,17 +1065,21 @@ class Helpers: dpath = os.path.join(folder, filename) rel = os.path.join(folder, raw_filename) if os.path.isdir(rel): - output += f"""
  • - \n
    - - - - {filename} - -
  • """ + if filename not in self.ignored_names: + output += f"""
  • + \n
    + + + + {filename} + +
  • """ else: - if filename != "crafty_managed.txt": - output += f"""
  • + + +
    + +
    +
    + +
    +
    + + +
    +
    +
    +
    + + {% if data['superuser'] %} + {% include "parts/crafty_config_list.html %} + {% end %} + + +
    +
    + +
    + +
    + + +
    + {% raw xsrf_form_html() %} + + {% for item in data['config-json'].items() %} + {% if item[0] == "reset_secrets_on_next_boot" %} +
    + {% else %} +
    + {% end %} +
    + {% if item[0] == 'language' %} + + {% elif item[0] == 'disabled_language_files' %} +
    + + + +
    + {% elif isinstance(item[1], list) %} + + {% elif isinstance(item[1], bool) %} +
    + {% if item[1] == True %} + +  
    + +   + {% else %} + +  
    + +   + {% end %} +
    + {% elif isinstance(item[1], int) %} + + {% else %} + + {% end %} +
    + {% end %} +   + +
    +
    +
    +
    + + +
    + + + + +{% end %} + +{% block js %} + + + +{% end %} \ No newline at end of file diff --git a/app/frontend/templates/panel/custom_login.html b/app/frontend/templates/panel/custom_login.html new file mode 100644 index 00000000..018eb006 --- /dev/null +++ b/app/frontend/templates/panel/custom_login.html @@ -0,0 +1,391 @@ +{% extends ../base.html %} + +{% block meta %} +{% end %} + +{% block title %}Crafty Controller - {{ translate('customLogin', 'pageTitle', data['lang']) }}{% end %} + +{% block content %} + +
    + +
    +
    + +
    +
    + + +
    +
    +
    +
    + + + {% if data['superuser'] %} + {% include "parts/crafty_config_list.html %} + {% end %} + + +
    +
    + +
    + +
    + + +
    +
    +
    +
    +
    +
    +

    {{ translate('customLogin', 'loginImage', data['lang']) }}

    +
    +
    + {% raw xsrf_form_html() %} + +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    {{ translate('customLogin', 'preview', data['lang']) }}:
    +
    +
    + +
    + +
    +
    + +
    + + +
    + +
    +
    +
    + Responsive image +
    + +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    + + + + +{% end %} + +{% block js %} + + + +{% end %} \ No newline at end of file diff --git a/app/frontend/templates/panel/panel_config.html b/app/frontend/templates/panel/panel_config.html index 4bf13833..249daf70 100644 --- a/app/frontend/templates/panel/panel_config.html +++ b/app/frontend/templates/panel/panel_config.html @@ -8,16 +8,16 @@ {% block content %}
    -
    -
    @@ -26,6 +26,23 @@
    + + {% if data['superuser'] %} + {% include "parts/crafty_config_list.html %} + {% end %} + + +
    +
    + +
    + +
    + +
    @@ -33,9 +50,7 @@

    {{ translate('panelConfig', 'users', data['lang']) }}

    {% if data['user_data']['hints'] %} - + {% end %}
    -
    -
    -
    -
    -

    {{ translate('panelConfig', 'customLoginPage', data['lang']) }}

    -
    -
    -
    -
    -

    {{ translate('panelConfig', 'loginImage', data['lang']) }}

    -
    -
    - {% raw xsrf_form_html() %} - -
    - -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    {{ translate('panelConfig', 'preview', data['lang']) }}:
    -
    -
    - -
    - -
    -
    - -
    - - -
    - -
    -
    -
    - Responsive image -
    - -
    -
    -
    - - -
    -
    -
    -
    -
    -
    -
    -
    -
    {% end %}
    @@ -461,116 +295,5 @@ } }); - {% end %} \ No newline at end of file diff --git a/app/frontend/templates/panel/parts/crafty_config_list.html b/app/frontend/templates/panel/parts/crafty_config_list.html new file mode 100644 index 00000000..5ef6b922 --- /dev/null +++ b/app/frontend/templates/panel/parts/crafty_config_list.html @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/app/frontend/templates/panel/server_files.html b/app/frontend/templates/panel/server_files.html index af287b43..5734f3a6 100644 --- a/app/frontend/templates/panel/server_files.html +++ b/app/frontend/templates/panel/server_files.html @@ -697,7 +697,7 @@ }); } - function sendFile(file, path, serverId, left, onProgress) { + async function sendFile(file, path, serverId, left, i, onProgress) { let xmlHttpRequest = new XMLHttpRequest(); let token = getCookie("_xsrf") let fileName = file.name @@ -719,7 +719,36 @@ onProgress(Math.floor(event.loaded / event.total * 100)), false); xmlHttpRequest.addEventListener('load', (event) => { if (event.target.responseText == 'success') { - console.log('Upload for file', file.name, 'was successful!') + console.log('Upload for file', file.name, 'was successful!'); + let caught = false; + try { + var par_el = document.getElementById(path + "ul"); + var items = par_el.children; + } catch { + caught = true; + var par_el = document.getElementById("files-tree"); + var items = par_el.children; + } + let name = file.name; + console.log(par_el) + let full_path = path + '/' + name + let flag = false; + for (var k = 0; k < items.length; ++k) { + if ($(items[k]).attr("data-name") == name) { + flag = true; + } + } + if (!flag) { + if (caught) { + $(par_el).append('
  • ' + name + '
  • '); + } else { + $(par_el).append('
  • ' + name + '
  • '); + } + setTreeViewContext(); + } + $(`#upload-progress-bar-${i + 1}`).removeClass("progress-bar-striped"); + $(`#upload-progress-bar-${i + 1}`).addClass("bg-success"); + $(`#upload-progress-bar-${i + 1}`).html('') } else { alert('Upload failed with response: ' + event.target.responseText); @@ -735,7 +764,7 @@ let uploadWaitDialog; let doUpload = true; - function uploadFilesE(event) { + async function uploadFilesE(event) { path = event.target.parentElement.getAttribute('data-path'); $(function () { var uploadHtml = "
    " + @@ -795,7 +824,7 @@ `; $('#upload-progress-bar-parent').append(progressHtml); - sendFile(files.files[i], path, serverId, nFiles - i - 1, (progress) => { + await sendFile(files.files[i], path, serverId, nFiles - i - 1, i, (progress) => { $(`#upload-progress-bar-${i + 1}`).attr('aria-valuenow', progress) $(`#upload-progress-bar-${i + 1}`).css('width', progress + '%') }); @@ -996,7 +1025,6 @@ function hideUploadBox() { if (!uploadWaitDialog) return; uploadWaitDialog.modal('hide'); - getTreeView(); } function createFileE(event) { @@ -1074,7 +1102,8 @@ callback: function (result) { if (!result) return; deleteFile(path, function () { - getTreeView() + el = document.getElementById(path + "li"); + $(el).remove(); document.getElementById('files-tree-nav').style.display = 'none'; }); } @@ -1102,7 +1131,8 @@ callback: function (result) { if (!result) return; deleteDir(path, function () { - getTreeView() + el = document.getElementById(path + "li"); + $(el).remove(); document.getElementById('files-tree-nav').style.display = 'none'; }); } diff --git a/app/migrations/20230129_secrets_shh.py b/app/migrations/20230129_secrets_shh.py new file mode 100644 index 00000000..5610f6e0 --- /dev/null +++ b/app/migrations/20230129_secrets_shh.py @@ -0,0 +1,16 @@ +# Generated by database migrator +import peewee + + +def migrate(migrator, database, **kwargs): + migrator.add_columns("crafty_settings", cookie_secret=peewee.CharField(default="")) + """ + Write your migrations here. + """ + + +def rollback(migrator, database, **kwargs): + migrator.drop_columns("crafty_settings", ["cookie_secret"]) + """ + Write your rollback migrations here. + """ diff --git a/app/translations/en_EN.json b/app/translations/en_EN.json index 296f1b16..15a8ed27 100644 --- a/app/translations/en_EN.json +++ b/app/translations/en_EN.json @@ -213,6 +213,8 @@ "assignedRoles": "Assigned Roles", "cancel": "Cancel", "clearComms": "Clear Un-executed Commands", + "select": "Select", + "apply": "Apply", "delete": "Delete", "edit": "Edit", "enabled": "Enabled", @@ -228,6 +230,9 @@ "superConfirmTitle": "Enable superuser? Are you sure?", "user": "User", "users": "Users", + "title": "Crafty Configuration" + }, + "customLogin": { "customLoginPage": "Customise the Login Page", "loginImage": "Upload a background image for the login screen.", "backgroundUpload": "Background Upload", @@ -235,8 +240,10 @@ "loginOpacity": "Select the Login Window Opacity", "select": "Select", "apply": "Apply", + "delete": "Delete", "selectImage": "Select an image", - "preview": "Preview" + "preview": "Preview", + "pageTitle": "Custom Login Page" }, "rolesConfig": { "config": "Role Config", diff --git a/docker_launcher.sh b/docker_launcher.sh index 3e2cfd6c..c270ad2e 100644 --- a/docker_launcher.sh +++ b/docker_launcher.sh @@ -18,16 +18,6 @@ if [ ! "$(ls -A --ignore=.gitkeep ./app/config)" ]; then else # Keep version file up to date with image cp -f ./app/config_original/version.json ./app/config/version.json - - # Compare if user's config is different from image, and show differences. - echo "\033[36mWrapper | \033[35m🏗️ Checking for config.json changes..." - cp -f ./app/config_original/config.json ./app/config/config_image_template - if [ "$(diff -q ./app/config/config.json ./app/config/config_image_template)" ]; then - echo "\033[36mWrapper | \033[33m👷 We've found differences in your local config, please review!," - echo "\033[36m | \033[33m (This could be an outdated config.json)" - else - echo "\033[36mWrapper | \033[32m✅ Config good! Proceeding..." - fi fi diff --git a/main.py b/main.py index 8976360e..5f05390e 100644 --- a/main.py +++ b/main.py @@ -14,6 +14,7 @@ from app.classes.shared.import3 import Import3 from app.classes.shared.console import Console from app.classes.shared.helpers import Helpers from app.classes.models.users import HelperUsers +from app.classes.models.management import HelpersManagement from app.classes.shared.import_helper import ImportHelpers console = Console() @@ -53,6 +54,9 @@ def do_intro(): """ Console.magenta(intro) + if not helper.check_file_exists(helper.settings_file): + Console.debug("No settings file detected. Creating one.") + helper.set_settings(Helpers.get_master_config()) def setup_logging(debug=True): @@ -121,7 +125,8 @@ if __name__ == "__main__": # do our installer stuff user_helper = HelperUsers(database, helper) - installer = DatabaseBuilder(database, helper, user_helper) + management_helper = HelpersManagement(database, helper) + installer = DatabaseBuilder(database, helper, user_helper, management_helper) FRESH_INSTALL = installer.is_fresh_install() if FRESH_INSTALL: @@ -135,10 +140,22 @@ if __name__ == "__main__": installer.default_settings() else: Console.debug("Existing install detected") + Console.info("Checking for reset secret flag") + if helper.get_setting("reset_secrets_on_next_boot"): + Console.info("Found Reset") + management_helper.set_secret_api_key(str(helper.random_string_generator(64))) + management_helper.set_cookie_secret(str(helper.random_string_generator(32))) + helper.set_setting("reset_secrets_on_next_boot", False) + else: + Console.info("No flag found. Secrets are staying") file_helper = FileHelpers(helper) import_helper = ImportHelpers(helper, file_helper) # now the tables are created, we can load the tasks_manager and server controller controller = Controller(database, helper, file_helper, import_helper) + 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) tasks_manager.start_webserver()