From 33eea02c6ef9ef6ee1b0d79e576d4461bdeaae7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Diot?= Date: Mon, 23 Sep 2024 12:00:51 +0200 Subject: [PATCH] Add readonly specifications to ui routes --- src/ui/app/models/ui_database.py | 21 +- src/ui/app/routes/bans.py | 74 +----- src/ui/app/routes/configs.py | 9 + src/ui/app/routes/instances.py | 5 + src/ui/app/routes/jobs.py | 3 + src/ui/app/routes/profile.py | 32 ++- src/ui/app/routes/services.py | 7 + src/ui/app/static/css/overrides.css | 3 + src/ui/app/static/js/pages/bans.js | 29 ++- src/ui/app/static/js/pages/config_edit.js | 10 + src/ui/app/static/js/pages/configs.js | 24 +- src/ui/app/static/js/pages/instances.js | 124 ++++++---- src/ui/app/static/js/pages/jobs.js | 9 + src/ui/app/static/js/pages/services.js | 33 ++- src/ui/app/static/js/plugins-settings.js | 16 ++ src/ui/app/templates/bans.html | 224 +++++++++--------- src/ui/app/templates/base.html | 1 + src/ui/app/templates/cache_view.html | 7 +- src/ui/app/templates/config_edit.html | 25 +- src/ui/app/templates/configs.html | 94 ++++---- src/ui/app/templates/dashboard.html | 8 +- src/ui/app/templates/instances.html | 164 ++++++------- src/ui/app/templates/jobs.html | 6 +- src/ui/app/templates/logs.html | 7 +- .../templates/models/plugins_settings.html | 71 ++++-- .../models/plugins_settings_easy.html | 41 +++- .../models/plugins_settings_raw.html | 19 +- src/ui/app/templates/profile.html | 149 +++++++----- src/ui/app/templates/services.html | 178 +++++++------- src/ui/gunicorn.conf.py | 20 +- src/ui/main.py | 13 +- 31 files changed, 852 insertions(+), 574 deletions(-) diff --git a/src/ui/app/models/ui_database.py b/src/ui/app/models/ui_database.py index af8d5c2ee..af90cb426 100644 --- a/src/ui/app/models/ui_database.py +++ b/src/ui/app/models/ui_database.py @@ -479,9 +479,10 @@ class UIDatabase(Database): return "" - def get_ui_user_sessions(self, username: str, current_session_id: Optional[str] = None) -> List[UserSessions]: + def get_ui_user_sessions(self, username: str, current_session_id: Optional[str] = None) -> List[dict]: """Get ui user sessions.""" with self._db_session() as session: + sessions = [] if current_session_id: current_session_query = session.query(UserSessions).filter_by(user_name=username, id=current_session_id) other_sessions_query = ( @@ -490,10 +491,22 @@ class UIDatabase(Database): .filter(UserSessions.id != current_session_id) .order_by(UserSessions.creation_date.desc()) ) - combined_query = current_session_query.union_all(other_sessions_query) - return combined_query.all() + query = current_session_query.union_all(other_sessions_query) else: - return session.query(UserSessions).filter_by(user_name=username).order_by(UserSessions.creation_date.desc()).all() + query = session.query(UserSessions).filter_by(user_name=username).order_by(UserSessions.creation_date.desc()) + + for session_data in query: + sessions.append( + { + "id": session_data.id, + "ip": session_data.ip, + "user_agent": session_data.user_agent, + "creation_date": session_data.creation_date, + "last_activity": session_data.last_activity, + } + ) + + return sessions def delete_ui_user_old_sessions(self, username: str) -> str: """Delete ui user old sessions.""" diff --git a/src/ui/app/routes/bans.py b/src/ui/app/routes/bans.py index c3306cd95..aa61e5d06 100644 --- a/src/ui/app/routes/bans.py +++ b/src/ui/app/routes/bans.py @@ -6,7 +6,7 @@ from time import time from flask import Blueprint, flash as flask_flash, redirect, render_template, request, url_for from flask_login import login_required -from app.dependencies import BW_INSTANCES_UTILS +from app.dependencies import BW_INSTANCES_UTILS, DB from app.utils import flash from app.routes.utils import get_redis_client, get_remain, handle_error, verify_data_in_form @@ -19,72 +19,6 @@ bans = Blueprint("bans", __name__) def bans_page(): redis_client = get_redis_client() - def get_load_data(): - try: - data = loads(request.form["data"]) - assert isinstance(data, list) - return data - except BaseException: - return handle_error("Data must be a list of dict", "bans", False, "exception") - - if request.method == "POST" and request.form["operation"] == "unban": - data = get_load_data() - - for unban in data: - try: - unban = loads(unban.replace('"', '"').replace("'", '"')) - except BaseException: - flask_flash(f"Invalid unban: {unban}, skipping it ...", "error") - continue - - if "ip" not in unban: - flask_flash(f"Invalid unban: {unban}, skipping it ...", "error") - continue - - if redis_client: - if not redis_client.delete(f"bans_ip_{unban['ip']}"): - flash(f"Couldn't unban {unban['ip']} on redis", "error") - - resp = BW_INSTANCES_UTILS.unban(unban["ip"]) - if resp: - flash(f"Couldn't unban {unban['ip']} on the following instances: {', '.join(resp)}", "error") - else: - flash(f"Successfully unbanned {unban['ip']}") - - return redirect(url_for("loading", next=url_for("bans.bans_page"), message="Update bans")) - - if request.method == "POST" and request.form["operation"] == "ban": - data = get_load_data() - - for ban in data: - if not isinstance(ban, dict) or "ip" not in ban: - flask_flash(f"Invalid ban: {ban}, skipping it ...", "error") - continue - - reason = ban.get("reason", "ui") - ban_end = 86400.0 - if "ban_end" in ban: - try: - ban_end = float(ban["ban_end"]) - except ValueError: - continue - current_time = datetime.now().astimezone() - ban_end = (datetime.fromtimestamp(ban_end, tz=current_time.tzinfo) - current_time).total_seconds() - - if redis_client: - ok = redis_client.set(f"bans_ip_{ban['ip']}", dumps({"reason": reason, "date": time()})) - if not ok: - flash(f"Couldn't ban {ban['ip']} on redis", "error") - redis_client.expire(f"bans_ip_{ban['ip']}", int(ban_end)) - - resp = BW_INSTANCES_UTILS.ban(ban["ip"], ban_end, reason) - if resp: - flash(f"Couldn't ban {ban['ip']} on the following instances: {', '.join(resp)}", "error") - else: - flash(f"Successfully banned {ban['ip']}") - - return redirect(url_for("loading", next=url_for("bans.bans_page"), message="Update bans")) - bans = [] if redis_client: for key in redis_client.scan_iter("bans_ip_*"): @@ -118,6 +52,9 @@ def bans_page(): @bans.route("/bans/ban", methods=["POST"]) @login_required def bans_ban(): + if DB.readonly: + return handle_error("Database is in read-only mode", "bans") + verify_data_in_form( data={"bans": None}, err_message="Missing bans parameter on /bans/ban.", @@ -165,6 +102,9 @@ def bans_ban(): @bans.route("/bans/unban", methods=["POST"]) @login_required def bans_unban(): + if DB.readonly: + return handle_error("Database is in read-only mode", "bans") + verify_data_in_form( data={"ips": None}, err_message="Missing bans parameter on /bans/unban.", diff --git a/src/ui/app/routes/configs.py b/src/ui/app/routes/configs.py index fb6b59dbd..b5a8eb219 100644 --- a/src/ui/app/routes/configs.py +++ b/src/ui/app/routes/configs.py @@ -44,6 +44,9 @@ def configs_page(): @configs.route("/configs/delete", methods=["POST"]) @login_required def configs_delete(): + if DB.readonly: + return handle_error("Database is in read-only mode", "configs") + verify_data_in_form( data={"configs": None}, err_message="Missing configs parameter on /configs/delete.", @@ -119,6 +122,9 @@ def configs_delete(): @login_required def configs_new(): if request.method == "POST": + if DB.readonly: + return handle_error("Database is in read-only mode", "configs") + verify_data_in_form( data={"service": None}, err_message="Missing service parameter on /configs/new.", @@ -239,6 +245,9 @@ def configs_edit(service: str, config_type: str, name: str): return handle_error(f"Config {config_type}/{name}{' for service ' + service if service else ''} does not exist.", "configs", True) if request.method == "POST": + if DB.readonly: + return handle_error("Database is in read-only mode", "configs") + if not db_config["template"] and db_config["method"] != "ui": return handle_error( f"Config {config_type}/{name}{' for service ' + service if service else ''} is not a UI custom config and cannot be edited.", "configs", True diff --git a/src/ui/app/routes/instances.py b/src/ui/app/routes/instances.py index 876573b02..714090d4a 100644 --- a/src/ui/app/routes/instances.py +++ b/src/ui/app/routes/instances.py @@ -30,6 +30,8 @@ def instances_page(): @instances.route("/instances/new", methods=["POST"]) @login_required def instances_new(): + if DB.readonly: + return handle_error("Database is in read-only mode", "instances") verify_data_in_form( data={"hostname": None}, err_message="Missing instance hostname parameter on /instances/new.", @@ -102,6 +104,9 @@ def instances_action(action: Literal["ping", "reload", "stop", "delete"]): # TO return jsonify({"succeed": succeed, "failed": failed}), 200 elif action == "delete": + if DB.readonly: + return handle_error("Database is in read-only mode", "instances") + delete_instances = set() non_ui_instances = set() for instance in DB.get_instances(): diff --git a/src/ui/app/routes/jobs.py b/src/ui/app/routes/jobs.py index ac5cb53e4..26dac6643 100644 --- a/src/ui/app/routes/jobs.py +++ b/src/ui/app/routes/jobs.py @@ -19,6 +19,9 @@ def jobs_page(): @jobs.route("/jobs/run", methods=["POST"]) @login_required def jobs_run(): + if DB.readonly: + return handle_error("Database is in read-only mode", "jobs") + verify_data_in_form( data={"jobs": None}, err_message="Missing jobs parameter on /jobs/run.", diff --git a/src/ui/app/routes/profile.py b/src/ui/app/routes/profile.py index 10947d9d9..5b4336af5 100644 --- a/src/ui/app/routes/profile.py +++ b/src/ui/app/routes/profile.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import Dict, Generator, Tuple, Union from flask import Blueprint, Response, jsonify, redirect, render_template, request, stream_with_context, url_for, session from flask_login import current_user, login_required, logout_user @@ -35,6 +36,8 @@ DEVICES = { def get_last_sessions(page: int, per_page: int) -> Tuple[Generator[Dict[str, Union[str, bool]], None, None], int]: db_sessions = DB.get_ui_user_sessions(current_user.username, session.get("session_id")) total_sessions = len(db_sessions) + if "session_id" not in session: + total_sessions += 1 if total_sessions <= per_page: per_page = total_sessions @@ -42,17 +45,26 @@ def get_last_sessions(page: int, per_page: int) -> Tuple[Generator[Dict[str, Uni elif total_sessions <= (page - 1) * per_page: page = total_sessions // per_page - def session_generator(): - for db_session in db_sessions[(page - 1) * per_page : page * per_page]: # noqa: E203 - ua_data = parse(db_session.user_agent) + def session_generator(page: int, per_page: int): + additional_sessions = [] + if page == 1 and "session_id" not in session and per_page > 1: + per_page -= 1 + additional_sessions.append(session) + + for db_session in additional_sessions + db_sessions[(page - 1) * per_page : page * per_page]: # noqa: E203 + ua_data = parse(db_session["user_agent"]) last_session = { - "current": db_session.id == session.get("session_id"), + "current": db_session["id"] == session.get("session_id") if "session_id" in session else "id" not in db_session, "browser": ua_data.get_browser(), "os": ua_data.get_os(), "device": ua_data.get_device(), - "ip": db_session.ip, - "creation_date": db_session.creation_date.astimezone().strftime("%Y-%m-%d %H:%M:%S %Z"), - "last_activity": db_session.last_activity.astimezone().strftime("%Y-%m-%d %H:%M:%S %Z"), + "ip": db_session["ip"], + "creation_date": db_session["creation_date"].astimezone().strftime("%Y-%m-%d %H:%M:%S %Z"), + "last_activity": ( + db_session["last_activity"].astimezone().strftime("%Y-%m-%d %H:%M:%S %Z") + if "id" in db_session + else datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S %Z") + ), } for browser, icon in BROWSERS.items(): @@ -69,7 +81,7 @@ def get_last_sessions(page: int, per_page: int) -> Tuple[Generator[Dict[str, Uni yield last_session - return session_generator(), total_sessions + return session_generator(page, per_page), total_sessions @profile.route("/profile", methods=["GET"]) @@ -295,7 +307,9 @@ def wipe_old_sessions(): if not current_user.check_password(request.form["password"]): return handle_error("The current password is incorrect.", "profile") - DATA["REVOKED_SESSIONS"] = [db_session.id for db_session in DB.get_ui_user_sessions(current_user.username) if db_session.id != session.get("session_id")] + DATA["REVOKED_SESSIONS"] = [ + db_session["id"] for db_session in DB.get_ui_user_sessions(current_user.username) if db_session["id"] != session.get("session_id") + ] ret = DB.delete_ui_user_old_sessions(current_user.username) if ret: diff --git a/src/ui/app/routes/services.py b/src/ui/app/routes/services.py index 432d11b40..df471cc11 100644 --- a/src/ui/app/routes/services.py +++ b/src/ui/app/routes/services.py @@ -21,6 +21,9 @@ def services_page(): @services.route("/services/convert", methods=["POST"]) @login_required def services_convert(): + if DB.readonly: + return handle_error("Database is in read-only mode", "services") + verify_data_in_form( data={"services": None}, err_message="Missing services parameter on /services/convert.", @@ -101,6 +104,9 @@ def services_convert(): @services.route("/services/delete", methods=["POST"]) @login_required def services_delete(): + if DB.readonly: + return handle_error("Database is in read-only mode", "services") + verify_data_in_form( data={"services": None}, err_message="Missing services parameter on /services/delete.", @@ -178,6 +184,7 @@ def services_service_page(service: str): if request.method == "POST": if DB.readonly: return handle_error("Database is in read-only mode", "services") + DATA.load_from_file() # Check variables diff --git a/src/ui/app/static/css/overrides.css b/src/ui/app/static/css/overrides.css index f6cad2587..d6ebcff7b 100644 --- a/src/ui/app/static/css/overrides.css +++ b/src/ui/app/static/css/overrides.css @@ -362,11 +362,14 @@ td.highlight { } .btn-text-bw-green, +.btn-text-bw-green.disabled, .btn-text-bw-green:hover { color: var(--bs-bw-green) !important; + border: none; } .btn-text-secondary, +.btn-text-secondary.disabled, .btn-text-secondary:hover { color: var(--bs-secondary) !important; } diff --git a/src/ui/app/static/js/pages/bans.js b/src/ui/app/static/js/pages/bans.js index 975c2cc6e..cda9ec7f8 100644 --- a/src/ui/app/static/js/pages/bans.js +++ b/src/ui/app/static/js/pages/bans.js @@ -2,6 +2,7 @@ $(document).ready(function () { var actionLock = false; let addBanNumber = 1; const banNumber = parseInt($("#bans_number").val()); + const isReadOnly = $("#is-read-only").val().trim() === "True"; // Utility functions function addDays(date, days) { @@ -253,8 +254,14 @@ $(document).ready(function () { $.fn.dataTable.ext.buttons.add_ban = { text: 'Add ban(s)', - className: "btn btn-sm btn-outline-bw-green", + className: `btn btn-sm btn-outline-bw-green${ + isReadOnly ? " disabled" : "" + }`, action: function (e, dt, node, config) { + if (isReadOnly) { + alert("This action is not allowed in read-only mode."); + return; + } const ban_modal = $("#modal-ban-ips"); const modal = new bootstrap.Modal(ban_modal); modal.show(); @@ -264,6 +271,10 @@ $(document).ready(function () { $.fn.dataTable.ext.buttons.unban_ips = { text: 'Unban', action: function (e, dt, node, config) { + if (isReadOnly) { + alert("This action is not allowed in read-only mode."); + return; + } if (actionLock) { return; } @@ -346,6 +357,14 @@ $(document).ready(function () { initComplete: function (settings, json) { $("#bans_wrapper .btn-secondary").removeClass("btn-secondary"); $("#bans_wrapper th").addClass("text-center"); + if (isReadOnly) + $("#bans_wrapper .dt-buttons") + .attr( + "data-bs-original-title", + "The database is in readonly, therefore you cannot add bans.", + ) + .attr("data-bs-placement", "right") + .tooltip(); }, }); @@ -388,6 +407,10 @@ $(document).ready(function () { }); $(".unban-ip").on("click", function () { + if (isReadOnly) { + alert("This action is not allowed in read-only mode."); + return; + } $this = $(this); setupUnbanModal([ { ip: $this.data("ip"), time_remaining: $this.data("time-left") }, @@ -440,6 +463,10 @@ $(document).ready(function () { }); $(document).on("click", ".delete-ban", function () { + if (isReadOnly) { + alert("This action is not allowed in read-only mode."); + return; + } const banContainer = $(this).closest("li"); if (banContainer.attr("id") === "ban-1") return; banContainer.remove(); diff --git a/src/ui/app/static/js/pages/config_edit.js b/src/ui/app/static/js/pages/config_edit.js index 37ed5b7bc..34619120c 100644 --- a/src/ui/app/static/js/pages/config_edit.js +++ b/src/ui/app/static/js/pages/config_edit.js @@ -1,4 +1,5 @@ $(document).ready(function () { + const isReadOnly = $("#is-read-only").val().trim() === "True"; let selectedService = $("#selected-service").val().trim(); const originalService = selectedService; let selectedType = $("#selected-type").val().trim(); @@ -9,6 +10,9 @@ $(document).ready(function () { const editor = ace.edit(editorElement[0]); editor.setTheme("ace/theme/cloud9_day"); // cloud9_night when dark mode is supported + if (isReadOnly && window.location.pathname.endsWith("/new")) + window.location.href = window.location.href.split("/new")[0]; + const language = editorElement.data("language"); // TODO: Support ModSecurity if (language === "NGINX") { editor.session.setMode("ace/mode/nginx"); @@ -138,6 +142,10 @@ $(document).ready(function () { }); $(".save-config").on("click", function () { + if (isReadOnly) { + alert("This action is not allowed in read-only mode."); + return; + } const value = editor.getValue().trim(); if ( value && @@ -226,6 +234,8 @@ $(document).ready(function () { changeTypesVisibility(); $(window).on("beforeunload", function (e) { + if (isReadOnly) return; + const value = editor.getValue().trim(); if ( value && diff --git a/src/ui/app/static/js/pages/configs.js b/src/ui/app/static/js/pages/configs.js index b04f48eca..3bd37cdb1 100644 --- a/src/ui/app/static/js/pages/configs.js +++ b/src/ui/app/static/js/pages/configs.js @@ -1,6 +1,7 @@ $(document).ready(function () { var actionLock = false; const configNumber = parseInt($("#configs_number").val()); + const isReadOnly = $("#is-read-only").val().trim() === "True"; const setupDeletionModal = (configs) => { const delete_modal = $("#modal-delete-configs"); @@ -201,8 +202,14 @@ $(document).ready(function () { $.fn.dataTable.ext.buttons.create_config = { text: 'Create new custom config', - className: "btn btn-sm btn-outline-bw-green", + className: `btn btn-sm btn-outline-bw-green${ + isReadOnly ? " disabled" : "" + }`, action: function (e, dt, node, config) { + if (isReadOnly) { + alert("This action is not allowed in read-only mode."); + return; + } window.location.href = `${window.location.href}/new`; }, }; @@ -210,6 +217,10 @@ $(document).ready(function () { $.fn.dataTable.ext.buttons.delete_configs = { text: 'Delete', action: function (e, dt, node, config) { + if (isReadOnly) { + alert("This action is not allowed in read-only mode."); + return; + } if (actionLock) { return; } @@ -276,6 +287,13 @@ $(document).ready(function () { initComplete: function (settings, json) { $("#configs_wrapper .btn-secondary").removeClass("btn-secondary"); $("#configs_wrapper th").addClass("text-center"); + $("#configs_wrapper .dt-buttons") + .attr( + "data-bs-original-title", + "The database is in readonly, therefore you cannot create new custom configurations.", + ) + .attr("data-bs-placement", "right") + .tooltip(); }, }); @@ -318,6 +336,10 @@ $(document).ready(function () { }); $(".delete-config").on("click", function () { + if (isReadOnly) { + alert("This action is not allowed in read-only mode."); + return; + } const config = { name: $(this).data("config-name"), type: $(this).data("config-type"), diff --git a/src/ui/app/static/js/pages/instances.js b/src/ui/app/static/js/pages/instances.js index c10c9aad6..36d3614a5 100644 --- a/src/ui/app/static/js/pages/instances.js +++ b/src/ui/app/static/js/pages/instances.js @@ -2,6 +2,7 @@ $(document).ready(function () { var toastNum = 0; var actionLock = false; const instanceNumber = parseInt($("#instances_number").val()); + const isReadOnly = $("#is-read-only").val().trim() === "True"; const pingInstances = (instances) => { setTimeout(() => { @@ -61,6 +62,54 @@ $(document).ready(function () { }); }; + const setupDeletionModal = (instances) => { + $("#selected-instances-input").val(instances.join(",")); + + const list = $( + ``, + ); + $("#selected-instances").append(list); + + const delete_modal = $("#modal-delete-instances"); + instances.forEach((instance) => { + const list = $( + ``, + ); + + // Create the list item using template literals + const listItem = + $(`
  • +
    +
    ${instance}
    +
    +
  • `); + list.append(listItem); + + // Clone the status element and append it to the list item + const statusClone = $("#status-" + instance).clone(); + const statusListItem = $( + `
  • `, + ); + statusListItem.append(statusClone.removeClass("highlight")); + list.append(statusListItem); + + // Append the list item to the list + $("#selected-instances").append(list); + }); + + const modal = new bootstrap.Modal(delete_modal); + modal.show(); + }; + const execForm = (instances, action) => { // Create a form element using jQuery and set its attributes const form = $("
    ", { @@ -214,8 +263,14 @@ $(document).ready(function () { $.fn.dataTable.ext.buttons.create_instance = { text: 'Create new instance', - className: "btn btn-sm btn-outline-bw-green", + className: `btn btn-sm btn-outline-bw-green${ + isReadOnly ? " disabled" : "" + }`, action: function (e, dt, node, config) { + if (isReadOnly) { + alert("This action is not allowed in read-only mode."); + return; + } const modal = new bootstrap.Modal($("#modal-create-instance")); modal.show(); @@ -273,6 +328,10 @@ $(document).ready(function () { $.fn.dataTable.ext.buttons.delete_instances = { text: 'Delete', action: function (e, dt, node, config) { + if (isReadOnly) { + alert("This action is not allowed in read-only mode."); + return; + } if (actionLock) { return; } @@ -285,51 +344,7 @@ $(document).ready(function () { return; } - $("#selected-instances-input").val(instances.join(",")); - - const list = $( - ``, - ); - $("#selected-instances").append(list); - - const delete_modal = $("#modal-delete-instances"); - instances.forEach((instance) => { - const list = $( - ``, - ); - - // Create the list item using template literals - const listItem = - $(`
  • -
    -
    ${instance}
    -
    -
  • `); - list.append(listItem); - - // Clone the status element and append it to the list item - const statusClone = $("#status-" + instance).clone(); - const statusListItem = $( - `
  • `, - ); - statusListItem.append(statusClone.removeClass("highlight")); - list.append(statusListItem); - - // Append the list item to the list - $("#selected-instances").append(list); - }); - - const modal = new bootstrap.Modal(delete_modal); - modal.show(); + setupDeletionModal(instances); actionLock = false; }, @@ -403,6 +418,14 @@ $(document).ready(function () { initComplete: function (settings, json) { $("#instances_wrapper .btn-secondary").removeClass("btn-secondary"); $("#instances_wrapper th").addClass("text-center"); + if (isReadOnly) + $("#instances_wrapper .dt-buttons") + .attr( + "data-bs-original-title", + "The database is in readonly, therefore you cannot create new instances.", + ) + .attr("data-bs-placement", "right") + .tooltip(); }, }); @@ -459,4 +482,13 @@ $(document).ready(function () { const action = $(this).hasClass("reload-instance") ? "reload" : "stop"; execForm([instance], action); }); + + $(".delete-instance").on("click", function () { + if (isReadOnly) { + alert("This action is not allowed in read-only mode."); + return; + } + const instance = $(this).data("instance"); + setupDeletionModal([instance]); + }); }); diff --git a/src/ui/app/static/js/pages/jobs.js b/src/ui/app/static/js/pages/jobs.js index 0c9208e89..1a6fbb07e 100644 --- a/src/ui/app/static/js/pages/jobs.js +++ b/src/ui/app/static/js/pages/jobs.js @@ -1,6 +1,7 @@ $(document).ready(function () { let actionLock = false; const jobNumber = parseInt($("#job_number").val()); + const isReadOnly = $("#is-read-only").val().trim() === "True"; const layout = { topStart: {}, @@ -130,6 +131,10 @@ $(document).ready(function () { $.fn.dataTable.ext.buttons.run_jobs = { text: 'Run selected jobs', action: function (e, dt, node, config) { + if (isReadOnly) { + alert("This action is not allowed in read-only mode."); + return; + } if (actionLock) { return; } @@ -275,6 +280,10 @@ $(document).ready(function () { }); $(".run-job").on("click", function () { + if (isReadOnly) { + alert("This action is not allowed in read-only mode."); + return; + } const job = { name: $(this).data("job"), plugin: $(this).data("plugin"), diff --git a/src/ui/app/static/js/pages/services.js b/src/ui/app/static/js/pages/services.js index d3e86c087..2a596dd6a 100644 --- a/src/ui/app/static/js/pages/services.js +++ b/src/ui/app/static/js/pages/services.js @@ -2,6 +2,7 @@ $(document).ready(function () { var toastNum = 0; var actionLock = false; const serviceNumber = parseInt($("#services_number").val()); + const isReadOnly = $("#is-read-only").val().trim() === "True"; const setupModal = (services, modal) => { const list = $( @@ -210,14 +211,24 @@ $(document).ready(function () { $.fn.dataTable.ext.buttons.create_service = { text: 'Create new service', - className: "btn btn-sm btn-outline-bw-green", + className: `btn btn-sm btn-outline-bw-green${ + isReadOnly ? " disabled" : "" + }`, action: function (e, dt, node, config) { + if (isReadOnly) { + alert("This action is not allowed in read-only mode."); + return; + } window.location.href = `${window.location.href}/new`; }, }; $.fn.dataTable.ext.buttons.convert_services = { action: function (e, dt, node, config) { + if (isReadOnly) { + alert("This action is not allowed in read-only mode."); + return; + } if (actionLock) { return; } @@ -259,6 +270,10 @@ $(document).ready(function () { $.fn.dataTable.ext.buttons.delete_services = { text: 'Delete', action: function (e, dt, node, config) { + if (isReadOnly) { + alert("This action is not allowed in read-only mode."); + return; + } if (actionLock) { return; } @@ -341,6 +356,14 @@ $(document).ready(function () { initComplete: function (settings, json) { $("#services_wrapper .btn-secondary").removeClass("btn-secondary"); $("#services_wrapper th").addClass("text-center"); + if (isReadOnly) + $("#services_wrapper .dt-buttons") + .attr( + "data-bs-original-title", + "The database is in readonly, therefore you cannot create new services.", + ) + .attr("data-bs-placement", "right") + .tooltip(); }, }); @@ -383,12 +406,20 @@ $(document).ready(function () { }); $(".convert-service").on("click", function () { + if (isReadOnly) { + alert("This action is not allowed in read-only mode."); + return; + } const service = $(this).data("service-id"); const convertionType = $(this).data("value"); setupConversionModal([service], convertionType); }); $(".delete-service").on("click", function () { + if (isReadOnly) { + alert("This action is not allowed in read-only mode."); + return; + } const service = $(this).data("service-id"); setupDeletionModal([service]); }); diff --git a/src/ui/app/static/js/plugins-settings.js b/src/ui/app/static/js/plugins-settings.js index d1b94019c..57dddceca 100644 --- a/src/ui/app/static/js/plugins-settings.js +++ b/src/ui/app/static/js/plugins-settings.js @@ -2,6 +2,10 @@ $(document).ready(() => { var toastNum = 0; let currentPlugin = "general"; let currentStep = 1; + const isReadOnly = $("#is-read-only").val().trim() === "True"; + + if (isReadOnly && window.location.pathname.endsWith("/new")) + window.location.href = window.location.href.split("/new")[0]; const $templateInput = $("#used-template"); let usedTemplate = "advanced"; @@ -761,6 +765,11 @@ $(document).ready(() => { }); $(".save-settings").on("click", function () { + if (isReadOnly) { + alert("This action is not allowed in read-only mode."); + return; + } + const form = getFormFromSettings($(this)); if (currentMode === "easy") { const currentStepId = `navs-steps-${currentTemplate}-${currentStep}`; @@ -1064,6 +1073,11 @@ $(document).ready(() => { editor.session.setMode("ace/mode/text"); // Default mode if language is unrecognized } + const method = $(this).data("method"); + if (method !== "ui") { + editor.setReadOnly(true); + } + // Set the editor's initial content editor.setValue(initialContent, -1); // The second parameter moves the cursor to the start @@ -1079,6 +1093,8 @@ $(document).ready(() => { }); $(window).on("beforeunload", function (e) { + if (isReadOnly) return; + const form = getFormFromSettings($(this)); if (currentMode !== "easy") { let minSettings = 4; diff --git a/src/ui/app/templates/bans.html b/src/ui/app/templates/bans.html index 1825008ef..bf43065ec 100644 --- a/src/ui/app/templates/bans.html +++ b/src/ui/app/templates/bans.html @@ -50,11 +50,11 @@
    + data-bs-original-title="{% if is_readonly %}Disabled by readonly{% else %}Unban {{ ban['ip'] }}{% endif %}">
    @@ -68,118 +68,120 @@
    -
    {% endblock %} diff --git a/src/ui/app/templates/config_edit.html b/src/ui/app/templates/config_edit.html index c780d3b4b..bf6bd812e 100644 --- a/src/ui/app/templates/config_edit.html +++ b/src/ui/app/templates/config_edit.html @@ -16,7 +16,7 @@
    - {% if not config_template and config_method and config_method != "ui" %} + {% if not config_template and config_method and config_method != "ui" or is_readonly %}
    -
    +
    @@ -131,13 +137,18 @@
    -

    Loading custom configuration...

    + {% if not config_template and config_method and config_method != "ui" or is_readonly %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Disabled by {% if is_readonly %}readonly{% else %}{{ config_method }}{% endif %} + " + {% endif %}> +

    Loading custom configuration...

    {{ config_value }}
    + class="visually-hidden ace-editor border rounded position-absolute top-0 start-0 end-0 bottom-0"> + {{ config_value }} +
    {% endblock %} diff --git a/src/ui/app/templates/configs.html b/src/ui/app/templates/configs.html index b61f7e43b..53b7bec96 100644 --- a/src/ui/app/templates/configs.html +++ b/src/ui/app/templates/configs.html @@ -69,7 +69,7 @@  {{ config["name"] }} + data-bs-original-title="{% if config['method'] != 'ui' or is_readonly %}View{% else %}Edit{% endif %} custom config {{ config['name'] }}"> {{ config["name"] }} @@ -101,25 +101,27 @@ href="{{ url_for("configs") }}/{{ service_id }}/{{ config['type'] }}/{{ config['name'] }}" data-bs-toggle="tooltip" data-bs-placement="bottom" - data-bs-original-title="Edit custom config {{ config['name'] }}"> - - - - + data-bs-original-title="{% if is_readonly %}View{% else %}Edit{% endif %} custom config {{ config['name'] }}"> + +
    + + + +
    + data-bs-original-title="{% if config['method'] != 'ui' or is_readonly %}Disabled by {% if is_readonly %}readonly{% else %}{{ config['method'] }}{% endif %}{% else %}Delete custom config {{ config['name'] }}{% endif %}">
    @@ -130,40 +132,42 @@ - -