Add notification sidebar to web UI with back-end logic

This commit is contained in:
Théophile Diot 2024-09-21 17:51:22 +02:00
parent 4f9e0e1fea
commit e723912588
No known key found for this signature in database
GPG key ID: FA995104A0BA376A
30 changed files with 274 additions and 163 deletions

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"] = []

View file

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

View file

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

View file

@ -18,4 +18,7 @@ $(document).ready(function () {
});
editor.renderer.setScrollMargin(10, 10);
editorElement.removeClass("visually-hidden");
$("#cache-waiting").addClass("visually-hidden");
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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>&nbsp;{{ 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>

View file

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

View file

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

View file

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

View file

@ -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 %}"

View file

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

View file

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

View file

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

View 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&amp;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>

View file

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

View file

@ -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"] = []