Add readonly specifications to ui routes

This commit is contained in:
Théophile Diot 2024-09-23 12:00:51 +02:00
parent 1263484e52
commit 33eea02c6e
No known key found for this signature in database
GPG key ID: FA995104A0BA376A
31 changed files with 852 additions and 574 deletions

View file

@ -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."""

View file

@ -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.",

View file

@ -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

View file

@ -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():

View file

@ -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.",

View file

@ -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:

View file

@ -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

View file

@ -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;
}

View file

@ -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: '<span class="tf-icons bx bx-plus-circle bx-18px me-2"></span>Add<span class="d-none d-md-inline"> ban(s)</span>',
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: '<span class="tf-icons bx bxs-buoy bx-18px me-2"></span>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();

View file

@ -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 &&

View file

@ -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: '<span class="tf-icons bx bx-plus-circle bx-18px me-2"></span>Create<span class="d-none d-md-inline"> new custom config</span>',
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: '<span class="tf-icons bx bx-trash bx-18px me-2"></span>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"),

View file

@ -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 = $(
`<ul class="list-group list-group-horizontal d-flex w-100">
<li class="list-group-item align-items-center text-center bg-secondary text-white" style="flex: 1 0;">
<div class="ms-2 me-auto">
<div class="fw-bold">Hostname</div>
</div>
</li>
<li class="list-group-item align-items-center text-center bg-secondary text-white" style="flex: 1 0;">
<div class="fw-bold">Health</div>
</li>
</ul>`,
);
$("#selected-instances").append(list);
const delete_modal = $("#modal-delete-instances");
instances.forEach((instance) => {
const list = $(
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`,
);
// Create the list item using template literals
const listItem =
$(`<li class="list-group-item align-items-center" style="flex: 1 0;">
<div class="ms-2 me-auto">
<div class="fw-bold">${instance}</div>
</div>
</li>`);
list.append(listItem);
// Clone the status element and append it to the list item
const statusClone = $("#status-" + instance).clone();
const statusListItem = $(
`<li class="list-group-item d-flex align-items-center justify-content-center" style="flex: 1 0;"></li>`,
);
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 = $("<form>", {
@ -214,8 +263,14 @@ $(document).ready(function () {
$.fn.dataTable.ext.buttons.create_instance = {
text: '<span class="tf-icons bx bx-plus-circle bx-18px me-2"></span>Create<span class="d-none d-md-inline"> new instance</span>',
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: '<span class="tf-icons bx bx-trash bx-18px me-2"></span>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 = $(
`<ul class="list-group list-group-horizontal d-flex w-100">
<li class="list-group-item align-items-center text-center bg-secondary text-white" style="flex: 1 0;">
<div class="ms-2 me-auto">
<div class="fw-bold">Hostname</div>
</div>
</li>
<li class="list-group-item align-items-center text-center bg-secondary text-white" style="flex: 1 0;">
<div class="fw-bold">Health</div>
</li>
</ul>`,
);
$("#selected-instances").append(list);
const delete_modal = $("#modal-delete-instances");
instances.forEach((instance) => {
const list = $(
`<ul class="list-group list-group-horizontal d-flex w-100"></ul>`,
);
// Create the list item using template literals
const listItem =
$(`<li class="list-group-item align-items-center" style="flex: 1 0;">
<div class="ms-2 me-auto">
<div class="fw-bold">${instance}</div>
</div>
</li>`);
list.append(listItem);
// Clone the status element and append it to the list item
const statusClone = $("#status-" + instance).clone();
const statusListItem = $(
`<li class="list-group-item d-flex align-items-center justify-content-center" style="flex: 1 0;"></li>`,
);
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]);
});
});

View file

@ -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: '<span class="tf-icons bx bx-play bx-18px me-2"></span>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"),

View file

@ -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: '<span class="tf-icons bx bx-plus-circle bx-18px me-2"></span>Create<span class="d-none d-md-inline"> new service</span>',
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: '<span class="tf-icons bx bx-trash bx-18px me-2"></span>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]);
});

View file

@ -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;

View file

@ -50,11 +50,11 @@
<div class="d-flex justify-content-center">
<div data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-original-title="Unban {{ ban['ip'] }}">
data-bs-original-title="{% if is_readonly %}Disabled by readonly{% else %}Unban {{ ban['ip'] }}{% endif %}">
<button type="button"
data-ip="{{ ban['ip'] }}"
data-time-left="{{ ban['remain'] }}"
class="btn btn-outline-danger btn-sm me-1 unban-ip">
class="btn btn-outline-danger btn-sm me-1 unban-ip{% if is_readonly %} disabled{% endif %}">
<i class="bx bxs-buoy bx-xs"></i>
</button>
</div>
@ -68,118 +68,120 @@
</span>
</table>
</div>
<div class="modal modal-xl fade"
id="modal-ban-ips"
data-bs-backdrop="static"
tabindex="-1"
aria-hidden="true"
role="dialog">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Ban(s)</h5>
<button id="add-ban"
type="button"
class="btn btn-text-bw-green rounded-pill p-0 ms-4 me-2">
<i class="bx bx-plus-circle"></i>&nbsp;INSERT
</button>
<button id="clear-bans"
type="button"
class="btn btn-text-danger rounded-pill p-0 ms-4">
<i class="bx bx-trash"></i>&nbsp;CLEAR
</button>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<form id="bans-form" action="{{ url_for("bans") }}/ban" method="POST">
<div class="modal-body justify-content-center">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<ul id="bans-container" class="list-group rounded-top w-100">
<li id="bans-header" class="list-group-item bg-secondary text-white">
<div class="row">
<div class="col-3 text-center fw-bold">IP Address</div>
<div class="col-5 border-start text-center fw-bold">End Date</div>
<div class="col-3 border-start text-center fw-bold">Reason</div>
<div class="col-1 border-start text-center fw-bold">Delete</div>
</div>
</li>
<li id="ban-1" class="list-group-item rounded-0">
<div class="row align-items-center d-flex">
<div class="col-3">
<input type="text"
name="ip"
class="form-control"
placeholder="127.0.0.1"
required />
</div>
<div class="col-5 border-start">
<input type="flapickr-datetime"
name="datetime"
class="form-control"
required />
</div>
<div class="col-3 border-start">
<input type="text" name="reason" class="form-control" value="ui" required />
</div>
<div class="col-1 border-start align-items-center d-flex"
data-bs-toggle="tooltip"
data-bs-placement="right"
data-bs-original-title="Can't delete the original Ban">
<button type="button"
class="btn btn-outline-danger btn-sm me-1 delete-ban disabled">
<i class="bx bx-trash bx-xs"></i>
</button>
</div>
</div>
</li>
</ul>
{% if not is_readonly %}
<div class="modal modal-xl fade"
id="modal-ban-ips"
data-bs-backdrop="static"
tabindex="-1"
aria-hidden="true"
role="dialog">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Ban(s)</h5>
<button id="add-ban"
type="button"
class="btn btn-text-bw-green rounded-pill p-0 ms-4 me-2">
<i class="bx bx-plus-circle"></i>&nbsp;INSERT
</button>
<button id="clear-bans"
type="button"
class="btn btn-text-danger rounded-pill p-0 ms-4">
<i class="bx bx-trash"></i>&nbsp;CLEAR
</button>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-footer justify-content-center">
<button type="submit" class="btn btn-outline-danger me-2">Ban</button>
<button type="reset"
class="btn btn-outline-secondary"
data-bs-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal modal-lg fade"
id="modal-unban-ips"
data-bs-backdrop="static"
tabindex="-1"
aria-hidden="true"
role="dialog">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm Unban</h5>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<form action="{{ url_for("bans") }}/unban" method="POST">
<div class="modal-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" id="selected-ips-input-unban" name="ips" value="" />
<div class="alert alert-danger text-center" role="alert">
Are you sure you want to unban the selected IP addresses?
<form id="bans-form" action="{{ url_for("bans") }}/ban" method="POST">
<div class="modal-body justify-content-center">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<ul id="bans-container" class="list-group rounded-top w-100">
<li id="bans-header" class="list-group-item bg-secondary text-white">
<div class="row">
<div class="col-3 text-center fw-bold">IP Address</div>
<div class="col-5 border-start text-center fw-bold">End Date</div>
<div class="col-3 border-start text-center fw-bold">Reason</div>
<div class="col-1 border-start text-center fw-bold">Delete</div>
</div>
</li>
<li id="ban-1" class="list-group-item rounded-0">
<div class="row align-items-center d-flex">
<div class="col-3">
<input type="text"
name="ip"
class="form-control"
placeholder="127.0.0.1"
required />
</div>
<div class="col-5 border-start">
<input type="flapickr-datetime"
name="datetime"
class="form-control"
required />
</div>
<div class="col-3 border-start">
<input type="text" name="reason" class="form-control" value="ui" required />
</div>
<div class="col-1 border-start align-items-center d-flex"
data-bs-toggle="tooltip"
data-bs-placement="right"
data-bs-original-title="Can't delete the original Ban">
<button type="button"
class="btn btn-outline-danger btn-sm me-1 delete-ban disabled">
<i class="bx bx-trash bx-xs"></i>
</button>
</div>
</div>
</li>
</ul>
</div>
<ul id="selected-ips-unban" class="list-group w-100 mb-3">
</ul>
</div>
<div class="modal-footer justify-content-center">
<button type="submit" class="btn btn-outline-danger me-2">Unban</button>
<button type="reset"
class="btn btn-outline-secondary"
data-bs-dismiss="modal">Cancel</button>
</div>
</form>
<div class="modal-footer justify-content-center">
<button type="submit" class="btn btn-outline-danger me-2">Ban</button>
<button type="reset"
class="btn btn-outline-secondary"
data-bs-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="modal modal-lg fade"
id="modal-unban-ips"
data-bs-backdrop="static"
tabindex="-1"
aria-hidden="true"
role="dialog">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm Unban</h5>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<form action="{{ url_for("bans") }}/unban" method="POST">
<div class="modal-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" id="selected-ips-input-unban" name="ips" value="" />
<div class="alert alert-danger text-center" role="alert">
Are you sure you want to unban the selected IP addresses?
</div>
<ul id="selected-ips-unban" class="list-group w-100 mb-3">
</ul>
</div>
<div class="modal-footer justify-content-center">
<button type="submit" class="btn btn-outline-danger me-2">Unban</button>
<button type="reset"
class="btn btn-outline-secondary"
data-bs-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
<!-- / Content -->
{% endblock %}

View file

@ -100,6 +100,7 @@
nonce="{{ script_nonce }}"></script>
</head>
<body>
<input type="hidden" id="is-read-only" value="{{ is_readonly }}" />
<!-- prettier-ignore -->
{% if current_endpoint != "loading" %}
{% include "flash.html" %}

View file

@ -17,9 +17,12 @@
</a>
</div>
</div>
<p id="cache-waiting" class="text-center relative w-full p-2 text-primary rounded-lg fw-bold">Loading cache file...</p>
<p id="cache-waiting"
class="text-center relative w-full p-2 text-primary rounded-lg fw-bold">Loading cache file...</p>
<div id="cache-value"
class="visually-hidden ace-editor border rounded position-absolute top-0 start-0 end-0 bottom-0">{{ cache_file }}</div>
class="visually-hidden ace-editor border rounded position-absolute top-0 start-0 end-0 bottom-0">
{{ cache_file }}
</div>
</div>
<!-- / Content -->
{% endblock %}

View file

@ -16,7 +16,7 @@
<div class="card p-1 mb-4 sticky-card">
<div class="d-flex flex-wrap justify-content-around align-items-center">
<div class="d-flex">
{% 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 %}
<button type="button" class="btn btn-sm btn-secondary ms-2 disabled">
<i class="bx bx-xs bx-cube"></i>
&nbsp;Service: {{ config_service or "no service" }}
@ -115,15 +115,21 @@
value="{{ name }}"
pattern="^[a-zA-Z0-9_\-]{1,64}$"
required
{% if not config_template and config_method and config_method != "ui" %}disabled{% endif %}>
{% if not config_template and config_method and config_method != "ui" or is_readonly %}disabled{% endif %}>
<label for="config-name">Configuration Name</label>
<span class="input-group-text border-0 border-primary border-bottom mt-2 pb-0 shadow-none"
id="config-name-suffix">.conf</span>
</div>
</div>
<div {% if not config_template and config_method and config_method != "ui" %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="The custom config was created using the {{ config_method }} method, therefore it is locked"{% endif %}>
<div {% 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="{% if is_readonly %}The database is in readonly{% else %}The custom config was created using the {{ config_method }} method{% endif %}
therefore
it
is
locked
"
{% endif %}>
<button type="button"
class="btn btn-sm btn-outline-bw-green save-config{% if not config_template and config_method and config_method != "ui" %} disabled{% endif %}">
class="btn btn-sm btn-outline-bw-green save-config{% if not config_template and config_method and config_method != "ui" or is_readonly %} disabled{% endif %}">
<i class="bx bx-save bx-sm"></i>
<span class="d-none d-md-inline">&nbsp;Save</span>
</button>
@ -131,13 +137,18 @@
</div>
</div>
<div class="card position-relative p-4 min-vh-70"
{% if not config_template and config_method and config_method != "ui" %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Disabled by {{ config_method }}"{% endif %}>
<p id="config-waiting" class="text-center relative w-full p-2 text-primary rounded-lg fw-bold">Loading custom configuration...</p>
{% 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 %}>
<p id="config-waiting"
class="text-center relative w-full p-2 text-primary rounded-lg fw-bold">Loading custom configuration...</p>
<div id="config-value"
data-language="{% if type and type.startswith(('CRS', 'MODSEC')) %}ModSecurity{% else %}NGINX{% endif %}"
data-method="{{ config_method or 'ui' }}"
data-template="{{ config_template }}"
class="visually-hidden ace-editor border rounded position-absolute top-0 start-0 end-0 bottom-0">{{ config_value }}</div>
class="visually-hidden ace-editor border rounded position-absolute top-0 start-0 end-0 bottom-0">
{{ config_value }}
</div>
</div>
<!-- / Content -->
{% endblock %}

View file

@ -69,7 +69,7 @@
<a 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'] }}"><i class="bx bx-edit bx-xs"></i>&nbsp;{{ config["name"] }}</a>
data-bs-original-title="{% if config['method'] != 'ui' or is_readonly %}View{% else %}Edit{% endif %} custom config {{ config['name'] }}"><i class="bx bx-{% if config['method'] != 'ui' or is_readonly %}show{% else %}edit{% endif %} bx-xs"></i>&nbsp;{{ config["name"] }}</a>
</td>
<td id="type-{{ config['type'] }}-{{ service_id.replace('.', '_') }}-{{ config['name'] }}">
<i class="bx bx-{{ config_icon }}"></i>
@ -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'] }}">
<i class="bx bx-edit bx-xs"></i>
</a>
<a role="button"
class="btn btn-outline-secondary btn-sm me-1"
href="{{ url_for("configs") }}/new?clone={{ service_id }}/{{ config['type'] }}/{{ config['name'] }}"
data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-original-title="Clone custom config {{ config['name'] }}">
<i class="bx bx-copy-alt bx-xs"></i>
data-bs-original-title="{% if is_readonly %}View{% else %}Edit{% endif %} custom config {{ config['name'] }}">
<i class="bx bx-{% if is_readonly %}show{% else %}edit{% endif %} bx-xs"></i>
</a>
<div {% if is_readonly %}data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-original-title="Disabled by readonly"{% endif %}>
<a role="button"
class="btn btn-outline-secondary btn-sm me-1{% if is_readonly %} disabled{% endif %}"
href="{{ url_for("configs") }}/new?clone={{ service_id }}/{{ config['type'] }}/{{ config['name'] }}"
data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-original-title="Clone custom config {{ config['name'] }}">
<i class="bx bx-copy-alt bx-xs"></i>
</a>
</div>
<div data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-original-title="{% if config['method'] != 'ui' %}Disabled by {% if config['template'] %}template: {{ config['template'] }}{% else %}{{ config['method'] }}{% endif %}{% else %}Delete custom config {{ config['name'] }}{% endif %}">
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 %}">
<button type="button"
data-config-name="{{ config['name'] }}"
data-config-type="{{ config['type'] }}"
data-config-service="{{ config['service_id'] or 'global' }}"
class="btn btn-outline-danger btn-sm me-1 delete-config{% if config['method'] != 'ui' %} disabled{% endif %}">
class="btn btn-outline-danger btn-sm me-1 delete-config{% if config['method'] != 'ui' or is_readonly %} disabled{% endif %}">
<i class="bx bx-trash bx-xs"></i>
</button>
</div>
@ -130,40 +132,42 @@
</tbody>
</table>
</div>
<div class="modal modal-lg fade"
id="modal-delete-configs"
data-bs-backdrop="static"
tabindex="-1"
aria-hidden="true"
role="dialog">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm deletion</h5>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
{% if not is_readonly %}
<div class="modal modal-lg fade"
id="modal-delete-configs"
data-bs-backdrop="static"
tabindex="-1"
aria-hidden="true"
role="dialog">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm deletion</h5>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<form action="{{ url_for("configs") }}/delete" method="POST">
<div class="modal-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden"
id="selected-configs-input-delete"
name="configs"
value="" />
<div class="alert alert-danger text-center" role="alert">Are you sure you want to delete the selected configs?</div>
<div id="selected-configs-delete" class="mb-3"></div>
</div>
<div class="modal-footer justify-content-center">
<button type="submit" class="btn btn-outline-danger me-2">Delete</button>
<button type="reset"
class="btn btn-outline-secondary"
data-bs-dismiss="modal">Cancel</button>
</div>
</form>
</div>
<form action="{{ url_for("configs") }}/delete" method="POST">
<div class="modal-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden"
id="selected-configs-input-delete"
name="configs"
value="" />
<div class="alert alert-danger text-center" role="alert">Are you sure you want to delete the selected configs?</div>
<div id="selected-configs-delete" class="mb-3"></div>
</div>
<div class="modal-footer justify-content-center">
<button type="submit" class="btn btn-outline-danger me-2">Delete</button>
<button type="reset"
class="btn btn-outline-secondary"
data-bs-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
<!-- / Content -->
{% endblock %}

View file

@ -141,10 +141,10 @@
<span class="d-none d-md-inline">Notifications</span>
</button>
{% if flash_messages %}
<span class="badge-dot-text position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
{{ flash_messages|length }}
<span class="visually-hidden">unread notifications</span>
</span>
<span class="badge-dot-text position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
{{ flash_messages|length }}
<span class="visually-hidden">unread notifications</span>
</span>
{% endif %}
</li>
<li class="position-relative">

View file

@ -69,7 +69,7 @@
<td>
{% if instance.status == "up" %}
<span id="status-{{ instance.hostname }}"
class="badge rounded-pill bg-label-success">Up</span>
class="badge rounded-pill bg-label-primary">Up</span>
{% elif instance.status == "down" %}
<span id="status-{{ instance.hostname }}"
class="badge rounded-pill bg-label-danger">Down</span>
@ -119,10 +119,10 @@
</div>
<div data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-original-title="{% if instance['method'] != 'ui' %}Disabled by {{ instance['method'] }}{% else %}Delete instance {{ instance['hostname'] }}{% endif %}">
data-bs-original-title="{% if instance['method'] != 'ui' or is_readonly %}Disabled by {% if is_readonly %}readonly{% else %}{{ instance['method'] }}{% endif %}{% else %}Delete instance {{ instance['hostname'] }}{% endif %}">
<button type="button"
data-instance="{{ instance['hostname'] }}"
class="btn btn-outline-danger btn-sm me-1 delete-instance{% if instance['method'] != 'ui' %} disabled{% endif %}">
class="btn btn-outline-danger btn-sm me-1 delete-instance{% if instance['method'] != 'ui' or is_readonly %} disabled{% endif %}">
<i class="bx bx-trash bx-xs"></i>
</button>
</div>
@ -153,89 +153,91 @@
</div>
<div class="toast-body">If you read this, it means that you're curious 👀</div>
</div>
<div class="modal fade"
id="modal-create-instance"
data-bs-backdrop="static"
tabindex="-1"
aria-hidden="true"
role="dialog">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Create new instance</h5>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
{% if not is_readonly %}
<div class="modal fade"
id="modal-create-instance"
data-bs-backdrop="static"
tabindex="-1"
aria-hidden="true"
role="dialog">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Create new instance</h5>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<form action="{{ url_for("instances") }}/new" method="POST">
<div class="modal-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="mb-3">
<label for="hostname" class="form-label">Hostname</label>
<input type="text"
class="form-control"
id="hostname"
name="hostname"
placeholder="http://bunkerweb"
maxlength="256"
required />
</div>
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text"
class="form-control"
id="name"
name="name"
placeholder="My Bunker"
maxlength="256"
required />
</div>
<div class="alert alert-primary text-center" role="alert">
You don't need to provide the port or the server_name as the values of both <code>API_HTTP_PORT</code> and <code>API_SERVER_NAME</code> will be used for the instance configuration.
</div>
</div>
<div class="modal-footer justify-content-center">
<button type="submit" class="btn btn-outline-primary me-2">Create Instance</button>
<button type="reset"
class="btn btn-outline-secondary"
data-bs-dismiss="modal">Close</button>
</div>
</form>
</div>
<form action="{{ url_for("instances") }}/new" method="POST">
<div class="modal-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="mb-3">
<label for="hostname" class="form-label">Hostname</label>
<input type="text"
class="form-control"
id="hostname"
name="hostname"
placeholder="http://bunkerweb"
maxlength="256"
required />
</div>
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text"
class="form-control"
id="name"
name="name"
placeholder="My Bunker"
maxlength="256"
required />
</div>
<div class="alert alert-primary text-center" role="alert">
You don't need to provide the port or the server_name as the values of both <code>API_HTTP_PORT</code> and <code>API_SERVER_NAME</code> will be used for the instance configuration.
</div>
</div>
<div class="modal-footer justify-content-center">
<button type="submit" class="btn btn-outline-primary me-2">Create Instance</button>
<button type="reset"
class="btn btn-outline-secondary"
data-bs-dismiss="modal">Close</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade"
id="modal-delete-instances"
data-bs-backdrop="static"
tabindex="-1"
aria-hidden="true"
role="dialog">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete instances</h5>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
<div class="modal fade"
id="modal-delete-instances"
data-bs-backdrop="static"
tabindex="-1"
aria-hidden="true"
role="dialog">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete instances</h5>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<form action="{{ url_for("instances") }}/delete" method="POST">
<div class="modal-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" id="selected-instances-input" name="instances" value="" />
<div class="alert alert-danger text-center" role="alert">Are you sure you want to delete the selected instances?</div>
<div id="selected-instances" class="mb-3"></div>
</div>
<div class="modal-footer justify-content-center">
<button type="submit" class="btn btn-outline-danger me-2">Delete instances</button>
<button type="reset"
class="btn btn-outline-secondary"
data-bs-dismiss="modal">Close</button>
</div>
</form>
</div>
<form action="{{ url_for("instances") }}/delete" method="POST">
<div class="modal-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" id="selected-instances-input" name="instances" value="" />
<div class="alert alert-danger text-center" role="alert">Are you sure you want to delete the selected instances?</div>
<div id="selected-instances" class="mb-3"></div>
</div>
<div class="modal-footer justify-content-center">
<button type="submit" class="btn btn-outline-danger me-2">Delete instances</button>
<button type="reset"
class="btn btn-outline-secondary"
data-bs-dismiss="modal">Close</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
<!-- / Content -->
{% endblock %}

View file

@ -44,7 +44,7 @@
<td>{{ job }}</td>
<td>{{ job_data["plugin_id"] }}</td>
<td>
<i class="bx {% if job_data['every'] == 'once' %}bx-check-square{% elif job_data['every'] == 'day' %}bx-calendar-event{% elif job_data['every'] == 'week' %}bx-calendar-week{% else %}bxs-hourglass{% endif %}"></i>&nbsp;{{ job_data["every"] }}
<i class="bx {% if job_data['every'] == 'once' %}bx-revision{% elif job_data['every'] == 'day' %}bx-calendar-event{% elif job_data['every'] == 'week' %}bx-calendar-week{% else %}bxs-hourglass{% endif %}"></i>&nbsp;{{ job_data["every"] }}
</td>
<td class="text-center">
<i class="bx bx-sm bx-{% if job_data['reload'] %}check-circle text-success{% else %}x-circle text-danger{% endif %}"></i>
@ -60,11 +60,11 @@
<div class="d-flex justify-content-center">
<div data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-original-title="Run the Job">
data-bs-original-title="{% if is_readonly %}Disabled by readonly{% else %}Run the Job{% endif %}">
<button type="button"
data-job="{{ job }}"
data-plugin="{{ job_data['plugin_id'] }}"
class="btn btn-primary btn-sm me-1 run-job">
class="btn btn-primary btn-sm me-1 run-job{% if is_readonly %} disabled{% endif %}">
<i class="bx bx-play bx-xs"></i>&nbsp;Run
</button>
</div>

View file

@ -65,9 +65,12 @@
</div>
</div>
<div class="card p-4 min-vh-70">
<p id="logs-waiting" class="text-center relative w-full p-2 text-primary rounded-lg fw-bold">Loading logs...</p>
<p id="logs-waiting"
class="text-center relative w-full p-2 text-primary rounded-lg fw-bold">Loading logs...</p>
<div id="raw-logs"
class="visually-hidden ace-editor border rounded position-absolute top-0 start-0 end-0 bottom-0">{{ logs }}</div>
class="visually-hidden ace-editor border rounded position-absolute top-0 start-0 end-0 bottom-0">
{{ logs }}
</div>
</div>
<!-- / Content -->
{% endblock %}

View file

@ -65,7 +65,7 @@
</div>
<div class="d-flex justify-content-center">
{% if current_endpoint != "global-config" %}
<div {% if current_endpoint != 'new' and service_method != 'ui' %}data-bs-toggle="tooltip" data-bs-placement="top" title="The draft mode can only be toggled on UI created services"{% endif %}>
<div {% if current_endpoint != 'new' and service_method != 'ui' %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="The draft mode can only be toggled on UI created services"{% endif %}>
<button type="button"
class="btn btn-sm btn-outline-secondary toggle-draft me-3 {% if current_endpoint != 'new' and service_method != 'ui' %}disabled{% endif %}">
<i class="bx bx-sm bx-{% if is_draft == 'yes' %}file-blank{% else %}globe{% endif %}"></i>
@ -79,9 +79,16 @@
</button>
</div>
{% endif %}
<div {% if service_method == "autoconf" %}data-bs-toggle="tooltip" data-bs-placement="top" title="The service was created using the autoconf method, therefore the configuration is locked"{% endif %}>
<div {% if service_method == "autoconf" or is_readonly %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="{% if is_readonly %}The database is in readonly{% else %}The service was created using the autoconf method{% endif %}
therefore
the
configuration
is
locked
"
{% endif %}>
<button type="button"
class="btn btn-sm btn-outline-bw-green save-settings {% if service_method == "autoconf" %}disabled{% endif %}">
class="btn btn-sm btn-outline-bw-green save-settings {% if service_method == "autoconf" or is_readonly %}disabled{% endif %}">
<i class="bx bx-save bx-sm"></i>
<span class="d-none d-md-inline">&nbsp;Save</span>
</button>
@ -115,7 +122,7 @@
rel="noopener"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="{% if plugin_data['stream'] != 'no' %}Supports{% else %}Doesn't support{% endif %} STREAM mode{% if plugin_data['stream'] == 'partial' %} partially{% endif %}">
data-bs-original-title="{% if plugin_data['stream'] != 'no' %}Supports{% else %}Doesn't support{% endif %} STREAM mode{% if plugin_data['stream'] == 'partial' %} partially{% endif %}">
<i class="bx bx-{% if plugin_data['stream'] == 'yes' %}badge-check{% elif plugin_data['stream'] == 'partial' %}message-square-detail{% else %}no-entry{% endif %}"></i>&nbsp;STREAM
</a>
<a href="{% if plugin_data['type'] == 'core' %}https://docs.bunkerweb.io/latest/settings/?utm_campaign=self&utm_source=ui#{% if plugin == 'general' %}global-settings{% else %}{{ plugin }}{% endif %}{% else %}https://docs.bunkerweb.io/latest/plugins/?utm_campaign=self&utm_source=ui{% endif %}"
@ -151,8 +158,12 @@
{% set setting_method = "autoconf" %}
{% set disabled = true %}
{% endif %}
{% if is_readonly %}
{% set disabled = true %}
{% set setting_method = "readonly" %}
{% endif %}
<div class="col-12 col-sm-6 col-lg-4 pb-3"
{% if disabled %}data-bs-toggle="tooltip" data-bs-placement="top" title="Disabled by {{ setting_method }}"{% endif %}>
{% if disabled %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Disabled by {{ setting_method }}"{% endif %}>
<div class="d-flex justify-content-between align-items-center">
<label id="label-setting-{{ plugin }}-{{ setting_data['id'] }}"
for="setting-{{ plugin }}-{{ setting_data['id'] }}"
@ -164,7 +175,7 @@
class="badge badge-center rounded-pill bg-secondary d-flex align-items-center justify-content-center p-1 me-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Multisite setting"
data-bs-original-title="Multisite setting"
target="_blank"
rel="noopener">
<span class="bx bx-server bx-xs"></span>
@ -174,7 +185,7 @@
<span class="badge badge-center rounded-pill bg-secondary d-flex align-items-center justify-content-center p-1 me-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="From template: {{ setting_template }}">
data-bs-original-title="From template: {{ setting_template }}">
<span class="bx bx-spreadsheet bx-xs"></span>
</span>
{% endif %}
@ -182,14 +193,14 @@
<span class="badge badge-center rounded-pill bg-primary-subtle text-dark d-flex align-items-center justify-content-center p-1 me-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="From global configuration">
data-bs-original-title="From global configuration">
<span class="bx bx-globe bx-xs"></span>
</span>
{% endif %}
<span class="badge rounded-pill bg-secondary-subtle text-dark d-flex align-items-center justify-content-center p-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="{{ setting_data['help'] }}">
data-bs-original-title="{{ setting_data['help'] }}">
<span class="bx bx-question-mark bx-xs"></span>
</span>
</div>
@ -235,19 +246,27 @@
</h6>
<div class="d-flex align-items-center">
{% if setting_suffix == "0" %}
<button id="add-multiple-{{ plugin }}-{{ multiple }}"
type="button"
class="btn btn-xs btn-text-bw-green rounded-pill add-multiple p-0 pe-2">
<i class="bx bx-plus-circle bx-sm"></i>&nbsp;ADD
</button>
{% else %}
<div>
<button id="remove-multiple-{{ plugin }}-{{ multiple }}-{{ setting_suffix }}"
<div class="me-1"
{% if service_method == "autoconf" or is_readonly %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="{% if is_readonly %}The database is in readonly{% else %}The service was created using the autoconf method{% endif %}
therefore
multiple
settings
are
locked
"
{% endif %}>
<button id="add-multiple-{{ plugin }}-{{ multiple }}"
type="button"
class="btn btn-xs btn-text-danger rounded-pill remove-multiple p-0 pe-2">
<i class="bx bx-trash bx-sm"></i>&nbsp;REMOVE
class="btn btn-xs btn-text-bw-green rounded-pill add-multiple p-0 pe-2{% if service_method == "autoconf" or is_readonly %} disabled{% endif %}">
<i class="bx bx-plus-circle bx-sm"></i>&nbsp;ADD
</button>
</div>
{% else %}
<button id="remove-multiple-{{ plugin }}-{{ multiple }}-{{ setting_suffix }}"
type="button"
class="btn btn-xs btn-text-danger rounded-pill remove-multiple p-0 pe-2">
<i class="bx bx-trash bx-sm"></i>&nbsp;REMOVE
</button>
{% endif %}
<button id="show-multiple-{{ plugin }}-{{ multiple }}-{{ setting_suffix }}"
type="button"
@ -277,8 +296,12 @@
{% set setting_method = "autoconf" %}
{% set disabled = true %}
{% endif %}
{% if is_readonly %}
{% set disabled = true %}
{% set setting_method = "readonly" %}
{% endif %}
<div class="col-12{% if multiple_settings %} col-md-6{% endif %}{% if settings|length > 2 and not multiple_multiples %} col-lg-4{% endif %} pb-2"
{% if disabled %}data-bs-toggle="tooltip" data-bs-placement="top" title="Disabled by {{ setting_method }}"{% endif %}>
{% if disabled %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Disabled by {{ setting_method }}"{% endif %}>
<div class="d-flex justify-content-between align-items-center">
<label id="label-multiple-setting-{{ plugin }}-{{ setting_data['id'] }}-{{ setting_suffix }}"
for="multiple-setting-{{ plugin }}-{{ setting_data['id'] }}-{{ setting_suffix }}"
@ -292,7 +315,7 @@
class="badge badge-center rounded-pill bg-secondary d-flex align-items-center justify-content-center p-1 me-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Multisite setting"
data-bs-original-title="Multisite setting"
target="_blank"
rel="noopener">
<span class="bx bx-server bx-xs"></span>
@ -302,7 +325,7 @@
<span class="badge badge-center rounded-pill bg-secondary d-flex align-items-center justify-content-center p-1 me-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="From template: {{ setting_template }}">
data-bs-original-title="From template: {{ setting_template }}">
<span class="bx bx-spreadsheet bx-xs"></span>
</span>
{% endif %}
@ -310,14 +333,14 @@
<span class="badge badge-center rounded-pill bg-primary-subtle text-dark d-flex align-items-center justify-content-center p-1 me-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="From global configuration">
data-bs-original-title="From global configuration">
<span class="bx bx-globe bx-xs"></span>
</span>
{% endif %}
<span class="badge rounded-pill bg-secondary-subtle text-dark d-flex align-items-center justify-content-center p-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="{{ setting_data['help'] }}">
data-bs-original-title="{{ setting_data['help'] }}">
<span class="bx bx-question-mark bx-xs"></span>
</span>
</div>

View file

@ -39,7 +39,7 @@
</div>
<div class="d-flex justify-content-center">
{% if current_endpoint != "global-config" %}
<div {% if current_endpoint != 'new' and service_method != 'ui' %}data-bs-toggle="tooltip" data-bs-placement="top" title="The draft mode can only be toggled on UI created services"{% endif %}>
<div {% if current_endpoint != 'new' and service_method != 'ui' %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="The draft mode can only be toggled on UI created services"{% endif %}>
<button type="button"
class="btn btn-sm btn-outline-secondary toggle-draft me-3 {% if current_endpoint != 'new' and service_method != 'ui' %}disabled{% endif %}">
<i class="bx bx-sm bx-{% if is_draft == 'yes' %}file-blank{% else %}globe{% endif %}"></i>
@ -58,7 +58,7 @@
class="btn btn-sm btn-outline-danger me-3"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Reset the current template settings">
data-bs-original-title="Reset the current template settings">
<i class="bx bx-sm bx-reset"></i>
<span class="d-none d-md-inline">&nbsp;
Reset
@ -141,8 +141,12 @@
{% set setting_method = "autoconf" %}
{% set disabled = true %}
{% endif %}
{% if is_readonly %}
{% set disabled = true %}
{% set setting_method = "readonly" %}
{% endif %}
<div class="col-12 col-sm-6 col-lg-4 pb-3"
{% if disabled %}data-bs-toggle="tooltip" data-bs-placement="top" title="Disabled by {{ setting_method }}"{% endif %}>
{% if disabled %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Disabled by {{ setting_method }}"{% endif %}>
<div class="d-flex justify-content-between align-items-center">
<label id="label-{{ template }}-setting-{{ template_data['plugin_id'] }}-{{ setting_data['id'] }}"
for="{{ template }}-setting-{{ template_data['plugin_id'] }}-{{ setting_data['id'] }}"
@ -156,7 +160,7 @@
class="badge badge-center rounded-pill bg-secondary d-flex align-items-center justify-content-center p-1 me-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Multisite setting"
data-bs-original-title="Multisite setting"
target="_blank"
rel="noopener">
<span class="bx bx-server bx-xs"></span>
@ -166,14 +170,14 @@
<span class="badge badge-center rounded-pill bg-primary-subtle text-dark d-flex align-items-center justify-content-center p-1 me-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="From global configuration">
data-bs-original-title="From global configuration">
<span class="bx bx-globe bx-xs"></span>
</span>
{% endif %}
<span class="badge rounded-pill bg-secondary-subtle text-dark d-flex align-items-center justify-content-center p-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="{{ setting_data['help'] }}">
data-bs-original-title="{{ setting_data['help'] }}">
<span class="bx bx-question-mark bx-xs"></span>
</span>
</div>
@ -202,15 +206,25 @@
{% for step_config in step_configs %}
{% set step_config_id = template + "-config-" + template_data['plugin_id'] + "-" + step_config.replace("/", "-").replace(".conf", "") %}
{% set step_config_value = configs.get(current_endpoint + "_" + step_config.replace(".conf", "").replace("/", "_")) %}
{% set step_config_method = "ui" %}
{% if step_config_value is none %}
{% set step_config_value = template_data["configs"].get(step_config, "") %}
{% else %}
{% set step_config_value = step_config_value["data"].decode("utf-8") %}
{% set step_config_method = step_config_value["method"] %}
{% endif %}
<div class="mb-3 pb-6 position-relative h-vh-40">
<div class="mb-3 pb-6 position-relative h-vh-40"
{% if service_method == "autoconf" or is_readonly %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="{% if is_readonly %}The database is in readonly{% else %}The service was created using the autoconf method{% endif %}
therefore
the
configuration
is
locked
"
{% endif %}>
<label for="{{ step_config_id }}" class="form-label fw-semibold fs-6">{{ step_config }}</label>
<textarea id="{{ step_config_id }}-default" class="visually-hidden">{{ template_data["configs"].get(step_config, "") }}</textarea>
<div id="{{ step_config_id }}" aria-labelledby="label-{{ step_config_id }}" data-language="{% if step_config.startswith(('crs', 'modsec')) %}ModSecurity{% else %}NGINX{% endif %}" data-name="CUSTOM_CONF_{{ step_config.split("/")[0] |upper }}_{{ step_config.split("/")[1] .replace(".conf", "") }}" class="ace-editor border rounded position-absolute top-0 start-0 end-0 bottom-0 mt-6">
<div id="{{ step_config_id }}" aria-labelledby="label-{{ step_config_id }}" data-language="{% if step_config.startswith(('crs', 'modsec')) %}ModSecurity{% else %}NGINX{% endif %}" data-name="CUSTOM_CONF_{{ step_config.split("/")[0] |upper }}_{{ step_config.split("/")[1] .replace(".conf", "") }}" data-method="{% if is_readonly %}readonly{% else %}{{ step_config_method }}{% endif %}" class="ace-editor border rounded position-absolute top-0 start-0 end-0 bottom-0 mt-6">
{{ step_config_value }}
</div>
</div>
@ -227,9 +241,16 @@
<div></div>
{% endif %}
{% if loop.index == template_data["steps"]|length %}
<div {% if service_method == "autoconf" %}data-bs-toggle="tooltip" data-bs-placement="top" title="The service was created using the autoconf method, therefore the configuration is locked"{% endif %}>
<div {% if service_method == "autoconf" or is_readonly %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="{% if is_readonly %}The database is in readonly{% else %}The service was created using the autoconf method{% endif %}
therefore
the
configuration
is
locked
"
{% endif %}>
<button type="button"
class="btn btn-outline-bw-green save-settings{% if service_method == "autoconf" %} disabled{% endif %}"
class="btn btn-outline-bw-green save-settings{% if service_method == "autoconf" or is_readonly %} disabled{% endif %}"
data-template="{{ template }}">
<i class="bx bx-save bx-sm ms-sm-n2"></i>
<span class="align-middle d-sm-inline-block d-none ms-sm-1">Save</span>

View file

@ -5,7 +5,7 @@
<div class="position-absolute top-0 end-0 m-3" style="z-index: 1000">
<div class="d-flex flex-wrap justify-content-center align-items-center">
<div class="card p-1 me-2"
{% if not request.is_secure %}data-bs-toggle="tooltip" data-bs-placement="top" title="The copy feature is only available over HTTPS"{% endif %}>
{% if not request.is_secure %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="The copy feature is only available over HTTPS"{% endif %}>
<button type="button"
class="btn btn-sm btn-outline-secondary copy-settings{% if not request.is_secure %} disabled{% endif %}">
<i class="bx bx-copy-alt bx-xs"></i>
@ -13,10 +13,17 @@
</button>
</div>
<div class="card p-1"
{% if service_method == "autoconf" %} data-bs-toggle="tooltip" data-bs-placement="top" title="The service was created using the autoconf method, therefore the configuration is locked"{% endif %}>
{% if service_method == "autoconf" or is_readonly %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="{% if is_readonly %}The database is in readonly{% else %}The service was created using the autoconf method{% endif %}
therefore
the
configuration
is
locked
"
{% endif %}>
<!-- Save button container -->
<button type="button"
class="btn btn-sm btn-outline-bw-green save-settings {% if service_method == "autoconf" %}disabled{% endif %}">
class="btn btn-sm btn-outline-bw-green save-settings {% if service_method == "autoconf" or is_readonly %}disabled{% endif %}">
<i class="bx bx-save bx-xs"></i>
<span class="d-none d-md-inline">&nbsp;Save</span>
</button>
@ -24,7 +31,9 @@
</div>
</div>
<div class="card bg-primary p-1 d-flex flex-column"
{% if service_method == "autoconf" %}data-bs-toggle="tooltip" data-bs-placement="top" title="Disabled by {{ service_method }}"{% endif %}>
{% if service_method == "autoconf" or is_readonly %}data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-original-title="Disabled by {% if is_readonly %}readonly{% else %}{{ service_method }}{% endif %}
"
{% endif %}>
{% set config_lines = ["IS_DRAFT=" + config.get('IS_DRAFT', {}).get('value', 'no')] %}
{% set default_settings = ["IS_DRAFT=no"] %}
{% for plugin_data in plugins.values() %}
@ -56,7 +65,7 @@
rows="35"
id="raw-config"
aria-label="Raw configuration"
{% if service_method == "autoconf" %}disabled{% endif %}>{{ raw_config|safe }}</textarea>
{% if service_method == "autoconf" or is_readonly %}readonly{% endif %}>{{ raw_config|safe }}</textarea>
<label for="raw-config" class="text-white">Raw configuration</label>
</div>
</div>

View file

@ -117,7 +117,8 @@
<form method="POST" action="{{ profile_url }}/edit">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="row g-3">
<div class="col-md-6">
<div class="col-md-6"
{% if is_readonly %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Disabled by readonly"{% endif %}>
<label for="username" class="form-label">Username</label>
<input class="form-control"
type="text"
@ -126,9 +127,11 @@
value="{{ current_user.get_id() }}"
placeholder="john.doe@example.com"
aria-label="Username"
required />
required
{% if is_readonly %}disabled{% endif %} />
</div>
<div class="col-md-6">
<div class="col-md-6"
{% if is_readonly %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Disabled by readonly"{% endif %}>
<label for="email" class="form-label">E-mail</label>
<input class="form-control"
type="email"
@ -136,24 +139,32 @@
name="email"
value="{{ current_user.email or '' }}"
placeholder="john.doe@example.com"
aria-label="E-mail" />
aria-label="E-mail"
{% if is_readonly %}disabled{% endif %} />
</div>
<div class="col-md-12 form-password-toggle">
<label class="form-label" for="password">Current password</label>
<div class="input-group input-group-merge">
<div class="input-group input-group-merge"
{% if is_readonly %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Disabled by readonly"{% endif %}>
<input type="password"
id="password"
class="form-control"
name="password"
placeholder="&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;"
aria-describedby="Current password"
required />
required
{% if is_readonly %}disabled{% endif %} />
<span class="input-group-text cursor-pointer"><i class="bx bx-hide"></i></span>
</div>
</div>
</div>
<div class="mt-4 justify-content-center d-flex">
<button type="submit" class="btn btn-primary me-2">Save Changes</button>
<div {% if is_readonly %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="The database is in readonly, therefore the configuration is locked"{% endif %}>
<button type="submit"
class="btn btn-primary me-2{% if is_readonly %} disabled{% endif %}">
Save Changes
</button>
</div>
<button type="reset" class="btn btn-outline-secondary">Cancel</button>
</div>
</form>
@ -165,19 +176,16 @@
<!-- Theme -->
<div class="card">
<h5 class="card-header">Change Theme</h5>
<div class="card-body pb-4">
<div class="card-body pb-4"
data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-html="true"
data-bs-original-title="<i class='bx bx-rocket bx-xs'></i><span>Coming soon</span>">
<!-- <form method="POST" action="{{ profile_url }}/edit"> -->
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="row g-3">
<div class="col-md-12 form-floating">
<select class="form-select"
id="theme"
name="theme"
disabled
data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-html="true"
data-bs-original-title="<i class='bx bx-rocket bx-xs'></i><span>Coming soon</span>">
<select class="form-select" id="theme" name="theme" disabled>
<option value="light"
{% if current_user.theme == "light" %}selected{% endif %}>
Light
@ -188,7 +196,7 @@
</div>
</div>
<div class="mt-4 justify-content-center d-flex">
<button type="submit" class="btn btn-primary me-2">Save Changes</button>
<button type="submit" class="btn btn-primary me-2 disabled">Save Changes</button>
<button type="reset" class="btn btn-outline-secondary">Cancel</button>
</div>
<!-- </form> -->
@ -210,16 +218,11 @@
<div class="row g-3">
<div class="col-md-12 form-password-toggle">
<label for="new_password" class="form-label">New Password</label>
<div class="input-group input-group-merge">
<input class="form-control"
type="password"
id="new_password"
name="new_password"
placeholder="&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;"
aria-label="New Password"
autocomplete="off"
required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[ !&quot;#$%&'()*+,./:;<=>?@[\\\]^_`{|}~-]).{8,}$" />
<div class="input-group input-group-merge"
{% if is_readonly %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Disabled by readonly"{% endif %}>
<input class="form-control" type="password" id="new_password" name="new_password" placeholder="&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;" aria-label="New Password" autocomplete="off" required pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[ !&quot;#$%&'()*+,./:;<=>?@[\\\]^_`{|}~-]).{8,}$"
{% if is_readonly %}disabled{% endif %}
/>
<span class="input-group-text cursor-pointer"><i class="bx bx-hide"></i></span>
</div>
<div class="mt-3">
@ -245,35 +248,37 @@
</div>
<div class="col-md-12 form-password-toggle">
<label for="new_password_confirm" class="form-label">Confirm Password</label>
<div class="input-group input-group-merge">
<input class="form-control"
type="password"
id="new_password_confirm"
name="new_password_confirm"
placeholder="&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;"
aria-label="Confirm Password"
autocomplete="off"
required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[ !&quot;#$%&'()*+,./:;<=>?@[\\\]^_`{|}~-]).{8,}$" />
<div class="input-group input-group-merge"
{% if is_readonly %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Disabled by readonly"{% endif %}>
<input class="form-control" type="password" id="new_password_confirm" name="new_password_confirm" placeholder="&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;" aria-label="Confirm Password" autocomplete="off" required pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[ !&quot;#$%&'()*+,./:;<=>?@[\\\]^_`{|}~-]).{8,}$"
{% if is_readonly %}disabled{% endif %}
/>
<span class="input-group-text cursor-pointer"><i class="bx bx-hide"></i></span>
</div>
</div>
<div class="col-md-12 form-password-toggle">
<label class="form-label" for="password">Current password</label>
<div class="input-group input-group-merge">
<div class="input-group input-group-merge"
{% if is_readonly %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Disabled by readonly"{% endif %}>
<input type="password"
id="password"
class="form-control"
name="password"
placeholder="&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;"
aria-describedby="Current password"
required />
required
{% if is_readonly %}disabled{% endif %} />
<span class="input-group-text cursor-pointer"><i class="bx bx-hide"></i></span>
</div>
</div>
</div>
<div class="mt-4 justify-content-center d-flex">
<button type="submit" class="btn btn-primary me-2">Save Changes</button>
<div {% if is_readonly %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="The database is in readonly, therefore the configuration is locked"{% endif %}>
<button type="submit"
class="btn btn-primary me-2{% if is_readonly %} disabled{% endif %}">
Save Changes
</button>
</div>
<button type="reset" class="btn btn-outline-secondary">Cancel</button>
</div>
</form>
@ -320,7 +325,8 @@
</span>
</div>
</div>
<div class="col-md-12">
<div class="col-md-12"
{% if is_readonly %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Disabled by readonly"{% endif %}>
<label for="2fa_code" class="form-label text-start d-block">2FA Code</label>
<div class="input-group">
<input class="form-control"
@ -329,25 +335,33 @@
name="totp_token"
placeholder="Enter code"
aria-label="2FA Code"
required />
required
{% if is_readonly %}disabled{% endif %} />
</div>
</div>
<div class="col-md-12 form-password-toggle">
<label class="form-label" for="password">Current password</label>
<div class="input-group input-group-merge">
<div class="input-group input-group-merge"
{% if is_readonly %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Disabled by readonly"{% endif %}>
<input type="password"
id="password"
class="form-control"
name="password"
placeholder="&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;"
aria-describedby="Current password"
required />
required
{% if is_readonly %}disabled{% endif %} />
<span class="input-group-text cursor-pointer"><i class="bx bx-hide"></i></span>
</div>
</div>
</div>
<div class="mt-4 justify-content-center d-flex">
<button type="submit" class="btn btn-primary">Enable TOTP</button>
<div {% if is_readonly %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="The database is in readonly, therefore the configuration is locked"{% endif %}>
<button type="submit"
class="btn btn-primary{% if is_readonly %} disabled{% endif %}">
Enable TOTP
</button>
</div>
</div>
</form>
<!-- /Enable 2FA -->
@ -361,32 +375,41 @@
</div>
<div class="col-md-12">
<label for="totp_token" class="form-label text-start d-block">2FA Code or Recovery Code</label>
<div class="input-group">
<div class="input-group"
{% if is_readonly %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Disabled by readonly"{% endif %}>
<input class="form-control"
type="text"
id="totp_token"
name="totp_token"
placeholder="Enter code"
aria-label="2FA Code or Recovery Code"
required />
required
{% if is_readonly %}disabled{% endif %} />
</div>
</div>
<div class="col-md-12 form-password-toggle">
<label class="form-label" for="password">Current password</label>
<div class="input-group input-group-merge">
<div class="input-group input-group-merge"
{% if is_readonly %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Disabled by readonly"{% endif %}>
<input type="password"
id="password"
class="form-control"
name="password"
placeholder="&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;"
aria-describedby="Current password"
required />
required
{% if is_readonly %}disabled{% endif %} />
<span class="input-group-text cursor-pointer"><i class="bx bx-hide"></i></span>
</div>
</div>
</div>
<div class="mt-4 justify-content-center d-flex">
<button type="submit" class="btn btn-primary">Disable TOTP</button>
<div {% if is_readonly %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="The database is in readonly, therefore the configuration is locked"{% endif %}>
<button type="submit"
class="btn btn-primary{% if is_readonly %} disabled{% endif %}">
Disable TOTP
</button>
</div>
</div>
</form>
<!-- /Disable 2FA -->
@ -408,7 +431,8 @@
autocomplete="off">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="row g-3">
<div class="col-md-12 form-password-toggle">
<div class="col-md-12 form-password-toggle"
{% if is_readonly %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Disabled by readonly"{% endif %}>
<label class="form-label" for="password">Current password</label>
<div class="input-group input-group-merge">
<input type="password"
@ -417,13 +441,19 @@
name="password"
placeholder="&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;"
aria-describedby="Current password"
required />
required
{% if is_readonly %}disabled{% endif %} />
<span class="input-group-text cursor-pointer"><i class="bx bx-hide"></i></span>
</div>
</div>
</div>
<div class="mt-4 justify-content-center d-flex">
<button type="submit" class="btn btn-primary">Refresh Recovery Codes</button>
<div {% if is_readonly %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="The database is in readonly, therefore the configuration is locked"{% endif %}>
<button type="submit"
class="btn btn-primary{% if is_readonly %} disabled{% endif %}">
Refresh Recovery Codes
</button>
</div>
</div>
</form>
</div>
@ -444,20 +474,27 @@
<div class="row g-3">
<div class="col-md-12 form-password-toggle">
<label class="form-label" for="password">Current password</label>
<div class="input-group input-group-merge">
<div class="input-group input-group-merge"
{% if is_readonly %} data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="Disabled by readonly"{% endif %}>
<input type="password"
id="password"
class="form-control"
name="password"
placeholder="&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;"
aria-describedby="Current password"
required />
required
{% if is_readonly %}disabled{% endif %} />
<span class="input-group-text cursor-pointer"><i class="bx bx-hide"></i></span>
</div>
</div>
</div>
<div class="mt-4 d-flex justify-content-center">
<button type="submit" class="btn btn-danger">Wipe other sessions</button>
<div {% if is_readonly %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="The database is in readonly, therefore the configuration is locked"{% endif %}>
<button type="submit"
class="btn btn-danger{% if is_readonly %} disabled{% endif %}">
Wipe other sessions
</button>
</div>
</div>
</div>
</div>

View file

@ -45,7 +45,7 @@
<a href="{{ url_for("services") }}/{{ service['id'] }}"
data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-original-title="Edit service {{ service['id'] }}"><i class="bx bx-edit bx-xs"></i>&nbsp;{{ service["id"] }}</a>
data-bs-original-title="{% if service['method'] == 'autoconf' or is_readonly %}View{% else %}Edit{% endif %} service {{ service['id'] }}"><i class="bx bx-{% if service['method'] == 'autoconf' or is_readonly %}show{% else %}edit{% endif %} bx-xs"></i>&nbsp;{{ service["id"] }}</a>
</td>
<td>
{% if service['is_draft'] %}
@ -78,29 +78,33 @@
href="{{ url_for("services") }}/{{ service['id'] }}"
data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-original-title="Edit service {{ service['id'] }}">
<i class="bx bx-edit bx-xs"></i>
data-bs-original-title="{% if service['method'] == 'autoconf' or is_readonly %}View{% else %}Edit{% endif %} service {{ service['id'] }}">
<i class="bx bx-{% if service['method'] == 'autoconf' or is_readonly %}show{% else %}edit{% endif %} bx-xs"></i>
</a>
<a role="button"
class="btn btn-outline-secondary btn-sm me-1"
href="{{ url_for("services") }}/new?clone={{ service['id'] }}"
data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-original-title="Clone service {{ service['id'] }}">
<i class="bx bx-copy-alt bx-xs"></i>
</a>
<button type="button"
class="btn btn-outline-secondary btn-sm me-1 convert-service"
data-service-id="{{ service['id'] }}"
data-value="{{ 'online' if service['is_draft'] else 'draft' }}"
data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-original-title="Convert service {{ service['id'] }} to {{ 'online' if service['is_draft'] else 'draft' }}">
<i class="bx bx-transfer bx-xs"></i>
</button>
<div {% if is_readonly %}data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-original-title="Disabled by readonly"{% endif %}>
<a role="button"
class="btn btn-outline-secondary btn-sm me-1{% if is_readonly %} disabled{% endif %}"
href="{{ url_for("services") }}/new?clone={{ service['id'] }}"
data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-original-title="Clone service {{ service['id'] }}">
<i class="bx bx-copy-alt bx-xs"></i>
</a>
</div>
<div {% if is_readonly %}data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-original-title="Disabled by readonly"{% endif %}>
<button type="button"
class="btn btn-outline-secondary btn-sm me-1 convert-service{% if is_readonly %} disabled{% endif %}"
data-service-id="{{ service['id'] }}"
data-value="{{ 'online' if service['is_draft'] else 'draft' }}"
data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-original-title="Convert service {{ service['id'] }} to {{ 'online' if service['is_draft'] else 'draft' }}">
<i class="bx bx-transfer bx-xs"></i>
</button>
</div>
<div data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-original-title="{% if service['method'] != 'ui' %}Disabled by {{ service['method'] }}{% else %}Delete service {{ service['id'] }}{% endif %}">
data-bs-original-title="{% if service['method'] != 'ui' or is_readonly %}Disabled by {% if is_readonly %}readonly{% else %}{{ service['method'] }}{% endif %}{% else %}Delete service {{ service['id'] }}{% endif %}">
<button type="button"
data-service-id="{{ service['id'] }}"
class="btn btn-outline-danger btn-sm me-1 delete-service{% if service['method'] != 'ui' %} disabled{% endif %}">
@ -134,76 +138,78 @@
</div>
<div class="toast-body">If you read this, it means that you're curious 👀</div>
</div>
<div class="modal fade"
id="modal-convert-services"
data-bs-backdrop="static"
tabindex="-1"
aria-hidden="true"
role="dialog">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm Conversion</h5>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
{% if not is_readonly %}
<div class="modal fade"
id="modal-convert-services"
data-bs-backdrop="static"
tabindex="-1"
aria-hidden="true"
role="dialog">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm Conversion</h5>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<form action="{{ url_for("services") }}/convert" method="POST">
<div class="modal-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" id="convertion-type" name="convert_to" value="draft" />
<input type="hidden"
id="selected-services-input-convert"
name="services"
value="" />
<div class="alert alert-danger text-center" role="alert">Are you sure you want to convert the selected services?</div>
<div id="selected-services-convert" class="mb-3"></div>
</div>
<div class="modal-footer justify-content-center">
<button type="submit" class="btn btn-outline-success me-2">Convert services</button>
<button type="reset"
class="btn btn-outline-secondary"
data-bs-dismiss="modal">Cancel</button>
</div>
</form>
</div>
<form action="{{ url_for("services") }}/convert" method="POST">
<div class="modal-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" id="convertion-type" name="convert_to" value="draft" />
<input type="hidden"
id="selected-services-input-convert"
name="services"
value="" />
<div class="alert alert-danger text-center" role="alert">Are you sure you want to convert the selected services?</div>
<div id="selected-services-convert" class="mb-3"></div>
</div>
<div class="modal-footer justify-content-center">
<button type="submit" class="btn btn-outline-success me-2">Convert services</button>
<button type="reset"
class="btn btn-outline-secondary"
data-bs-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade"
id="modal-delete-services"
data-bs-backdrop="static"
tabindex="-1"
aria-hidden="true"
role="dialog">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm deletion</h5>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
<div class="modal fade"
id="modal-delete-services"
data-bs-backdrop="static"
tabindex="-1"
aria-hidden="true"
role="dialog">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm deletion</h5>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<form action="{{ url_for("services") }}/delete" method="POST">
<div class="modal-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden"
id="selected-services-input-delete"
name="services"
value="" />
<div class="alert alert-danger text-center" role="alert">Are you sure you want to delete the selected services?</div>
<div id="selected-services-delete" class="mb-3"></div>
</div>
<div class="modal-footer justify-content-center">
<button type="submit" class="btn btn-outline-danger me-2">Delete</button>
<button type="reset"
class="btn btn-outline-secondary"
data-bs-dismiss="modal">Cancel</button>
</div>
</form>
</div>
<form action="{{ url_for("services") }}/delete" method="POST">
<div class="modal-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden"
id="selected-services-input-delete"
name="services"
value="" />
<div class="alert alert-danger text-center" role="alert">Are you sure you want to delete the selected services?</div>
<div id="selected-services-delete" class="mb-3"></div>
</div>
<div class="modal-footer justify-content-center">
<button type="submit" class="btn btn-outline-danger me-2">Delete</button>
<button type="reset"
class="btn btn-outline-secondary"
data-bs-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
<!-- / Content -->
{% endblock %}

View file

@ -91,7 +91,7 @@ def on_starting(server):
x += 1
TOTP_SECRETS = tmp_secrets.copy()
del tmp_secrets
invalid_totp_secrets = True
invalid_totp_secrets = x == 1
if not TOTP_SECRETS:
LOGGER.warning("The TOTP_SECRETS environment variable is missing, generating a random one ...")
@ -114,14 +114,18 @@ def on_starting(server):
ret, err = DB.init_ui_tables(BW_VERSION)
if not ret and err:
LOGGER.error(f"Exception while checking database tables : {err}")
exit(1)
if err.startswith("The database is read-only"):
LOGGER.warning(err)
else:
LOGGER.error(f"Exception while checking database tables : {err}")
exit(1)
elif not ret:
LOGGER.info("Database ui tables didn't change, skipping update ...")
else:
LOGGER.info("Database ui tables successfully updated")
if not DB.get_ui_roles(as_dict=True):
ret = DB.create_ui_role("admin", "Admins can create new users, edit and read the data.", ["manage", "write", "read"])
if ret:
LOGGER.error(f"Couldn't create the admin role in the database: {ret}")
@ -210,7 +214,15 @@ def on_starting(server):
latest_version = latest_release["tag_name"].removeprefix("v")
TMP_DIR.joinpath("ui_data.json").write_text(
dumps({"LATEST_VERSION": latest_version, "LATEST_VERSION_LAST_CHECK": datetime.now().astimezone().isoformat(), "TO_FLASH": []}), encoding="utf-8"
dumps(
{
"LATEST_VERSION": latest_version,
"LATEST_VERSION_LAST_CHECK": datetime.now().astimezone().isoformat(),
"TO_FLASH": [],
"READONLY_MODE": DB.readonly,
}
),
encoding="utf-8",
)
LOGGER.info("UI is ready")

View file

@ -122,7 +122,7 @@ with app.app_context():
@app.context_processor
def inject_variables():
if request.path.startswith(("/setup", "/loading", "/login", "/totp")):
if request.path.startswith(("/check_reloading", "/setup", "/loading", "/login", "/totp")):
return dict(script_nonce=app.config["SCRIPT_NONCE"])
DATA.load_from_file()
@ -256,7 +256,7 @@ def check_database_state():
"LAST_DATABASE_RETRY": DB.last_connection_retry.isoformat() if DB.last_connection_retry else datetime.now().astimezone().isoformat(),
}
)
elif not DATA.get("READONLY_MODE", False) and request.method == "POST" and not ("/totp" in request.path or "/login" in request.path):
elif DB.database_uri and not DATA.get("READONLY_MODE", False) and request.method == "POST" and not ("/totp" in request.path or "/login" in request.path):
try:
DB.test_write()
DATA["READONLY_MODE"] = False
@ -293,8 +293,8 @@ def before_request():
DB.readonly = DATA.get("READONLY_MODE", False)
if DB.readonly:
flask_flash("Database connection is in read-only mode : no modification possible.", "error")
if not request.path.startswith(("/check_reloading", "/loading", "/login", "/totp")) and DB.readonly:
flask_flash("Database connection is in read-only mode : no modifications possible.", "error")
if current_user.is_authenticated:
passed = True
@ -310,7 +310,7 @@ def before_request():
elif session["user_agent"] != request.headers.get("User-Agent"):
LOGGER.warning(f"User {current_user.get_id()} tried to access his session with a different User-Agent.")
passed = False
elif session["session_id"] in DATA.get("REVOKED_SESSIONS", []):
elif "session_id" in session and session["session_id"] in DATA.get("REVOKED_SESSIONS", []):
LOGGER.warning(f"User {current_user.get_id()} tried to access a revoked session.")
passed = False
@ -319,6 +319,9 @@ def before_request():
def mark_user_access(session_id):
if DB.readonly:
return
ret = DB.mark_ui_user_access(session_id, datetime.now().astimezone())
if ret:
LOGGER.error(f"Couldn't mark the user access: {ret}")