mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
Add notification sidebar to web UI with back-end logic
This commit is contained in:
parent
4f9e0e1fea
commit
e723912588
30 changed files with 274 additions and 163 deletions
|
|
@ -3,10 +3,11 @@ from json import JSONDecodeError, dumps, loads
|
|||
from math import floor
|
||||
from time import time
|
||||
|
||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||
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.utils import flash
|
||||
|
||||
from app.routes.utils import get_redis_client, get_remain, handle_error, verify_data_in_form
|
||||
|
||||
|
|
@ -33,11 +34,11 @@ def bans_page():
|
|||
try:
|
||||
unban = loads(unban.replace('"', '"').replace("'", '"'))
|
||||
except BaseException:
|
||||
flash(f"Invalid unban: {unban}, skipping it ...", "error")
|
||||
flask_flash(f"Invalid unban: {unban}, skipping it ...", "error")
|
||||
continue
|
||||
|
||||
if "ip" not in unban:
|
||||
flash(f"Invalid unban: {unban}, skipping it ...", "error")
|
||||
flask_flash(f"Invalid unban: {unban}, skipping it ...", "error")
|
||||
continue
|
||||
|
||||
if redis_client:
|
||||
|
|
@ -57,7 +58,7 @@ def bans_page():
|
|||
|
||||
for ban in data:
|
||||
if not isinstance(ban, dict) or "ip" not in ban:
|
||||
flash(f"Invalid ban: {ban}, skipping it ...", "error")
|
||||
flask_flash(f"Invalid ban: {ban}, skipping it ...", "error")
|
||||
continue
|
||||
|
||||
reason = ban.get("reason", "ui")
|
||||
|
|
@ -134,14 +135,14 @@ def bans_ban():
|
|||
redis_client = get_redis_client()
|
||||
for ban in bans:
|
||||
if not isinstance(ban, dict) or "ip" not in ban:
|
||||
flash(f"Invalid ban: {ban}, skipping it ...", "error")
|
||||
flask_flash(f"Invalid ban: {ban}, skipping it ...", "error")
|
||||
continue
|
||||
|
||||
reason = ban.get("reason", "ui")
|
||||
try:
|
||||
ban_end = datetime.fromisoformat(ban["end_date"])
|
||||
except ValueError:
|
||||
flash(f"Invalid ban: {ban}, skipping it ...", "error")
|
||||
flask_flash(f"Invalid ban: {ban}, skipping it ...", "error")
|
||||
continue
|
||||
current_time = datetime.now().astimezone()
|
||||
ban_end = (ban_end - current_time).total_seconds()
|
||||
|
|
@ -181,7 +182,7 @@ def bans_unban():
|
|||
redis_client = get_redis_client()
|
||||
for unban in unbans:
|
||||
if "ip" not in unban:
|
||||
flash(f"Invalid unban: {unban}, skipping it ...", "error")
|
||||
flask_flash(f"Invalid unban: {unban}, skipping it ...", "error")
|
||||
continue
|
||||
|
||||
if redis_client:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from io import BytesIO
|
||||
|
||||
from flask import Blueprint, Response, flash, redirect, render_template, request, send_file, url_for
|
||||
from flask import Blueprint, Response, flash as flask_flash, redirect, render_template, request, send_file, url_for
|
||||
from flask_login import login_required
|
||||
from magic import Magic
|
||||
from werkzeug.utils import secure_filename
|
||||
|
|
@ -41,7 +41,7 @@ def cache_view(service: str, plugin_id: str, job_name: str, file_name: str):
|
|||
return send_file(BytesIO(cache_file), as_attachment=True, download_name=file_name)
|
||||
|
||||
if not cache_file:
|
||||
flash(f"Cache file {file_name} from job {job_name}, plugin {plugin_id}{', service ' + service if service != 'global' else ''} not found", "error")
|
||||
flask_flash(f"Cache file {file_name} from job {job_name}, plugin {plugin_id}{', service ' + service if service != 'global' else ''} not found", "error")
|
||||
return redirect(url_for("cache.cache_page"))
|
||||
|
||||
file_type = Magic(mime=True).from_buffer(cache_file)
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ from threading import Thread
|
|||
from time import time
|
||||
from typing import Dict, Literal, Optional
|
||||
|
||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||
from flask import Blueprint, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from app.dependencies import BW_CONFIG, DATA, DB
|
||||
from app.utils import flash
|
||||
|
||||
from app.routes.utils import handle_error, verify_data_in_form, wait_applying
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ from threading import Thread
|
|||
from time import time
|
||||
from typing import Dict
|
||||
|
||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||
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_CONFIG, DATA, DB
|
||||
from app.utils import flash
|
||||
|
||||
from app.routes.utils import handle_error, manage_bunkerweb, wait_applying
|
||||
|
||||
|
|
@ -66,9 +67,9 @@ def global_config_page():
|
|||
with suppress(BaseException):
|
||||
if config["PRO_LICENSE_KEY"]["value"] != variables["PRO_LICENSE_KEY"]:
|
||||
if threaded:
|
||||
DATA["TO_FLASH"].append({"content": "Checking license key to upgrade.", "type": "success"})
|
||||
DATA["TO_FLASH"].append({"content": "Checking license key to upgrade.", "type": "success", "save": False})
|
||||
else:
|
||||
flash("Checking license key to upgrade.", "success")
|
||||
flask_flash("Checking license key to upgrade.", "success")
|
||||
|
||||
manage_bunkerweb("global_config", variables, threaded=threaded)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
|
|||
from threading import Thread
|
||||
from time import time
|
||||
from typing import Literal
|
||||
from flask import Blueprint, flash, jsonify, redirect, render_template, request, url_for
|
||||
from flask import Blueprint, jsonify, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
|
||||
from app.dependencies import BW_CONFIG, BW_INSTANCES_UTILS, DATA, DB
|
||||
from app.utils import flash
|
||||
|
||||
from app.models.instance import Instance
|
||||
from app.routes.utils import handle_error, verify_data_in_form
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
from json import JSONDecodeError, loads
|
||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||
from flask import Blueprint, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
|
||||
from app.dependencies import DB
|
||||
from app.utils import flash
|
||||
|
||||
from app.routes.utils import handle_error, verify_data_in_form
|
||||
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
from flask import Blueprint, redirect, render_template, request, url_for
|
||||
from flask_login import login_required
|
||||
|
||||
from app.dependencies import DB
|
||||
|
||||
from app.routes.utils import get_service_data, handle_error, update_service
|
||||
|
||||
modes = Blueprint("modes", __name__)
|
||||
|
||||
|
||||
@modes.route("/modes", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def services_modes():
|
||||
if request.method == "POST":
|
||||
if DB.readonly:
|
||||
return handle_error("Database is in read-only mode", "services")
|
||||
|
||||
config, variables, format_configs, server_name, old_server_name, operation, is_draft, was_draft, is_draft_unchanged, mode = get_service_data("modes")
|
||||
message = update_service(config, variables, format_configs, server_name, old_server_name, operation, is_draft, was_draft, is_draft_unchanged)
|
||||
# print(message, flush=True)
|
||||
# print("mode", mode, "service name", server_name, flush=True)
|
||||
# TODO: redirect to /mode?service_name=my_service&mode=my_mode
|
||||
# Following is not working :
|
||||
# return redirect(url_for("loading", next=url_for("modes", mode=mode, service_name=server_name), message=message))
|
||||
# or
|
||||
# return redirect(url_for("loading", next=url_for(f"modes?service_name={server_name}&mode={mode}"), message=message))
|
||||
return redirect(url_for("loading", next=url_for(request.endpoint), message=message))
|
||||
|
||||
if not request.args.get("mode"):
|
||||
return handle_error("Mode type is missing to access /modes.", "services")
|
||||
|
||||
# mode = request.args.get("mode")
|
||||
service_name = request.args.get("service_name")
|
||||
total_config = DB.get_config(methods=True, with_drafts=True)
|
||||
service_names = total_config["SERVER_NAME"]["value"].split(" ")
|
||||
|
||||
if service_name and service_name not in service_names:
|
||||
return handle_error("Service name not found to access advanced mode.", "services")
|
||||
|
||||
# global_config = BW_CONFIG.get_config(global_only=True, methods=True)
|
||||
# plugins = BW_CONFIG.get_plugins()
|
||||
|
||||
# builder = None
|
||||
# templates_db = DB.get_templates()
|
||||
|
||||
# if mode == "raw":
|
||||
# builder = raw_mode_builder(templates_db, plugins, global_config, total_config, service_name or "new", not service_name)
|
||||
# elif mode == "advanced":
|
||||
# builder = advanced_mode_builder(templates_db, plugins, global_config, total_config, service_name or "new", not service_name)
|
||||
# elif mode == "easy":
|
||||
# builder = easy_mode_builder(templates_db, plugins, global_config, total_config, service_name or "new", not service_name)
|
||||
|
||||
# return render_template("modes.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
|
||||
return render_template("modes.html") # TODO
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
from typing import Dict, Generator, Tuple, Union
|
||||
from flask import Blueprint, Response, flash, jsonify, redirect, render_template, request, stream_with_context, url_for, session
|
||||
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
|
||||
from user_agents import parse
|
||||
|
||||
from app.models.totp import totp as TOTP
|
||||
|
||||
from app.dependencies import DATA, DB
|
||||
from app.utils import USER_PASSWORD_RX, gen_password_hash
|
||||
from app.utils import USER_PASSWORD_RX, flash, gen_password_hash
|
||||
|
||||
from app.routes.utils import cors_required, handle_error, verify_data_in_form
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
from flask import Blueprint, flash, redirect, render_template, request, session, url_for
|
||||
from flask import Blueprint, redirect, render_template, request, session, url_for
|
||||
from flask_login import current_user, login_required
|
||||
|
||||
from app.dependencies import DB
|
||||
from app.models.totp import totp as TOTP
|
||||
from app.routes.utils import handle_error, verify_data_in_form
|
||||
from app.routes.utils import flash, handle_error, verify_data_in_form
|
||||
|
||||
totp = Blueprint("totp", __name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from io import BytesIO
|
|||
from time import sleep
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
from flask import Response, flash, redirect, request, session, url_for
|
||||
from flask import Response, redirect, request, url_for
|
||||
from qrcode.main import QRCode
|
||||
from redis import Redis, Sentinel
|
||||
from regex import compile as re_compile
|
||||
|
|
@ -14,7 +14,7 @@ from regex import compile as re_compile
|
|||
from app.models.instance import Instance
|
||||
|
||||
from app.dependencies import BW_CONFIG, DATA, DB
|
||||
from app.utils import LOGGER
|
||||
from app.utils import LOGGER, flash
|
||||
|
||||
|
||||
LOG_RX = re_compile(r"^(?P<date>\d+/\d+/\d+\s\d+:\d+:\d+)\s\[(?P<level>[a-z]+)\]\s\d+#\d+:\s(?P<message>[^\n]+)$")
|
||||
|
|
@ -108,13 +108,7 @@ def manage_bunkerweb(method: str, *args, operation: str = "reloads", is_draft: b
|
|||
|
||||
if not threaded:
|
||||
for f in DATA.get("TO_FLASH", []):
|
||||
if f["type"] == "error":
|
||||
flash(f["content"], "error")
|
||||
else:
|
||||
flash(f["content"])
|
||||
|
||||
if "flash_messages" in session:
|
||||
session["flash_messages"].append((f["content"], f["type"], datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S %Z")))
|
||||
flash(f["content"], f["type"], save=f.get("save", True))
|
||||
|
||||
DATA["TO_FLASH"] = []
|
||||
|
||||
|
|
|
|||
|
|
@ -154,6 +154,10 @@ button.list-group-item-secondary.active {
|
|||
background-color: var(--bs-secondary) !important;
|
||||
}
|
||||
|
||||
.bg-label-secondary {
|
||||
color: var(--bs-secondary) !important;
|
||||
}
|
||||
|
||||
@keyframes colorPhase {
|
||||
0% {
|
||||
box-shadow: 0 1px 20px 1px var(--bs-primary); /* Primary shadow */
|
||||
|
|
|
|||
|
|
@ -165,4 +165,24 @@ $(document).ready(function () {
|
|||
.removeClass("chevron-rotate")
|
||||
.addClass("chevron-rotate-back");
|
||||
});
|
||||
|
||||
$(".toast-datetime").each(function () {
|
||||
const isoDateStr = $(this).text().trim();
|
||||
|
||||
// Parse the ISO format date string
|
||||
const date = new Date(isoDateStr);
|
||||
|
||||
// Check if the date is valid
|
||||
if (!isNaN(date)) {
|
||||
// Convert to local date and time string
|
||||
const localDateStr = date.toLocaleString();
|
||||
|
||||
// Update the text content with the local date string
|
||||
$(this).text(localDateStr);
|
||||
} else {
|
||||
// Handle invalid date
|
||||
console.error(`Invalid date string: ${isoDateStr}`);
|
||||
$(this).text("Invalid date");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,4 +18,7 @@ $(document).ready(function () {
|
|||
});
|
||||
|
||||
editor.renderer.setScrollMargin(10, 10);
|
||||
|
||||
editorElement.removeClass("visually-hidden");
|
||||
$("#cache-waiting").addClass("visually-hidden");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ $(document).ready(function () {
|
|||
|
||||
editor.renderer.setScrollMargin(10, 10);
|
||||
|
||||
editorElement.removeClass("visually-hidden");
|
||||
$("#config-waiting").addClass("visually-hidden");
|
||||
|
||||
const $serviceSearch = $("#service-search");
|
||||
const $serviceDropdownMenu = $("#services-dropdown-menu");
|
||||
const $serviceDropdownItems = $("#services-dropdown-menu li.nav-item");
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ $(document).ready(function () {
|
|||
|
||||
editor.renderer.setScrollMargin(10, 10);
|
||||
|
||||
editorElement.removeClass("visually-hidden");
|
||||
$("#logs-waiting").addClass("visually-hidden");
|
||||
|
||||
$("#copy-logs").click(function () {
|
||||
$this = $(this);
|
||||
editor.selectAll();
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class News {
|
|||
>`),
|
||||
);
|
||||
document.querySelector("#news-button").insertAdjacentHTML(
|
||||
"beforeend",
|
||||
"afterend",
|
||||
DOMPurify.sanitize(`<span
|
||||
class="badge-dot-text position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger"
|
||||
>
|
||||
|
|
@ -58,12 +58,12 @@ class News {
|
|||
);
|
||||
}
|
||||
|
||||
const newsContainer = document.querySelector("[data-news-container]");
|
||||
const newsContainer = document.getElementById("data-news-container");
|
||||
const lastItem = lastNews[0];
|
||||
//remove default message
|
||||
newsContainer.textContent = "";
|
||||
document
|
||||
.querySelector("[data-news-container]")
|
||||
.getElementById("data-news-container")
|
||||
.insertAdjacentHTML(
|
||||
"afterbegin",
|
||||
`<div data-news-row class="row g-6 justify-content-center">`,
|
||||
|
|
@ -99,7 +99,7 @@ class News {
|
|||
});
|
||||
});
|
||||
document
|
||||
.querySelector("[data-news-container]")
|
||||
.getElementById("data-news-container")
|
||||
.insertAdjacentHTML("beforeend", "</div>");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,21 @@
|
|||
{% set current_endpoint = request.path.split("/")[-1] %}
|
||||
{% set pro_diamond_url = url_for('static', filename='img/diamond.svg') %}
|
||||
{% set avatar_url = url_for('static', filename='img/avatar_profil_BW.png') %}
|
||||
{% set plugin_types = {
|
||||
"core": {
|
||||
"icon": "<i class=\"bx bx-shield bx-xs\"></i>",
|
||||
"title-class": " border-dark"
|
||||
},
|
||||
"external": {
|
||||
"icon": "<i class=\"bx bx-plug bx-xs\"></i>",
|
||||
"title-class": " border-secondary",
|
||||
"text-class": " text-secondary fw-bold"
|
||||
},
|
||||
"pro": {
|
||||
"title-class": " border-primary",
|
||||
"text-class": " text-primary fw-bold shine"
|
||||
}
|
||||
} %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"
|
||||
class="light-style layout-navbar-fixed layout-menu-fixed"
|
||||
|
|
@ -25,7 +40,7 @@
|
|||
<link rel="stylesheet"
|
||||
href="{{ url_for('static', filename='fonts/boxicons.min.css') }}"
|
||||
nonce="{{ style_nonce }}" />
|
||||
{% if current_endpoint in ("instances", "services", "configs", "cache", "reports", "bans", "jobs") %}
|
||||
{% if current_endpoint in ("instances", "services", "configs", "plugins", "cache", "reports", "bans", "jobs") or "plugins" in request.path %}
|
||||
<!-- Datatables -->
|
||||
<link rel="stylesheet"
|
||||
href="{{ url_for('static', filename='libs/datatables/datatables.min.css') }}"
|
||||
|
|
@ -101,7 +116,7 @@
|
|||
nonce="{{ script_nonce }}"></script>
|
||||
<script src="{{ url_for('static', filename='libs/purify/purify.min.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% if current_endpoint in ("instances", "services", "configs", "cache", "reports", "bans", "jobs") %}
|
||||
{% if current_endpoint in ("instances", "services", "configs", "plugins", "cache", "reports", "bans", "jobs") or "plugins" in request.path %}
|
||||
<script src="{{ url_for('static', filename='libs/datatables/datatables.min.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% endif %}
|
||||
|
|
@ -155,6 +170,9 @@
|
|||
{% elif current_endpoint != "configs" and "configs" in request.path %}
|
||||
<script src="{{ url_for('static', filename='js/pages/config_edit.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% elif current_endpoint == "plugins" %}
|
||||
<script src="{{ url_for('static', filename='js/pages/plugins.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
{% elif current_endpoint == "cache" %}
|
||||
<script src="{{ url_for('static', filename='js/pages/cache.js') }}"
|
||||
nonce="{{ script_nonce }}"></script>
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@
|
|||
</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>
|
||||
<div id="cache-value"
|
||||
class="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 %}
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@
|
|||
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" 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" %}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 %}>
|
||||
<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 %}">
|
||||
<i class="bx bx-save bx-sm"></i>
|
||||
|
|
@ -131,12 +131,13 @@
|
|||
</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" title="Disabled by {{ config_method }}"{% endif %}>
|
||||
{% 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>
|
||||
<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="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 %}
|
||||
|
|
|
|||
|
|
@ -43,8 +43,26 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% set config_types = {
|
||||
"crs": "shield-alt",
|
||||
"modsec_crs": "shield-quarter",
|
||||
"modsec": "shield-alt-2",
|
||||
"stream": "network-chart",
|
||||
"custom": "window-alt"
|
||||
} %}
|
||||
{% for config in configs %}
|
||||
{% set service_id = config['service_id'] if config['service_id'] else 'global' %}
|
||||
{% if config['type'].startswith('crs') %}
|
||||
{% set config_icon = "shield-alt" %}
|
||||
{% elif config['type'] == 'modsec_crs' %}
|
||||
{% set config_icon = "shield-quarter" %}
|
||||
{% elif config['type'] == 'modsec' %}
|
||||
{% set config_icon = "shield-alt-2" %}
|
||||
{% elif 'stream' in config['type'] %}
|
||||
{% set config_icon = "network-chart" %}
|
||||
{% else %}
|
||||
{% set config_icon = "window-alt" %}
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
|
|
@ -54,7 +72,7 @@
|
|||
data-bs-original-title="Edit custom config {{ config['name'] }}"><i class="bx bx-edit bx-xs"></i> {{ config["name"] }}</a>
|
||||
</td>
|
||||
<td id="type-{{ config['type'] }}-{{ service_id.replace('.', '_') }}-{{ config['name'] }}">
|
||||
<i class="bx bx-{% if config['type'].startswith('crs') %}shield-alt{% elif config['type'] == 'modsec_crs' %}shield-quarter{% elif config['type'] == 'modsec' %}shield-alt-2{% elif 'stream' in config['type'] %}network-chart{% else %}window-alt{% endif %}"></i>
|
||||
<i class="bx bx-{{ config_icon }}"></i>
|
||||
{{ config["type"]|upper }}
|
||||
</td>
|
||||
<td>{{ config["method"] }}</td>
|
||||
|
|
|
|||
|
|
@ -129,14 +129,32 @@
|
|||
<span class="d-none d-md-inline">Need help?</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<li class="me-3 position-relative">
|
||||
<button id="notifications-button"
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary text-uppercase d-flex align-items-center"
|
||||
aria-pressed="true"
|
||||
data-bs-toggle="offcanvas"
|
||||
data-bs-target="#side-offcanvas-notifications"
|
||||
aria-controls="side-offcanvas-notifications">
|
||||
<span class="bx bx-bell me-0 me-md-2"></span>
|
||||
<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>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="position-relative">
|
||||
<button id="news-button"
|
||||
type="button"
|
||||
class="btn btn-sm btn-dark text-uppercase d-flex align-items-center"
|
||||
aria-pressed="true"
|
||||
data-bs-toggle="offcanvas"
|
||||
data-bs-target="#side-offcanvas"
|
||||
aria-controls="side-offcanvas">
|
||||
data-bs-target="#side-offcanvas-news"
|
||||
aria-controls="side-offcanvas-news">
|
||||
<span class="bx bx-news me-0 me-md-2"></span>
|
||||
<span class="d-none d-md-inline">News</span>
|
||||
</button>
|
||||
|
|
@ -147,7 +165,8 @@
|
|||
</div>
|
||||
<!-- prettier-ignore -->
|
||||
{% include "footer.html" %}
|
||||
{% include "sidebar.html" %}
|
||||
{% include "sidebar-notifications.html" %}
|
||||
{% include "sidebar-news.html" %}
|
||||
{% if not is_pro_version %}
|
||||
<div class="buy-now">
|
||||
<a class="btn btn-responsive btn-buy-now"
|
||||
|
|
|
|||
|
|
@ -3,21 +3,6 @@
|
|||
<!-- Content -->
|
||||
{% set blacklisted_settings = get_blacklisted_settings(true) %}
|
||||
{% set service_method = "ui" %}
|
||||
{% set plugin_types = {
|
||||
"core": {
|
||||
"icon": "<i class=\"bx bx-cube\"></i>",
|
||||
"title-class": " border-dark"
|
||||
},
|
||||
"external": {
|
||||
"icon": "<i class=\"bx bx-plug\"></i>",
|
||||
"title-class": " border-secondary",
|
||||
"text-class": " text-secondary fw-bold"
|
||||
},
|
||||
"pro": {
|
||||
"title-class": " border-primary",
|
||||
"text-class": " text-primary fw-bold shine"
|
||||
}
|
||||
} %}
|
||||
{% include "models/plugins_settings.html" %}
|
||||
<!-- / Content -->
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<div class="d-flex flex-wrap justify-content-around align-items-center">
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="dropdown btn-group"
|
||||
{% if not files %}data-bs-toggle="tooltip" data-bs-placement="top" title="No log files available"{% endif %}>
|
||||
{% if not files %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="No log files available"{% endif %}>
|
||||
<button type="button"
|
||||
class="btn btn-outline-primary dropdown-toggle{% if not files %} disabled{% endif %}"
|
||||
data-bs-toggle="dropdown"
|
||||
|
|
@ -54,7 +54,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div {% if not request.is_secure %}data-bs-toggle="tooltip" data-bs-placement="top" title="The copy feature is only available over HTTPS"{% endif %}>
|
||||
<div {% 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 id="copy-logs"
|
||||
type="button"
|
||||
class="btn btn-outline-secondary {% if not request.is_secure %}disabled{% endif %}">
|
||||
|
|
@ -65,8 +65,9 @@
|
|||
</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>
|
||||
<div id="raw-logs"
|
||||
class="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 %}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@
|
|||
{% for plugin, plugin_data in plugins.items() %}
|
||||
{% with not_pro_pro_plugin = not is_pro_version and plugin_data['type'] == "pro" %}
|
||||
{% if not_pro_pro_plugin or plugin_data['page'] %}
|
||||
<li class="menu-item{% if current_endpoint == plugin %} active{% endif %}"{% if not_pro_pro_plugin %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-html="true" title="<i class='bx bx-diamond bx-xs'></i><span>Pro feature</span>"
|
||||
<li class="menu-item{% if current_endpoint == plugin %} active{% endif %}"{% if not_pro_pro_plugin %}data-bs-toggle="tooltip" data-bs-placement="top" data-bs-html="true" data-bs-original-title="<i class='bx bx-diamond bx-xs'></i><span>Pro feature</span>"
|
||||
{% endif %}
|
||||
>
|
||||
<a href="{% if not_pro_pro_plugin %}https://panel.bunkerweb.io/?utm_campaign=self&utm_source=ui#pro{% else %}{{ url_for("plugins") }}/{{ plugin }}{% endif %}"
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@
|
|||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
data-bs-html="true"
|
||||
title="<i class='bx bx-rocket bx-xs'></i><span>Coming soon</span>">
|
||||
data-bs-original-title="<i class='bx bx-rocket bx-xs'></i><span>Coming soon</span>">
|
||||
<label for="upload" class="btn btn-primary me-3 mb-4 disabled" tabindex="0">
|
||||
<i class="bx bx-upload me-0 me-sm-1"></i>
|
||||
<span class="d-none d-sm-block">Upload new photo</span>
|
||||
|
|
@ -177,7 +177,7 @@
|
|||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
data-bs-html="true"
|
||||
title="<i class='bx bx-rocket bx-xs'></i><span>Coming soon</span>">
|
||||
data-bs-original-title="<i class='bx bx-rocket bx-xs'></i><span>Coming soon</span>">
|
||||
<option value="light"
|
||||
{% if current_user.theme == "light" %}selected{% endif %}>
|
||||
Light
|
||||
|
|
|
|||
|
|
@ -3,21 +3,6 @@
|
|||
<!-- Content -->
|
||||
{% set blacklisted_settings = get_blacklisted_settings() %}
|
||||
{% set service_method = "ui" %}
|
||||
{% set plugin_types = {
|
||||
"core": {
|
||||
"icon": "<i class=\"bx bx-cube\"></i>",
|
||||
"title-class": " border-dark"
|
||||
},
|
||||
"external": {
|
||||
"icon": "<i class=\"bx bx-plug\"></i>",
|
||||
"title-class": " border-secondary",
|
||||
"text-class": " text-secondary fw-bold"
|
||||
},
|
||||
"pro": {
|
||||
"title-class": " border-primary",
|
||||
"text-class": " text-primary fw-bold shine"
|
||||
}
|
||||
} %}
|
||||
{% set service_method = config.get("SERVER_NAME", {"method": "ui"})["method"] %}
|
||||
{% set is_draft = config.get("IS_DRAFT", {"value": "no"})["value"] %}
|
||||
<input type="hidden"
|
||||
|
|
|
|||
|
|
@ -1,36 +1,36 @@
|
|||
<div class="offcanvas offcanvas-end"
|
||||
tabindex="-1"
|
||||
id="side-offcanvas"
|
||||
id="side-offcanvas-news"
|
||||
data-bs-keyboard="false"
|
||||
data-bs-backdrop="false">
|
||||
<div class="offcanvas-header">
|
||||
<div class="social-buttons">
|
||||
<a href="https://discord.bunkerity.com/?utm_campaign=self&utm_source=ui"
|
||||
class="btn btn-link"
|
||||
class="btn btn-sm btn-link"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
<i class='bx bxl-discord-alt bx-sm'></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bunkerity/"
|
||||
class="btn btn-link"
|
||||
class="btn btn-sm btn-link"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
<i class='bx bxl-linkedin bx-sm'></i>
|
||||
</a>
|
||||
<a href="https://www.reddit.com/r/BunkerWeb/"
|
||||
class="btn btn-link"
|
||||
class="btn btn-sm btn-link"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
<i class='bx bxl-reddit bx-sm'></i>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/bunkerweb/"
|
||||
class="btn btn-link"
|
||||
class="btn btn-sm btn-link"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
<i class='bx bxl-instagram bx-sm'></i>
|
||||
</a>
|
||||
<a href="https://x.com/bunkerity/"
|
||||
class="btn btn-link"
|
||||
class="btn btn-sm btn-link"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
<i class='bx bxl-twitter bx-sm'></i>
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
data-bs-dismiss="offcanvas"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div data-news-container
|
||||
<div id="data-news-container"
|
||||
class="offcanvas-body position-relative ps-3 pe-3">
|
||||
<p class="text-center relative w-full p-2 text-primary rounded-lg fw-bold">Impossible to connect to blog news.</p>
|
||||
</div>
|
||||
108
src/ui/app/templates/sidebar-notifications.html
Normal file
108
src/ui/app/templates/sidebar-notifications.html
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<div class="offcanvas offcanvas-end"
|
||||
tabindex="-1"
|
||||
id="side-offcanvas-notifications"
|
||||
data-bs-keyboard="false"
|
||||
data-bs-backdrop="false">
|
||||
<div class="offcanvas-header">
|
||||
<div class="social-buttons">
|
||||
<a href="https://discord.bunkerity.com/?utm_campaign=self&utm_source=ui"
|
||||
class="btn btn-sm btn-link"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
<i class='bx bxl-discord-alt bx-sm'></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bunkerity/"
|
||||
class="btn btn-sm btn-link"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
<i class='bx bxl-linkedin bx-sm'></i>
|
||||
</a>
|
||||
<a href="https://www.reddit.com/r/BunkerWeb/"
|
||||
class="btn btn-sm btn-link"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
<i class='bx bxl-reddit bx-sm'></i>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/bunkerweb/"
|
||||
class="btn btn-sm btn-link"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
<i class='bx bxl-instagram bx-sm'></i>
|
||||
</a>
|
||||
<a href="https://x.com/bunkerity/"
|
||||
class="btn btn-sm btn-link"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
<i class='bx bxl-twitter bx-sm'></i>
|
||||
</a>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="btn-close text-reset"
|
||||
data-bs-dismiss="offcanvas"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div id="data-notifications-container"
|
||||
class="offcanvas-body row justify-content-center ps-3 pe-3{% if flash_messages %} pt-0{% endif %}">
|
||||
{% if flash_messages %}
|
||||
<div id="feedback-toast-container"
|
||||
class="toast-container position-relative g-3 pt-6">
|
||||
{% for message in flash_messages|reverse %}
|
||||
{% set content = message[0] %}
|
||||
{% set category = message[1] %}
|
||||
{% set datetime = message[2] %}
|
||||
<div class="col-12 bs-toast toast show {% if category == 'error' %}bg-danger{% elif category == 'warning' %}bg-warning{% else %}bg-primary text-white{% endif %}">
|
||||
<div class="toast-header">
|
||||
<i class="d-block w-px-20 h-auto rounded me-2 tf-icons bx bx-bell"></i>
|
||||
<span class="fw-medium me-auto">
|
||||
{% if category != 'message' %}
|
||||
{{ category|capitalize }}
|
||||
{% else %}
|
||||
Success
|
||||
{% endif %}
|
||||
</span>
|
||||
<small class="text-body-secondary toast-datetime">{{ datetime }}</small>
|
||||
</div>
|
||||
<div class="toast-body">{{ content|safe }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<span class="position-absolute top-0 start-50 translate-middle badge rounded-pill bg-secondary">
|
||||
TZ: <script nonce="{{ script_nonce }}">document.write(Intl.DateTimeFormat().resolvedOptions().timeZone);</script>
|
||||
</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="g-3 text-center relative w-full p-2 text-primary rounded-lg fw-bold">No notifications to show</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Newsletter Signup Section -->
|
||||
<div class="newsletter-signup position-sticky bottom-0 start-0 w-100 p-4 bg-white border-top">
|
||||
<h5 class="mb-3 text-dark">Join the Newsletter</h5>
|
||||
<form action="https://bunkerity.us1.list-manage.com/subscribe/post?u=ec5b1577cf427972b9bd491a6&id=37076d9d67"
|
||||
method="POST"
|
||||
id="subscribe-newsletter">
|
||||
<div class="mb-3">
|
||||
<input type="email"
|
||||
id="newsletter-email"
|
||||
name="EMAIL"
|
||||
class="form-control"
|
||||
placeholder="John.doe@example.com"
|
||||
required />
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox"
|
||||
class="form-check-input"
|
||||
id="newsletter-check"
|
||||
name="newsletter-check"
|
||||
required />
|
||||
<label class="form-check-label" for="privacyPolicyCheck">
|
||||
I've read and agree to the
|
||||
<a class="fst-italic"
|
||||
href="https://www.bunkerity.com/en/privacy-policy?utm_campaign=self&utm_source=ui"
|
||||
target="_blank"
|
||||
rel="noopener">privacy policy</a>
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100 text-uppercase">Subscribe</button>
|
||||
</form>
|
||||
</div>
|
||||
<!-- End Newsletter Signup Section -->
|
||||
</div>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from datetime import datetime
|
||||
from os import _exit, getenv
|
||||
from os.path import join, sep
|
||||
from pathlib import Path
|
||||
|
|
@ -7,6 +8,7 @@ from subprocess import PIPE, Popen, call
|
|||
from typing import Dict, List, Optional, Set
|
||||
|
||||
from bcrypt import checkpw, gensalt, hashpw
|
||||
from flask import flash as flask_flash, session
|
||||
from magic import Magic
|
||||
from regex import compile as re_compile, match
|
||||
from requests import get
|
||||
|
|
@ -267,3 +269,13 @@ def get_latest_stable_release():
|
|||
if not release["prerelease"]:
|
||||
return release
|
||||
return None
|
||||
|
||||
|
||||
def flash(message: str, category: str = "success", *, save: bool = True) -> None:
|
||||
if category != "success":
|
||||
flask_flash(message, category)
|
||||
else:
|
||||
flask_flash(message)
|
||||
|
||||
if save and "flash_messages" in session:
|
||||
session["flash_messages"].append((message, category, datetime.now().astimezone().isoformat()))
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ for deps_path in [join(sep, "usr", "share", "bunkerweb", *paths) for paths in ((
|
|||
if deps_path not in sys_path:
|
||||
sys_path.append(deps_path)
|
||||
|
||||
from flask import Flask, Response, flash, jsonify, make_response, redirect, render_template, request, session, url_for
|
||||
from flask import Flask, Response, flash as flask_flash, jsonify, make_response, redirect, render_template, request, session, url_for
|
||||
from flask_executor import Executor
|
||||
from flask_login import current_user, LoginManager, login_required, logout_user
|
||||
from flask_principal import ActionNeed, identity_loaded, Permission, Principal, RoleNeed, TypeNeed, UserNeed
|
||||
|
|
@ -41,7 +41,7 @@ from app.routes.totp import totp
|
|||
|
||||
from app.dependencies import BW_CONFIG, DATA, DB
|
||||
from app.models.models import AnonymousUser
|
||||
from app.utils import TMP_DIR, LOGGER, get_blacklisted_settings, get_filtered_settings, get_latest_stable_release, get_multiples, handle_stop, stop
|
||||
from app.utils import TMP_DIR, LOGGER, flash, get_blacklisted_settings, get_filtered_settings, get_latest_stable_release, get_multiples, handle_stop, stop
|
||||
|
||||
signal(SIGINT, handle_stop)
|
||||
signal(SIGTERM, handle_stop)
|
||||
|
|
@ -147,23 +147,14 @@ def inject_variables():
|
|||
flash("The last changes have been applied successfully.", "success")
|
||||
DATA["CONFIG_CHANGED"] = False
|
||||
|
||||
services = BW_CONFIG.get_config(global_only=True, with_drafts=True, methods=False, filtered_settings=("SERVER_NAME"))["SERVER_NAME"].split(" ")
|
||||
|
||||
# check that is value is in tuple
|
||||
return dict(
|
||||
script_nonce=app.config["SCRIPT_NONCE"],
|
||||
bw_version=metadata["version"],
|
||||
latest_version=DATA.get("LATEST_VERSION", "unknown"),
|
||||
is_pro_version=metadata["is_pro"],
|
||||
services=services,
|
||||
pro_status=metadata["pro_status"],
|
||||
pro_services=metadata["pro_services"],
|
||||
pro_expire=metadata["pro_expire"].strftime("%Y-%m-%d") if metadata["pro_expire"] else "Unknown",
|
||||
pro_overlapped=metadata["pro_overlapped"],
|
||||
plugins=BW_CONFIG.get_plugins(),
|
||||
pro_loading=DATA.get("PRO_LOADING", False),
|
||||
flash_messages=session.get("flash_messages", []),
|
||||
is_readonly=DATA.get("READONLY_MODE", False),
|
||||
username=current_user.get_id() if current_user.is_authenticated else "",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -187,7 +178,7 @@ def load_user(username):
|
|||
and session.get("totp_validated", False)
|
||||
and not ui_user.list_recovery_codes
|
||||
):
|
||||
flash(
|
||||
flask_flash(
|
||||
f"""The two-factor authentication is enabled but no recovery codes are available, please refresh them:
|
||||
<div class="mt-2 pt-2 border-top border-white">
|
||||
<a role='button' class='btn btn-sm btn-dark d-flex align-items-center' aria-pressed='true' href='{url_for('profile.profile_page')}'>here</a>
|
||||
|
|
@ -227,7 +218,7 @@ def handle_csrf_error(_):
|
|||
LOGGER.error(f"CSRF token is missing or invalid for {request.path} by {current_user.get_id()}")
|
||||
session.clear()
|
||||
logout_user()
|
||||
flash("Wrong CSRF token !", "error")
|
||||
flask_flash("Wrong CSRF token !", "error")
|
||||
if not current_user:
|
||||
return redirect(url_for("setup.setup_page")), 403
|
||||
return redirect(url_for("login.login_page")), 403
|
||||
|
|
@ -303,7 +294,7 @@ def before_request():
|
|||
DB.readonly = DATA.get("READONLY_MODE", False)
|
||||
|
||||
if DB.readonly:
|
||||
flash("Database connection is in read-only mode : no modification possible.", "error")
|
||||
flask_flash("Database connection is in read-only mode : no modification possible.", "error")
|
||||
|
||||
if current_user.is_authenticated:
|
||||
passed = True
|
||||
|
|
@ -405,17 +396,11 @@ def check_reloading():
|
|||
if not DATA.get("RELOADING", False) or DATA.get("LAST_RELOAD", 0) + 60 < time():
|
||||
if DATA.get("RELOADING", False):
|
||||
LOGGER.warning("Reloading took too long, forcing the state to be reloaded")
|
||||
flash("Forced the status to be reloaded", "error")
|
||||
flask_flash("Forced the status to be reloaded", "error")
|
||||
DATA["RELOADING"] = False
|
||||
|
||||
for f in DATA.get("TO_FLASH", []):
|
||||
if f["type"] != "success":
|
||||
flash(f["content"], f["type"])
|
||||
else:
|
||||
flash(f["content"])
|
||||
|
||||
if "flash_messages" in session:
|
||||
session["flash_messages"].append((f["content"], f["type"], datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S %Z")))
|
||||
flash(f["content"], f["type"], save=f.get("save", True))
|
||||
|
||||
DATA["TO_FLASH"] = []
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue