Add setup wizard page and optimize a few pages

This commit is contained in:
Théophile Diot 2024-09-30 11:52:36 +02:00
parent 970874e983
commit d7863f8df2
No known key found for this signature in database
GPG key ID: FA995104A0BA376A
23 changed files with 1586 additions and 150 deletions

View file

@ -8,25 +8,78 @@ access_by_lua_block {
return ngx.redirect("https://" .. http_host .. request_uri, ngx.HTTP_MOVED_PERMANENTLY)
end
}
set $backendui "{{ UI_HOST }}";
# Serve CSS, Fonts, Images, JavaScript and Libs without modifying the response body
location ~ ^/setup/(css|fonts|img|js|libs)(.*)$ {
# Capture the asset type and the remaining path
# Example: /setup/js/app.js -> /js/app.js
proxy_pass $backendui/$1$2;
# Proxy headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Prefix "/";
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Timeouts
proxy_connect_timeout 60s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
# Buffering
proxy_buffering on;
}
location /setup {
etag off;
add_header Last-Modified "";
set $backendui "{{ UI_HOST }}";
proxy_pass $backendui;
# Proxy headers
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Prefix "/";
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering on;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Timeouts
proxy_connect_timeout 60s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
# Buffering
proxy_buffering on;
# Response body modifications
body_filter_by_lua_block {
local str = ngx.arg[1]
local patterns = {
["/css"] = "/setup/css",
["/fonts"] = "/setup/fonts",
["/img"] = "/setup/img",
["/js"] = "/setup/js",
["/libs"] = "/setup/libs",
}
for pattern, replacement in pairs(patterns) do
str = ngx.re.gsub(str, pattern, replacement, "ijo")
end
ngx.arg[1] = str
}
}
location /setup/check {

View file

@ -165,7 +165,7 @@ class InstancesUtils:
def __init__(self, db):
self.__db = db
def get_instances(self) -> list[Instance]:
def get_instances(self, status: Optional[Literal["loading", "up", "down"]] = None) -> List[Instance]:
return [
Instance(
instance["hostname"],
@ -185,6 +185,7 @@ class InstancesUtils:
),
)
for instance in self.__db.get_instances()
if not status or instance["status"] == status
]
def reload_instances(self, *, instances: Optional[List[Instance]] = None) -> Union[list[str], str]:
@ -193,10 +194,10 @@ class InstancesUtils:
] or "Successfully reloaded instances"
def ban(self, ip: str, exp: float, reason: str, *, instances: Optional[List[Instance]] = None) -> Union[list[str], str]:
return [instance.name for instance in instances or self.get_instances() if instance.ban(ip, exp, reason).startswith("Can't ban")] or ""
return [instance.name for instance in instances or self.get_instances(status="up") if instance.ban(ip, exp, reason).startswith("Can't ban")] or ""
def unban(self, ip: str, *, instances: Optional[List[Instance]] = None) -> Union[list[str], str]:
return [instance.name for instance in instances or self.get_instances() if instance.unban(ip).startswith("Can't unban")] or ""
return [instance.name for instance in instances or self.get_instances(status="up") if instance.unban(ip).startswith("Can't unban")] or ""
def get_bans(self, hostname: Optional[str] = None, *, instances: Optional[List[Instance]] = None) -> List[dict[str, Any]]:
"""Get unique bans from all instances or a specific instance and sort them by expiration date"""
@ -214,7 +215,7 @@ class InstancesUtils:
return []
bans = get_instance_bans(instance)
else:
for instance in instances or self.get_instances():
for instance in instances or self.get_instances(status="up"):
bans.extend(get_instance_bans(instance))
unique_bans = {}
@ -236,7 +237,7 @@ class InstancesUtils:
return []
reports = get_instance_reports(instance)
else:
for instance in instances or self.get_instances():
for instance in instances or self.get_instances(status="up"):
reports.extend(get_instance_reports(instance))
return sorted(reports, key=itemgetter("date"), reverse=True)
@ -311,14 +312,14 @@ class InstancesUtils:
return {}
return update_metrics_from_instance(instance, metrics.copy())
for instance in instances or self.get_instances():
for instance in instances or self.get_instances(status="up"):
metrics = update_metrics_from_instance(instance, metrics.copy())
return metrics
def get_ping(self, plugin_id: str, *, instances: Optional[List[Instance]] = None):
"""Get ping from all instances and return the first success"""
ping = {"status": "error"}
for instance in instances or self.get_instances():
for instance in instances or self.get_instances(status="up"):
try:
resp, ping_data = instance.ping(plugin_id)
except:
@ -336,7 +337,7 @@ class InstancesUtils:
def get_data(self, plugin_endpoint: str, *, instances: Optional[List[Instance]] = None):
"""Get data from all instances and return the first success"""
data = []
for instance in instances or self.get_instances():
for instance in instances or self.get_instances(status="up"):
try:
resp, instance_data = instance.data(plugin_endpoint)
except:

View file

@ -4,9 +4,12 @@ from string import ascii_letters, digits
from threading import Thread
from time import time
from flask import Blueprint, Response, flash, redirect, render_template, request, url_for
from flask import Blueprint, Response, flash, redirect, render_template, request, session, url_for
from flask_login import current_user
from app.dependencies import BW_CONFIG, DATA, DB # TODO: remember about DATA.load_from_file()
from app.models.totp import totp as TOTP
from app.dependencies import BW_CONFIG, DATA, DB
from app.utils import USER_PASSWORD_RX, gen_password_hash
from app.routes.utils import REVERSE_PROXY_PATH, handle_error, manage_bunkerweb
@ -16,7 +19,12 @@ setup = Blueprint("setup", __name__)
@setup.route("/setup", methods=["GET", "POST"])
def setup_page():
db_config = BW_CONFIG.get_config(methods=False, filtered_settings=("SERVER_NAME", "MULTISITE", "USE_UI", "UI_HOST", "AUTO_LETS_ENCRYPT"))
if current_user.is_authenticated:
return redirect(url_for("home.home_page"))
db_config = BW_CONFIG.get_config(
methods=False,
filtered_settings=("SERVER_NAME", "MULTISITE", "USE_UI", "UI_HOST", "AUTO_LETS_ENCRYPT", "USE_LETS_ENCRYPT_STAGING", "EMAIL_LETS_ENCRYPT"),
)
admin_user = DB.get_ui_user()
@ -34,9 +42,9 @@ def setup_page():
required_keys = []
if not ui_reverse_proxy:
required_keys.extend(["server_name", "ui_host", "ui_url"])
required_keys.extend(["server_name", "ui_host", "ui_url", "auto_lets_encrypt", "lets_encrypt_staging", "email_lets_encrypt"])
if not admin_user:
required_keys.extend(["admin_username", "admin_password", "admin_password_check"])
required_keys.extend(["admin_username", "admin_email", "admin_password", "admin_password_check", "2fa_code"])
if not any(key in request.form for key in required_keys):
return handle_error(f"Missing either one of the following parameters: {', '.join(required_keys)}.", "setup")
@ -44,17 +52,36 @@ def setup_page():
if not admin_user:
if len(request.form["admin_username"]) > 256:
return handle_error("The admin username is too long. It must be less than 256 characters.", "setup")
if request.form["admin_password"] != request.form["admin_password_check"]:
elif len(request.form["admin_email"]) > 256:
return handle_error("The admin email is too long. It must be less than 256 characters.", "setup")
elif request.form["admin_password"] != request.form["admin_password_check"]:
return handle_error("The passwords do not match.", "setup")
if not USER_PASSWORD_RX.match(request.form["admin_password"]):
elif not USER_PASSWORD_RX.match(request.form["admin_password"]):
return handle_error(
"The admin password is not strong enough. It must contain at least 8 characters, including at least 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character (#@?!$%^&*-).",
"setup",
)
ret = DB.create_ui_user(request.form["admin_username"], gen_password_hash(request.form["admin_password"]), ["admin"], method="ui", admin=True)
totp_secret = None
totp_recovery_codes = None
if request.form["2fa_code"]:
totp_secret = session.pop("tmp_totp_secret", "")
if not TOTP.verify_totp(request.form["2fa_code"], totp_secret=totp_secret, user=current_user):
return handle_error("The totp token is invalid.", "setup")
totp_recovery_codes = TOTP.generate_recovery_codes()
ret = DB.create_ui_user(
request.form["admin_username"],
gen_password_hash(request.form["admin_password"]),
["admin"],
request.form["admin_email"] or None,
totp_secret=totp_secret,
totp_recovery_codes=totp_recovery_codes,
method="ui",
admin=True,
)
if ret:
return handle_error(f"Couldn't create the admin user in the database: {ret}", "setup", False, "error")
@ -81,13 +108,20 @@ def setup_page():
"USE_REVERSE_PROXY": "yes",
"REVERSE_PROXY_HOST": request.form["ui_host"],
"REVERSE_PROXY_URL": request.form["ui_url"] or "/",
"USE_LETS_ENCRYPT_STAGING": request.form["lets_encrypt_staging"],
"EMAIL_LETS_ENCRYPT": request.form["email_lets_encrypt"],
}
if request.form.get("auto_lets_encrypt", "no") == "yes":
config["AUTO_LETS_ENCRYPT"] = "yes"
else:
config["GENERATE_SELF_SIGNED_SSL"] = "yes"
config["SELF_SIGNED_SSL_SUBJ"] = f"/CN={request.form['server_name']}/"
config.update(
{
"USE_CUSTOM_SSL": "yes",
"CUSTOM_SSL_CERT": "/var/cache/bunkerweb/misc/default-server-cert.pem",
"CUSTOM_SSL_KEY": "/var/cache/bunkerweb/misc/default-server-cert.key",
}
)
if not config.get("MULTISITE", "no") == "yes":
BW_CONFIG.edit_global_conf({"MULTISITE": "yes"}, check_changes=False)
@ -102,6 +136,9 @@ def setup_page():
return Response(status=200)
session["tmp_totp_secret"] = TOTP.generate_totp_secret()
totp_qr_image = TOTP.generate_qrcode(current_user.get_id(), session["tmp_totp_secret"])
return render_template(
"setup.html",
ui_user=admin_user,
@ -109,11 +146,30 @@ def setup_page():
username=getenv("ADMIN_USERNAME", ""),
password=getenv("ADMIN_PASSWORD", ""),
ui_host=db_config.get("UI_HOST", getenv("UI_HOST", "")),
auto_lets_encrypt=db_config.get("AUTO_LETS_ENCRYPT", getenv("AUTO_LETS_ENCRYPT", "no")) == "yes",
auto_lets_encrypt=db_config.get("AUTO_LETS_ENCRYPT", getenv("AUTO_LETS_ENCRYPT", "no")),
lets_encrypt_staging=db_config.get("USE_LETS_ENCRYPT_STAGING", getenv("USE_LETS_ENCRYPT_STAGING", "no")),
email_lets_encrypt=db_config.get("EMAIL_LETS_ENCRYPT", getenv("EMAIL_LETS_ENCRYPT", "")),
random_url=f"/{''.join(choice(ascii_letters + digits) for _ in range(10))}",
totp_qr_image=totp_qr_image,
totp_secret=TOTP.get_totp_pretty_key(session.get("tmp_totp_secret", "")),
)
@setup.route("/setup/loading", methods=["GET"])
def setup_loading():
return render_template("setup_loading.html")
if current_user.is_authenticated:
return redirect(url_for("home.home_page"))
if DB.get_ui_user():
db_config = BW_CONFIG.get_config(methods=False, filtered_settings=("SERVER_NAME", "USE_UI", "REVERSE_PROXY_URL"))
for server_name in db_config["SERVER_NAME"].split(" "):
if server_name and db_config.get(f"{server_name}_USE_UI", db_config.get("USE_UI", "no")) == "yes":
return redirect(url_for("login.login_page"), 301)
target_endpoint = request.args.get("target_endpoint", "")
return render_template(
"loading.html",
message="Setting up Web UI...",
target_endpoint=target_endpoint,
next=target_endpoint.replace("/check", "/login") if target_endpoint else url_for("login.login_page"),
)

View file

@ -344,6 +344,11 @@ td.highlight {
color: var(--bs-bw-green) !important;
}
.btn-primary.active {
background-color: var(--bs-primary) !important;
border-color: var(--bs-primary) !important;
}
.btn-bw-green {
color: #fff;
background-color: var(--bs-bw-green);
@ -742,3 +747,15 @@ a.text-decoration-underline.link-underline-primary:hover {
color: var(--bs-primary) !important;
text-decoration: underline;
}
.authentication-wrapper.authentication-basic .authentication-inner.w-100 {
max-width: 90% !important;
}
.form-floating > .form-control:focus,
.form-floating > .form-control:not(:placeholder-shown),
.form-floating > .form-control-plaintext:focus,
.form-floating > .form-control-plaintext:not(:placeholder-shown) {
padding-top: 1.625rem !important;
padding-bottom: 0.625rem !important;
}

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 107.3 113.4">
<defs>
<style>
.cls-1 {
fill: #2eac68;
}
.cls-2 {
fill: #fff;
}
</style>
</defs>
<!-- Generator: Adobe Illustrator 28.7.1, SVG Export Plug-In . SVG Version: 1.2.0 Build 142) -->
<g>
<g id="Calque_1">
<g>
<path class="cls-2" d="M97.5,66.6c-.7-6.7-2.1-14.1-8.5-17.7-2.7-1.5-3.7-5.2-1.9-7.8,3.9-5.8,3.7-12.6,3-19.2-1.8-15.9-15.1-22.5-39.9-19.2-15.8,2.1-23.7,3.2-39.4,5.6-3.6.6-6.1,3.7-5.5,7.1,3.7,23.4,7.4,46.7,11.1,70.1h76.5c4-4.9,5.6-11.2,4.7-18.9ZM44.8,71.4c-.3-6.9-.5-10.3-.8-17.2-2.9-1.6-5-4.4-5.5-7.8-.8-5.9,3.5-11.4,9.7-12.2,6.2-.8,11.8,3.3,12.5,9.2.4,3.4-.9,6.7-3.2,9,1.5,6.7,2.3,10.1,3.8,16.8-6.6.9-9.9,1.3-16.5,2.2Z"/>
<g>
<rect class="cls-1" x="8.6" y="89" width="93.7" height="24.5"/>
<g>
<path class="cls-2" d="M40.2,111h-1.9s-3.4-9.9-3.4-9.9l-3.3,9.9h-1.9s-3.2-14.3-3.2-14.3h-.8c-.4,0-.6,0-.8-.2s-.2-.3-.2-.6,0-.4.2-.6.4-.2.8-.2h4.2c.4,0,.6,0,.8.2s.2.3.2.6,0,.4-.2.6-.4.2-.8.2h-2s2.8,12.2,2.8,12.2l3.3-9.8h1.8s3.4,9.7,3.4,9.7l2.6-12.2h-1.9c-.4,0-.6,0-.8-.2-.2-.1-.2-.3-.2-.6s0-.4.2-.6.4-.2.8-.2h4.2c.4,0,.6,0,.8.2s.2.3.2.6,0,.4-.2.6-.4.2-.8.2h-.7s-3.1,14.4-3.1,14.4Z"/>
<path class="cls-2" d="M65.8,103.3h-15.7c.3,2.1,1.1,3.7,2.5,4.9,1.4,1.2,3.1,1.8,5.2,1.8s2.4-.2,3.6-.6c1.3-.4,2.3-.9,3.1-1.5.2-.2.4-.3.6-.3s.4,0,.5.2c.1.2.2.3.2.5s0,.4-.3.6c-.6.6-1.6,1.2-3.1,1.7-1.5.5-3,.8-4.6.8-2.6,0-4.9-.8-6.6-2.6-1.8-1.7-2.7-3.8-2.7-6.3s.8-4.2,2.5-5.8c1.7-1.6,3.7-2.4,6.2-2.4s4.6.8,6.2,2.4c1.6,1.6,2.4,3.8,2.4,6.4ZM64.2,101.8c-.3-1.7-1.1-3.1-2.4-4.1-1.3-1.1-2.9-1.6-4.7-1.6-1.8,0-3.3.5-4.6,1.6-1.3,1.1-2.1,2.5-2.4,4.2h14.1Z"/>
<path class="cls-2" d="M74.1,87.8v10.2c1.9-2.4,4.1-3.7,6.8-3.7s4.2.8,5.8,2.4c1.6,1.6,2.4,3.6,2.4,6s-.8,4.4-2.4,6.1c-1.6,1.7-3.5,2.5-5.7,2.5s-4.9-1.2-6.8-3.6v3s-3.6,0-3.6,0c-.4,0-.6,0-.8-.2s-.2-.3-.2-.5,0-.4.2-.6c.2-.1.4-.2.8-.2h2.1s0-19.9,0-19.9h-2.1c-.4,0-.6,0-.8-.2s-.2-.3-.2-.6,0-.4.2-.6.4-.2.8-.2h3.6ZM87.6,102.9c0-1.9-.7-3.6-2-4.9-1.3-1.3-2.9-2-4.7-2s-3.3.7-4.7,2c-1.3,1.4-2,3-2,5s.7,3.6,2,4.9,2.9,2,4.7,2,3.3-.7,4.7-2c1.3-1.4,2-3,2-5Z"/>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 107.3 113.4">
<defs>
<style>
.cls-1 {
fill: #0b5577;
}
.cls-2 {
fill: #2eac68;
}
.cls-3 {
fill: #fff;
}
</style>
</defs>
<!-- Generator: Adobe Illustrator 28.7.1, SVG Export Plug-In . SVG Version: 1.2.0 Build 142) -->
<g>
<g id="Calque_1">
<g>
<path class="cls-1" d="M97.5,66.6c-.7-6.7-2.1-14.1-8.5-17.7-2.7-1.5-3.7-5.2-1.9-7.8,3.9-5.8,3.7-12.6,3-19.2-1.8-15.9-15.1-22.5-39.9-19.2-15.8,2.1-23.7,3.2-39.4,5.6-3.6.6-6.1,3.7-5.5,7.1,3.7,23.4,7.4,46.7,11.1,70.1h76.5c4-4.9,5.6-11.2,4.7-18.9ZM44.8,71.4c-.3-6.9-.5-10.3-.8-17.2-2.9-1.6-5-4.4-5.5-7.8-.8-5.9,3.5-11.4,9.7-12.2,6.2-.8,11.8,3.3,12.5,9.2.4,3.4-.9,6.7-3.2,9,1.5,6.7,2.3,10.1,3.8,16.8-6.6.9-9.9,1.3-16.5,2.2Z"/>
<g>
<rect class="cls-2" x="8.6" y="89" width="93.7" height="24.5"/>
<g>
<path class="cls-3" d="M40.2,111h-1.9s-3.4-9.9-3.4-9.9l-3.3,9.9h-1.9s-3.2-14.3-3.2-14.3h-.8c-.4,0-.6,0-.8-.2s-.2-.3-.2-.6,0-.4.2-.6.4-.2.8-.2h4.2c.4,0,.6,0,.8.2s.2.3.2.6,0,.4-.2.6-.4.2-.8.2h-2s2.8,12.2,2.8,12.2l3.3-9.8h1.8s3.4,9.7,3.4,9.7l2.6-12.2h-1.9c-.4,0-.6,0-.8-.2-.2-.1-.2-.3-.2-.6s0-.4.2-.6.4-.2.8-.2h4.2c.4,0,.6,0,.8.2s.2.3.2.6,0,.4-.2.6-.4.2-.8.2h-.7s-3.1,14.4-3.1,14.4Z"/>
<path class="cls-3" d="M65.8,103.3h-15.7c.3,2.1,1.1,3.7,2.5,4.9,1.4,1.2,3.1,1.8,5.2,1.8s2.4-.2,3.6-.6c1.3-.4,2.3-.9,3.1-1.5.2-.2.4-.3.6-.3s.4,0,.5.2c.1.2.2.3.2.5s0,.4-.3.6c-.6.6-1.6,1.2-3.1,1.7-1.5.5-3,.8-4.6.8-2.6,0-4.9-.8-6.6-2.6-1.8-1.7-2.7-3.8-2.7-6.3s.8-4.2,2.5-5.8c1.7-1.6,3.7-2.4,6.2-2.4s4.6.8,6.2,2.4c1.6,1.6,2.4,3.8,2.4,6.4ZM64.2,101.8c-.3-1.7-1.1-3.1-2.4-4.1-1.3-1.1-2.9-1.6-4.7-1.6-1.8,0-3.3.5-4.6,1.6-1.3,1.1-2.1,2.5-2.4,4.2h14.1Z"/>
<path class="cls-3" d="M74.1,87.8v10.2c1.9-2.4,4.1-3.7,6.8-3.7s4.2.8,5.8,2.4c1.6,1.6,2.4,3.6,2.4,6s-.8,4.4-2.4,6.1c-1.6,1.7-3.5,2.5-5.7,2.5s-4.9-1.2-6.8-3.6v3s-3.6,0-3.6,0c-.4,0-.6,0-.8-.2s-.2-.3-.2-.5,0-.4.2-.6c.2-.1.4-.2.8-.2h2.1s0-19.9,0-19.9h-2.1c-.4,0-.6,0-.8-.2s-.2-.3-.2-.6,0-.4.2-.6.4-.2.8-.2h3.6ZM87.6,102.9c0-1.9-.7-3.6-2-4.9-1.3-1.3-2.9-2-4.7-2s-3.3.7-4.7,2c-1.3,1.4-2,3-2,5s.7,3.6,2,4.9,2.9,2,4.7,2,3.3-.7,4.7-2c1.3-1.4,2-3,2-5Z"/>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -20,7 +20,7 @@ $(document).ready(function () {
isValid,
);
isValid = validateCondition(
/[ !"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/.test(password),
/[ -~]/.test(password),
"#special-check i",
isValid,
);
@ -83,17 +83,32 @@ $(document).ready(function () {
const target = targetClass.substring(1).replace("navs-pills-", "");
const isProfileTab = target === "profile";
const isSessionsTab = target === "sessions";
const sessionsPagination = $("#navs-pills-sessions-pagination");
if (!isSessionsTab) {
$("#navs-pills-sessions-pagination").removeClass("show active");
setTimeout(() => {
$("#navs-pills-sessions-pagination").parent().addClass("d-none");
}, 200);
} else {
$("#navs-pills-sessions-pagination").parent().removeClass("d-none");
setTimeout(() => {
$("#navs-pills-sessions-pagination").addClass("show active");
}, 200);
if (sessionsPagination.length) {
if (!isSessionsTab) {
sessionsPagination.removeClass("show active");
setTimeout(() => {
sessionsPagination.parent().addClass("d-none");
$(".nav-pills .tf-icons")
.parent()
.find("span")
.removeClass("d-xxxl-inline")
.addClass("d-sm-inline");
$(".nav-pills .tf-icons").closest("div").removeClass("col-md-4");
}, 200);
} else {
sessionsPagination.parent().removeClass("d-none");
setTimeout(() => {
sessionsPagination.addClass("show active");
$(".nav-pills .tf-icons")
.parent()
.find("span")
.removeClass("d-sm-inline")
.addClass("d-xxxl-inline");
$(".nav-pills .tf-icons").closest("div").addClass("col-md-4");
}, 200);
}
}
if (isProfileTab && window.location.hash) {

View file

@ -421,26 +421,29 @@ $(function () {
$("#reports").removeClass("d-none");
$("#reports-waiting").addClass("visually-hidden");
let lastRowIdx = null;
reports_table.on("mouseenter", "td", function () {
const cellIdx = reports_table.cell(this).index();
if (!cellIdx) return;
const rowIdx = cellIdx.row;
if (reports_table.cell(this).index() === undefined) return;
const rowIdx = reports_table.cell(this).index().row;
if (lastRowIdx !== null && lastRowIdx !== rowIdx) {
reports_table.row(lastRowIdx).nodes().to$().removeClass("highlight");
}
reports_table
.cells()
.nodes()
.each((el) => el.classList.remove("highlight"));
reports_table.row(rowIdx).nodes().to$().addClass("highlight");
lastRowIdx = rowIdx;
reports_table
.cells()
.nodes()
.each(function (el) {
if (reports_table.cell(el).index().row === rowIdx)
el.classList.add("highlight");
});
});
reports_table.on("mouseleave", "td", function () {
if (lastRowIdx !== null) {
reports_table.row(lastRowIdx).nodes().to$().removeClass("highlight");
lastRowIdx = null;
}
reports_table
.cells()
.nodes()
.each((el) => el.classList.remove("highlight"));
});
reports_table.on("draw.dt", updateCountryTooltips);

View file

@ -419,26 +419,29 @@ $(function () {
$("#services").removeClass("d-none");
$("#services-waiting").addClass("visually-hidden");
let lastRowIdx = null;
services_table.on("mouseenter", "td", function () {
const cellIdx = services_table.cell(this).index();
if (!cellIdx) return;
const rowIdx = cellIdx.row;
if (services_table.cell(this).index() === undefined) return;
const rowIdx = services_table.cell(this).index().row;
if (lastRowIdx !== null && lastRowIdx !== rowIdx) {
services_table.row(lastRowIdx).nodes().to$().removeClass("highlight");
}
services_table
.cells()
.nodes()
.each((el) => el.classList.remove("highlight"));
services_table.row(rowIdx).nodes().to$().addClass("highlight");
lastRowIdx = rowIdx;
services_table
.cells()
.nodes()
.each(function (el) {
if (services_table.cell(el).index().row === rowIdx)
el.classList.add("highlight");
});
});
services_table.on("mouseleave", "td", function () {
if (lastRowIdx !== null) {
services_table.row(lastRowIdx).nodes().to$().removeClass("highlight");
lastRowIdx = null;
}
services_table
.cells()
.nodes()
.each((el) => el.classList.remove("highlight"));
});
// Event listener for the select-all checkbox

View file

@ -0,0 +1,554 @@
$(document).ready(() => {
// Initialize variables
let toastNum = 1;
let currentStep = 1;
const CHECK_STEP = 3;
const uiUser = $("#ui_user").val() === "yes";
const uiReverseProxy = $("#ui_reverse_proxy").val() === "yes";
// Cache jQuery selectors for performance
const $window = $(window);
const $passwordInput = $("#password");
const $2faInput = $("#2fa_code");
const $confirmPasswordInput = $("#confirm_password");
const $serverNameInput = $("#SERVER_NAME");
const $overview2faEnabled = $("#overview-2fa-enabled");
const $overviewUniqueServerName = $("#overview-unique-server-name");
const $saveSettingsButton = $(".save-settings");
const $previousStepButton = $(".previous-step");
const $nextStepButton = $(".next-step");
const $breadcrumbItems = $(".template-steps-container .breadcrumb-item");
const $csrfTokenInput = $("#csrf_token");
// Utility Functions
/**
* Validates a password based on multiple conditions.
* @returns {boolean} True if the password is valid, else false.
*/
const validatePassword = () => {
const password = $passwordInput.val();
let isValid = true;
isValid = validateCondition(
password.length >= 8,
"#length-check i",
isValid,
);
isValid = validateCondition(
/[A-Z]/.test(password),
"#uppercase-check i",
isValid,
);
isValid = validateCondition(
/\d/.test(password),
"#number-check i",
isValid,
);
isValid = validateCondition(
/[ -~]/.test(password),
"#special-check i",
isValid,
); // Check for special characters
return isValid;
};
/**
* Updates the UI based on a validation condition.
* @param {boolean} condition - The validation condition.
* @param {string} selector - The selector for the UI element to update.
* @param {boolean} currentValidity - The current validity state.
* @returns {boolean} Updated validity state.
*/
const validateCondition = (condition, selector, currentValidity) => {
const $element = $(selector);
if (condition) {
$element
.removeClass("bx-x text-danger")
.addClass("bx-check text-success");
} else {
$element
.removeClass("bx-check text-success")
.addClass("bx-x text-danger");
}
return currentValidity && condition;
};
/**
* Toggles validation classes based on the validity of an input.
* @param {string} selector - The selector for the input element.
* @param {boolean} isValid - The validity state.
*/
const updateValidationState = (selector, isValid) => {
$(selector)
.toggleClass("is-valid", isValid)
.toggleClass("is-invalid", !isValid);
};
/**
* Debounce function to limit the rate at which a function can fire.
* @param {Function} func - The function to debounce.
* @param {number} delay - The delay in milliseconds.
* @returns {Function} Debounced function.
*/
const debounce = (func, delay) => {
let debounceTimer;
return function (...args) {
const context = this;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => func.apply(context, args), delay);
};
};
/**
* Extracts and encodes the server name from the input.
* @returns {string} Encoded server name.
*/
const getServerName = () => {
const serverName = $serverNameInput.val().trim().split(" ")[0];
return encodeURIComponent(serverName); // Encode to prevent injection
};
/**
* Performs a DNS check by fetching the given URL.
* @param {string} url - The URL to fetch.
* @returns {Promise<boolean>} True if the check is successful, else false.
*/
const fetchCheck = async (url) => {
try {
const response = await fetch(url, { cache: "no-store" }); // Prevent caching
const text = (await response.text()).trim().toLowerCase();
if (response.status === 200 && text === "ok") {
return true;
}
return false;
} catch (error) {
return false;
}
};
/**
* Handles the DNS checking logic with primary and fallback URLs.
*/
const checkDNS = async () => {
$overviewUniqueServerName
.find("i")
.toggleClass("bx-check text-success bx-x text-danger", false)
.toggleClass("bx-question-mark text-warning", true);
const serverName = getServerName();
if (!serverName) {
$checkResultSpan.text("Invalid server name.");
return;
}
const primaryURL = `https://${serverName}/setup/check`;
const fallbackURL = `${window.location.origin}/setup/check?server_name=${serverName}`;
let isSuccess = await fetchCheck(primaryURL);
if (!isSuccess) {
isSuccess = await fetchCheck(fallbackURL);
}
$overviewUniqueServerName
.find("i")
.toggleClass("bx-question-mark text-warning", false);
const feedbackToast = $("#feedback-toast").clone(); // Clone the feedback toast
feedbackToast.attr("id", `feedback-toast-${toastNum++}`); // Corrected to set the ID for the toast
if (!isSuccess) {
feedbackToast.removeClass("bg-primary");
feedbackToast.addClass("bg-danger");
feedbackToast.find("span").text("Server name is not unique.");
feedbackToast
.find("div.toast-body")
.text("Please choose a different server name.");
$overviewUniqueServerName
.find("i")
.toggleClass("bx-check text-success", false)
.toggleClass("bx-x text-danger", true);
} else {
feedbackToast.find("span").text("Server name is unique.");
feedbackToast
.find("div.toast-body")
.text("You can proceed with the setup.");
$overviewUniqueServerName
.find("i")
.toggleClass("bx-check text-success", true)
.toggleClass("bx-x text-danger", false);
}
feedbackToast.appendTo("#feedback-toast-container"); // Ensure the toast is appended to the container
feedbackToast.toast("show");
};
/**
* Validates all inputs within the current step.
* @param {jQuery} $currentStepContainer - The container of the current step.
* @returns {boolean} True if the step is valid, else false.
*/
const validateCurrentStepInputs = ($currentStepContainer) => {
let isStepValid = true;
$currentStepContainer.find(".plugin-setting").each(function () {
const $input = $(this);
const value = $input.val().trim();
const isRequired = $input.prop("required");
const pattern = $input.attr("pattern");
const fieldName =
$input.data("field-name") || $input.attr("name") || "This field";
let errorMessage = "";
let isValid = true;
// Custom error messages
const requiredMessage =
$input.data("required-message") || `${fieldName} is required.`;
const patternMessage =
$input.data("pattern-message") || `Please enter a valid ${fieldName}.`;
// Check if the field is required and not empty
if (isRequired && value === "") {
errorMessage = requiredMessage;
isValid = false;
}
// Validate based on pattern if the input is not empty
if (isValid && pattern && value !== "") {
const regex = new RegExp(pattern);
if (!regex.test(value)) {
errorMessage = patternMessage;
isValid = false;
}
}
// Toggle valid/invalid classes
$input.toggleClass("is-invalid", !isValid);
// Manage the invalid-feedback element
let $feedback = $input.siblings(".invalid-feedback");
if (!$feedback.length) {
const $textSpan = $input.parent().find("span.input-group-text");
$feedback = $('<div class="invalid-feedback"></div>').insertAfter(
$textSpan.length ? $textSpan : $input,
);
}
if (!isValid) {
$feedback.text(errorMessage);
isStepValid = false;
} else {
$feedback.text("");
}
});
if (!isStepValid) {
// Focus the first invalid input
$currentStepContainer.find(".is-invalid").first().focus();
}
return isStepValid;
};
/**
* Navigates to the specified step.
* @param {number} newStep - The step number to navigate to.
*/
const navigateToStep = (newStep) => {
const $newTabTrigger = $(`button[data-bs-target="#navs-steps-${newStep}"]`);
// Update breadcrumb UI
$breadcrumbItems.each(function () {
$(this)
.find("div.text-primary")
.removeClass("text-primary")
.addClass("text-muted");
$(this).find("button").addClass("disabled");
});
// Activate the new tab
const newTab = new bootstrap.Tab($newTabTrigger[0]);
newTab.show();
// Update breadcrumb item
$newTabTrigger
.parent()
.find("div.text-muted")
.removeClass("text-muted")
.addClass("text-primary");
$newTabTrigger.removeClass("disabled");
// Scroll into view
$newTabTrigger[0].scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center",
});
currentStep = newStep;
// Toggle button states based on the new step
toggleButtonStates();
};
/**
* Toggles the visibility and state of navigation buttons based on the current step.
*/
const toggleButtonStates = () => {
if (currentStep === 1) {
$previousStepButton.addClass("disabled");
} else {
$previousStepButton.removeClass("disabled");
}
if (currentStep === 3) {
$nextStepButton.addClass("d-none");
$saveSettingsButton.removeClass("d-none");
populateOverview();
} else {
if (
!uiUser &&
currentStep === 2 &&
$("#EMAIL_LETS_ENCRYPT").val().trim() === ""
) {
$("#EMAIL_LETS_ENCRYPT").val($("#email").val().trim());
}
$nextStepButton.removeClass("d-none");
$saveSettingsButton.addClass("d-none");
}
};
/**
* Populates the overview fields with the entered data.
*/
const populateOverview = () => {
if (!uiUser) {
$("#overview_username").val($("#username").val());
$("#overview_password").val($("#password").val());
}
if (!uiReverseProxy) {
$("#overview_service_url").val(
`https://${getServerName()}${$("#REVERSE_PROXY_URL").val()}`,
);
$("#overview_email_lets_encrypt").val($("#EMAIL_LETS_ENCRYPT").val());
}
};
/**
* Handles navigation to the next or previous step.
* @param {boolean} isNext - True if navigating forward, false if backward.
*/
const handleStepNavigation = (isNext) => {
const newStep = isNext ? currentStep + 1 : currentStep - 1;
const $currentStepContainer = $(`#navs-steps-${currentStep}`);
if (isNext) {
let isStepValid = validateCurrentStepInputs($currentStepContainer);
// Additional validation for step 1 (password confirmation)
if (!uiUser && currentStep === 1) {
const password = $passwordInput.val();
const confirmPassword = $confirmPasswordInput.val();
if (password !== confirmPassword) {
$confirmPasswordInput.addClass("is-invalid");
let $feedback = $confirmPasswordInput.siblings(".invalid-feedback");
if (!$feedback.length) {
const $textSpan = $confirmPasswordInput
.parent()
.find("span.input-group-text");
$feedback = $(
'<div class="invalid-feedback">Passwords do not match.</div>',
).insertAfter($textSpan.length ? $textSpan : $confirmPasswordInput);
} else {
$feedback.text("Passwords do not match.");
}
isStepValid = false;
} else {
$confirmPasswordInput.removeClass("is-invalid");
$confirmPasswordInput.siblings(".invalid-feedback").text("");
}
}
if (!isStepValid) return;
currentStep = newStep;
navigateToStep(newStep);
} else {
currentStep = newStep;
navigateToStep(newStep);
}
};
// Event Handlers
// Real-time password validation
$passwordInput.on(
"input",
debounce(function () {
const isValid = validatePassword();
updateValidationState(this, isValid);
}, 100),
);
// Real-time validation for other plugin settings
$(document).on(
"input",
".plugin-setting",
debounce(function () {
const $this = $(this);
const pattern = $this.attr("pattern");
const value = $this.val();
const isValid = pattern ? new RegExp(pattern).test(value) : true;
$this
.toggleClass("is-valid", isValid)
.toggleClass("is-invalid", !isValid);
}, 100),
);
// Remove validation state on focus out
$(document).on("focusout", ".plugin-setting", function () {
$(this).removeClass("is-valid");
$(".invalid-feedback").remove();
});
// Save Settings Button Click
$saveSettingsButton.on("click", function (e) {
e.preventDefault();
if (currentStep !== 3) return;
$("#loadingModal").modal("show");
// Create a new FormData object
const formData = new FormData();
// Append the CSRF token
formData.append("csrf_token", $csrfTokenInput.val() || "");
const server_name = getServerName();
const ui_url = $("#REVERSE_PROXY_URL").val();
if (!uiUser) {
formData.append("admin_username", $("#username").val());
formData.append("admin_email", $("#email").val());
formData.append("admin_password", $("#password").val());
formData.append("admin_password_check", $("#confirm_password").val());
formData.append("2fa_code", $("#2fa_code").val());
}
if (!uiReverseProxy) {
formData.append("server_name", server_name);
formData.append("ui_host", $("#REVERSE_PROXY_HOST").val());
formData.append("ui_url", ui_url);
formData.append(
"auto_lets_encrypt",
$("#AUTO_LETS_ENCRYPT").prop("checked") ? "yes" : "no",
);
formData.append(
"lets_encrypt_staging",
$("#LETS_ENCRYPT_STAGING").prop("checked") ? "yes" : "no",
);
formData.append("email_lets_encrypt", $("#EMAIL_LETS_ENCRYPT").val());
}
// Remove beforeunload event to prevent prompt on form submission
$window.off("beforeunload");
if (!uiReverseProxy) {
var api = `https://${server_name}`;
if (!ui_url.startsWith("/")) {
api = `${api}/`;
}
api = `${api}${ui_url}/check`;
var redirect = `https://${server_name}/setup/loading?target_endpoint=${api}`;
} else {
var redirect = window.location.href.replace("setup", "login");
}
// Submit the form
fetch(window.location.href, {
method: "POST",
body: formData,
redirect: "error",
})
.then((res) => {
if (res.status === 200) {
window.location.href = redirect;
}
})
.catch((err) => {
$("#loadingModal").modal("hide");
setTimeout(() => {
const feedbackToast = $("#feedback-toast").clone(); // Clone the feedback toast
feedbackToast.attr("id", `feedback-toast-${toastNum++}`); // Corrected to set the ID for the failed toast
feedbackToast.removeClass("bg-primary");
feedbackToast.addClass("bg-danger");
feedbackToast.find("span").text("Error");
feedbackToast
.find("div.toast-body")
.text("Error while setting up web UI. Please try again.");
feedbackToast.appendTo("#feedback-toast-container"); // Ensure the toast is appended to the container
feedbackToast.toast("show");
}, 400);
setTimeout(() => {
location.reload();
}, 2500);
});
});
// Next and Previous Step Buttons Click
$(document).on("click", ".next-step, .previous-step", function (e) {
e.preventDefault();
const isNext = $(this).hasClass("next-step");
handleStepNavigation(isNext);
});
// DNS Check Button Click
$("#check-dns").on("click", function (e) {
e.preventDefault();
if (uiReverseProxy) return;
if (currentStep !== CHECK_STEP) return;
checkDNS();
});
$2faInput.on("input", function () {
if (uiUser) return;
const $this = $(this);
const value = $this.val();
const isValid = /^[0-9]{6}$/.test(value);
updateValidationState(this, isValid);
$overview2faEnabled
.find("i")
.toggleClass("bx-x text-danger bx-check text-success", false)
.toggleClass("bx-question-mark text-warning", value === "");
if (value) {
if (isValid) {
$overview2faEnabled
.find("i")
.toggleClass("bx-x text-danger", false)
.toggleClass("bx-check text-success", true);
} else {
$overview2faEnabled
.find("i")
.toggleClass("bx-check text-success", false)
.toggleClass("bx-x text-danger", true);
}
}
});
// Before Unload Event to Warn Users About Unsaved Changes
$window.on("beforeunload", function (e) {
const message =
"Are you sure you want to leave? Changes you made may not be saved.";
e.returnValue = message; // Standard for most browsers
return message; // Required for some browsers
});
// Initialize the UI based on the initial step
toggleButtonStates();
});

View file

@ -87,7 +87,7 @@
{% endif %}
<!-- Page CSS -->
<!-- Page -->
{% if current_endpoint == "login" or current_endpoint == "totp" %}
{% if current_endpoint in ("setup", "login", "totp") %}
<link rel="stylesheet"
href="{{ url_for('static', filename='css/pages/login.css') }}"
nonce="{{ style_nonce }}" />
@ -96,13 +96,13 @@
href="{{ url_for('static', filename='css/pages/loading.css') }}"
nonce="{{ style_nonce }}" />
{% elif current_endpoint == "profile" %}
<link rel="stylesheet"
href="{{ url_for('static', filename='css/pages/profile.css') }}"
nonce="{{ style_nonce }}" />
<link rel="stylesheet"
href="{{ url_for('static', filename='css/pages/profile.css') }}"
nonce="{{ style_nonce }}" />
{% elif current_endpoint == "home" %}
<link rel="stylesheet"
href="{{ url_for('static', filename='css/pages/home.css') }}"
nonce="{{ style_nonce }}" />
<link rel="stylesheet"
href="{{ url_for('static', filename='css/pages/home.css') }}"
nonce="{{ style_nonce }}" />
{% endif %}
<!-- Helpers -->
<script src="{{ url_for('static', filename='js/helpers.js') }}"
@ -157,7 +157,7 @@
<!-- Main JS -->
<script src="{{ url_for('static', filename='js/main.js') }}"
nonce="{{ script_nonce }}"></script>
{% if current_endpoint != "login" and current_endpoint != "totp" %}
{% if current_endpoint not in ("setup", "login", "totp") %}
<script async
defer
src="{{ url_for('static', filename='js/utils.js') }}"
@ -170,9 +170,14 @@
nonce="{{ script_nonce }}"></script>
{% endif %}
<!-- Page JS -->
{% if current_endpoint == "home" %}
<script async defer src="{{ url_for('static', filename='js/pages/home.js') }}"
nonce="{{ script_nonce }}"></script>
{% if current_endpoint == "setup" %}
<script src="{{ url_for('static', filename='js/pages/setup.js') }}"
nonce="{{ script_nonce }}"></script>
{% elif current_endpoint == "home" %}
<script async
defer
src="{{ url_for('static', filename='js/pages/home.js') }}"
nonce="{{ script_nonce }}"></script>
{% elif current_endpoint == "profile" %}
<script src="{{ url_for('static', filename='js/pages/profile.js') }}"
nonce="{{ script_nonce }}"></script>

View file

@ -115,14 +115,14 @@
<div class="col-sm-6 mt-2 mb-2">
<div class="card p-4 position-relative shadow-sm rounded-3">
<div class="card-header p-2">
<div class="card-title mb-0 d-flex justify-content-between">
<h5>News :</h5>
<a class="text-decoration-underline link-underline-primary"
href="https://www.bunkerweb.io/blog?utm_campaign=self&amp;utm_source=ui"
target="_blank"
rel="noopener">See more</a>
<div class="card-title mb-0 d-flex justify-content-between">
<h5>News :</h5>
<a class="text-decoration-underline link-underline-primary"
href="https://www.bunkerweb.io/blog?utm_campaign=self&amp;utm_source=ui"
target="_blank"
rel="noopener">See more</a>
</div>
</div>
</div>
<div class="bg-bw-green position-relative w-100 p-2 text-white rounded fw-bold overflow-hidden">
<div class="d-flex justify-content-between align-items-center">
<div class="flex-grow-1 overflow-hidden me-2">
@ -144,15 +144,12 @@
</div>
</div>
<div class="col-sm-3 mb-2">
<div
class="card p-4 position-relative shadow-sm rounded-3 bg-secondary text-white h-100">
<div class="card p-4 position-relative shadow-sm rounded-3 bg-secondary text-white h-100">
<i class='bx bx-broadcast bx-sm position-absolute top-0 end-0 m-3 text-white'></i>
<p class="ps-4 fs-4 mb-2">
Total Requests
<br />
<span class="fs-3 fw-bold">
{{ human_readable_number(request_errors.values() |sum) }}
</span>
<span class="fs-3 fw-bold">{{ human_readable_number(request_errors.values() |sum) }}</span>
</p>
</div>
</div>
@ -163,15 +160,12 @@
{% endif %}
{% endfor %}
<div class="col-sm-3 mb-2">
<div
class="card p-4 position-relative shadow-sm rounded-3 h-100">
<div class="card p-4 position-relative shadow-sm rounded-3 h-100">
<i class='bx bx-globe bx-sm position-absolute top-0 end-0 m-3 text-secondary'></i>
<p class="ps-4 fs-4 mb-2">
Unique Ips
<br />
<span class="fs-3 fw-bold">
{{ human_readable_number(request_ips|length) }}
</span>
<span class="fs-3 fw-bold">{{ human_readable_number(request_ips|length) }}</span>
</p>
</div>
</div>
@ -185,26 +179,23 @@
<br />
<span class="fs-3 fw-bold">
{% set ns = namespace(blocked_requests=0) %}
{% for status, count in request_errors.items() %}
{% if status in (403, 429, 444) %}
{% set ns.blocked_requests = ns.blocked_requests + count %}
{% endif %}
{% endfor %}
{% for status, count in request_errors.items() %}
{% if status in (403, 429, 444) %}
{% set ns.blocked_requests = ns.blocked_requests + count %}
{% endif %}
{% endfor %}
{{ human_readable_number(ns.blocked_requests) }}
</span>
</p>
</a>
</div>
<div class="col-sm-3 mb-2">
<div
class="card p-4 position-relative shadow-sm rounded-3 h-100">
<div class="card p-4 position-relative shadow-sm rounded-3 h-100">
<i class='bx bx-globe bx-sm position-absolute top-0 end-0 m-3 text-danger'></i>
<p class="ps-4 fs-4 mb-2">
Blocked Unique Ips
<br />
<span class="fs-3 fw-bold">
{{ human_readable_number(ips_ns.blocked_ips) }}
</span>
<span class="fs-3 fw-bold">{{ human_readable_number(ips_ns.blocked_ips) }}</span>
</p>
</div>
</div>

View file

@ -11,10 +11,13 @@
alt="Logo" />
</div>
{% if message %}
<div class="layout-main-info">
<div class="layout-main-info mb-1">
<h3>{{ message }}</h3>
</div>
{% endif %}
<div id="auto-redirect" class="layout-main-info">
<h3>Auto Redirecting in 60 seconds...</h3>
</div>
</div>
</div>
</div>
@ -23,24 +26,57 @@
<!-- / Content -->
<script nonce="{{ script_nonce }}">
const reloading = setInterval(check_reloading, 2000);
{% if target_endpoint %}
var target_endpoint = "{{ target_endpoint }}";
{% else %}
var target_endpoint = null;
{% endif %}
check_reloading();
// Set the full timeout for redirection after 60 seconds
const fullTimeout = setTimeout(() => {
window.location.replace(
target_endpoint ? target_endpoint : ("{{ next }}" + (window.location.hash ? window.location.hash : ""))
);
}, 60000); // 60 seconds
// Initialize timeRemaining
let timeRemaining = 60;
// Start the countdown interval
const countdownInterval = setInterval(() => {
timeRemaining--;
// Update the countdown display
const countdownElement = document.getElementById("auto-redirect");
if (countdownElement) {
countdownElement.innerHTML = `<h3>Auto Redirecting in ${timeRemaining} seconds...</h3>`;
}
// If time runs out, clear the interval
if (timeRemaining <= 0) {
clearInterval(countdownInterval);
}
}, 1000);
async function check_reloading() {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 2000);
const response = await fetch(
`${location.href.replace("/loading", "/check_reloading")}`,
target_endpoint ? target_endpoint : `${location.href.replace("/loading", "/check_reloading")}`,
{ signal: controller.signal },
);
if (response.status === 200) {
res = await response.json();
if (res.reloading === false) {
clearInterval(reloading);
window.location.replace(
"{{ next }}" + (window.location.hash ? window.location.hash : ""),
);
}
try {
const res = await response.json();
if (res.message === "ok" || res.reloading === false) {
clearInterval(reloading);
clearInterval(countdownInterval);
clearTimeout(fullTimeout);
window.location.replace("{{ next }}" + (window.location.hash ? window.location.hash : ""));
}
} catch (error) {}
}
}
</script>

View file

@ -9,9 +9,10 @@
data-original="{% if current_endpoint != 'new' %}{{ setting_value }}{% else %}{{ setting_default }}{% endif %}"
data-default="{{ setting_default }}"
{% if disabled %}disabled{% endif %}
{% if required %}required{% endif %}>
{% if required %}required{% endif %}
{% if input_readonly %}readonly{% endif %}>
<label for="{{ setting_id_prefix }}{{ setting_data['id'] }}{{ setting_id_suffix }}">{{ setting }}</label>
{% if setting_data['type'] == 'password' %}
<span class="input-group-text cursor-pointer"><i class="bx bx-hide"></i></span>
<span class="input-group-text cursor-pointer rounded-end"><i class="bx bx-hide"></i></span>
{% endif %}
</div>

View file

@ -96,7 +96,7 @@
</div>
</div>
</div>
<div class="card tab-content m-1 p-2 position-relative">
<div class="card tab-content p-2 position-relative">
{% for plugin, plugin_data in plugins.items() %}
{% set filtered_settings = get_filtered_settings(plugin_data["settings"], current_endpoint == "global-config") %}
{% if filtered_settings %}

View file

@ -87,7 +87,7 @@
</div>
<div class="card-body">
{% if template_data["steps"] %}
<nav class="p-3 template-steps-container align-items-center mw-100 border rounded-top{{ plugin_types[template_plugin['type']].get('title-class', '') }}"
<nav class="p-3 template-steps-container align-items-center mw-100{{ plugin_types[template_plugin['type']].get('title-class', '') }}"
aria-label="breadcrumb">
<ol class="breadcrumb nav nav-scroller mb-0 flex-nowrap overflow-hidden{% if loop.index == 1 %} active{% endif %}"
role="tablist">
@ -109,7 +109,7 @@
{% endfor %}
</ol>
</nav>
<div class="tab-content p-3 align-items-center mw-100 border border-top-0 rounded-bottom{{ plugin_types[template_plugin['type']].get('title-class', '') }}">
<div class="tab-content p-3 align-items-center mw-100{{ plugin_types[template_plugin['type']].get('title-class', '') }}">
{% for step in template_data["steps"] %}
<div id="navs-steps-{{ template }}-{{ loop.index }}"
class="ps-2 pe-2 tab-pane fade{% if loop.index == 1 %} show active{% endif %}"

View file

@ -4,7 +4,7 @@
{% set total_pages = (total_sessions // 3) + (1 if total_sessions % 3 > 0 else 0) %}
<!-- Content -->
<div class="d-flex row">
<div class="col-md-4">
<div>
<ul class="nav nav-pills flex-column flex-md-row mb-6">
<li class="nav-item me-0 me-sm-2" role="presentation">
<button type="button"
@ -122,7 +122,6 @@
<label for="username" class="form-label">Username</label>
<input class="form-control"
type="text"
id="username"
name="username"
value="{{ current_user.get_id() }}"
placeholder="john.doe@example.com"
@ -135,7 +134,6 @@
<label for="email" class="form-label">E-mail</label>
<input class="form-control"
type="email"
id="email"
name="email"
value="{{ current_user.email or '' }}"
placeholder="john.doe@example.com"
@ -147,7 +145,6 @@
<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;"
@ -220,9 +217,16 @@
<label for="new_password" class="form-label">New Password</label>
<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 %}
/>
<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)(?=.*[ -~]).{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">
@ -261,7 +265,6 @@
<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;"
@ -309,7 +312,6 @@
<label for="secret_token" class="form-label text-start d-block">Secret Token</label>
<div class="input-group input-group-merge">
<input type="password"
id="secret_token"
name="secret_token"
class="form-control"
placeholder="Secret Token"
@ -317,7 +319,7 @@
readonly />
{% if request.is_secure %}
<span class="input-group-text cursor-pointer copy-to-clipboard">
<i class="bx bx-copy-alt copy-to-clipboard"></i>
<i class="bx bx-copy-alt"></i>
</span>
{% endif %}
<span class="input-group-text cursor-pointer">
@ -331,7 +333,6 @@
<div class="input-group">
<input class="form-control"
type="text"
id="totp_token"
name="totp_token"
placeholder="Enter code"
aria-label="2FA Code"
@ -344,7 +345,6 @@
<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;"
@ -379,7 +379,6 @@
{% 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"
@ -392,7 +391,6 @@
<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;"
@ -436,7 +434,6 @@
<label class="form-label" for="password">Current password</label>
<div class="input-group input-group-merge">
<input type="password"
id="password"
class="form-control"
name="password"
placeholder="&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;&#xb7;"
@ -477,7 +474,6 @@
<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;"
@ -554,8 +550,7 @@
</strong>
&nbsp;
<div class="input-group input-group-sm input-group-merge w-auto">
<input id="ip"
class="form-control"
<input class="form-control"
type="password"
autocomplete="off"
value="{{ session['ip'] }}"

View file

@ -47,7 +47,21 @@
<tr>
<td class="report-date">{{ report["date"] }}</td>
<td>{{ report["ip"] }}</td>
<td><div class="d-flex align-items-center"><img src="{{ base_url }}/{% if report['country'] == "local" %}zz{% else %}{{ report['country']|lower }}{% endif %}.svg" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-original-title="{% if report['country'] == "local" %}N/A{% else %}{{ report["country"] }}{% endif %}" height="17" />&nbsp;&nbsp;{% if report['country'] == "local" %}N/A{% else %}{{ report["country"] }}{% endif %}</div></td>
<td>
<div class="d-flex align-items-center">
<img src="{{ base_url }}/{% if report['country'] == "local" %}zz{% else %}{{ report['country']|lower }}{% endif %}.svg"
data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-original-title="{% if report['country'] == "local" %}N/A{% else %}{{ report["country"] }}{% endif %}"
height="17" />
&nbsp;&nbsp;
{% if report['country'] == "local" %}
N/A
{% else %}
{{ report["country"] }}
{% endif %}
</div>
</td>
<td>{{ report["method"] }}</td>
<td>{{ report["url"] }}</td>
<td>{{ report["status"] }}</td>

View file

@ -0,0 +1,630 @@
{% extends "base.html" %}
{% block page %}
<!-- Content -->
<input type="hidden" id="ui_user" value="{{ 'yes' if ui_user else 'no' }}" />
<input type="hidden"
id="ui_reverse_proxy"
value="{{ 'yes' if ui_reverse_proxy else 'no' }}" />
<input type="hidden"
id="csrf_token"
name="csrf_token"
value="{{ csrf_token() }}" />
<div class="container-xxl">
<div class="authentication-wrapper authentication-basic container-p-y">
<div class="authentication-inner w-100">
<!-- Setup -->
<div class="card px-sm-6 px-0">
<div class="card-header d-flex justify-content-between align-items-center mw-100">
<div class="pt-1">
<h4 class="card-title d-inline border fw-bold p-2">Setup Wizard</h4>
<p class="card-subtitle text-muted text-truncate mt-2">Follow the steps to complete the setup wizard</p>
</div>
<!-- Logo -->
<div class="app-brand justify-content-center">
<a href="https://www.bunkerweb.io/?utm_campaign=self&utm_source=ui"
target="_blank"
rel="noopener"
class="app-brand-link gap-2">
<span class="app-brand-logo login">
<img class="img-fluid"
src="{{ url_for('static', filename='img/icon.svg') }}"
alt="BunkerWeb logo"
width="50" />
</span>
</a>
</div>
<!-- /Logo -->
</div>
<div class="card-body p-2">
<div class="d-flex justify-content-center">
<nav class="p-3 template-steps-container align-items-center mw-100 rounded-top"
aria-label="breadcrumb">
<ol class="breadcrumb nav nav-scroller mb-0 flex-nowrap overflow-hidden active"
role="tablist">
<li class="breadcrumb-item nav-link d-flex align-items-center pe-0">
<button class="btn btn-primary pt-3 pb-3 me-3 active"
role="tab"
data-bs-toggle="tab"
data-bs-target="#navs-steps-1"
aria-controls="navs-steps-1"
aria-selected="true">1</button>
<div class="text-nowrap">
<div class="fw-bold text-primary">Admin User</div>
<small class="text-muted">Create Admin User</small>
</div>
</li>
<li class="breadcrumb-item nav-link d-flex align-items-center pe-0">
<button class="btn btn-primary pt-3 pb-3 me-3 disabled"
role="tab"
data-bs-toggle="tab"
data-bs-target="#navs-steps-2"
aria-controls="navs-steps-2">2</button>
<div class="text-nowrap">
<div class="fw-bold text-muted">UI specific Settings</div>
<small class="text-muted">Configure web UI specific settings</small>
</div>
</li>
<li class="breadcrumb-item nav-link d-flex align-items-center pe-0">
<button class="btn btn-primary pt-3 pb-3 me-3 disabled"
role="tab"
data-bs-toggle="tab"
data-bs-target="#navs-steps-3"
aria-controls="navs-steps-3">3</button>
<div class="text-nowrap">
<div class="fw-bold text-muted">Overview</div>
<small class="text-muted">Review your settings</small>
</div>
</li>
</ol>
</nav>
</div>
<div class="tab-content p-3 align-items-center mw-100">
<div id="navs-steps-1"
class="ps-2 pe-2 tab-pane fade show active"
role="tabpanel"
data-step="1"
aria-labelledby="navs-steps-1-tab">
<div class="pt-1 pb-4">
<h5 class="mb-1 fw-bold">Admin User</h5>
<p class="card-subtitle text-muted">Create Admin User</p>
</div>
{% if not ui_user %}
<div class="row pb-0">
<div class="col-12 col-sm-6 pb-3">
<div class="d-flex justify-content-between align-items-center">
<label id="label-username"
for="username"
class="form-label fw-semibold text-truncate">
Admin Username
</label>
<div class="d-flex align-items-center">
<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"
data-bs-original-title="Username must be between 1 and 256 characters long">
<span class="bx bx-question-mark bx-xs"></span>
</span>
</div>
</div>
{% set setting = "username" %}
{% set setting_value = username %}
{% set setting_data = {"type": "text", "id": "username", "regex": "^.{1,256}$"} %}
{% set required = true %}
{% include "models/input_setting.html" %}
</div>
<div class="col-12 col-sm-6 pb-3">
<div class="d-flex justify-content-between align-items-center">
<label id="label-username"
for="username"
class="form-label fw-semibold text-truncate">Admin Email</label>
<div class="d-flex align-items-center">
<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"
data-bs-original-title="Email must be between 1 and 256 characters long">
<span class="bx bx-question-mark bx-xs"></span>
</span>
</div>
</div>
{% set setting = "email" %}
{% set setting_data = {"type": "email", "id": "email", "regex": "^.{1,256}$"} %}
{% set required = false %}
{% include "models/input_setting.html" %}
</div>
<div class="col-12 col-sm-6 pb-3">
<div class="d-flex justify-content-between align-items-center">
<label id="label-username"
for="username"
class="form-label fw-semibold text-truncate">
Admin Password
</label>
<div class="d-flex align-items-center">
<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"
data-bs-original-title="Password must have at least 8 characters, 1 uppercase letter, 1 number, and 1 special character">
<span class="bx bx-question-mark bx-xs"></span>
</span>
</div>
</div>
{% set setting = "password" %}
{% set setting_value = password %}
{% set setting_data = {"type": "password", "id": "password", "regex": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[ -~]).{8,}$"} %}
{% set required = true %}
{% include "models/input_setting.html" %}
<div class="mt-3">
<ul class="list-unstyled" id="password-requirements">
<li id="length-check">
<i class="bx bx-x text-danger"></i> Your password must
have at least 8 characters.
</li>
<li id="uppercase-check">
<i class="bx bx-x text-danger"></i> Your password must
have at least 1 uppercase letter.
</li>
<li id="number-check">
<i class="bx bx-x text-danger"></i> Your password must
have at least 1 number.
</li>
<li id="special-check">
<i class="bx bx-x text-danger"></i> Your password must
have at least 1 special character.
</li>
</ul>
</div>
</div>
<div class="col-12 col-sm-6 pb-3">
<div class="d-flex justify-content-between align-items-center">
<label id="label-username"
for="username"
class="form-label fw-semibold text-truncate">
Confirm Password
</label>
<div class="d-flex align-items-center">
<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"
data-bs-original-title="Confirm password must match the password">
<span class="bx bx-question-mark bx-xs"></span>
</span>
</div>
</div>
{% set setting = "confirm_password" %}
{% set setting_data = {"type": "password", "id": "confirm_password", "regex": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[ -~]).{8,}$"} %}
{% set required = true %}
{% include "models/input_setting.html" %}
</div>
<div class="pt-1 pb-4">
<h5 class="mb-1 fw-bold">Enable 2FA</h5>
<p class="card-subtitle text-muted">Enable Two-Factor Authentication (Strongly Recommended)</p>
</div>
<div class="col-12 pb-3 d-flex justify-content-center">
<div class="g-3 w-75">
<div class="d-flex justify-content-center">
<img class="img-fluid"
src="{{ totp_qr_image }}"
alt="2FA QR Code"
height="200"
width="200" />
</div>
<div class="form-password-toggle">
<label for="secret_token" class="form-label text-start d-block">Secret Token</label>
<div class="input-group input-group-merge">
<input type="password"
id="secret_token"
name="secret_token"
class="form-control"
placeholder="Secret Token"
value="{{ totp_secret }}"
readonly />
{% if request.is_secure %}
<span class="input-group-text cursor-pointer copy-to-clipboard">
<i class="bx bx-copy-alt"></i>
</span>
{% endif %}
<span class="input-group-text cursor-pointer">
<i class="bx bx-hide"></i>
</span>
</div>
<small class="form-text text-muted">Save it in a safe place, you will need the generated code to setup your 2FA in the last step.</small>
</div>
</div>
</div>
</div>
{% else %}
<p class="text-center relative w-full p-2 text-primary rounded-lg fw-bold">
🧑‍🚀 An admin user already exists, please proceed to the next step
</p>
{% endif %}
</div>
<div id="navs-steps-2"
class="ps-2 pe-2 tab-pane fade"
role="tabpanel"
data-step="2"
aria-labelledby="navs-steps-2-tab">
<div class="pt-1 pb-4">
<h5 class="mb-1 fw-bold">UI specific Settings</h5>
<p class="card-subtitle text-muted">Configure web UI specific settings</p>
</div>
{% if not ui_reverse_proxy %}
<div class="row pb-0">
<div class="col-12 pb-6">
<div class="d-flex justify-content-between align-items-center">
<label id="label-username"
for="username"
class="form-label fw-semibold text-truncate">Server name</label>
<div class="d-flex align-items-center">
<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"
data-bs-original-title="The server name of the service">
<span class="bx bx-question-mark bx-xs"></span>
</span>
</div>
</div>
{% set setting = "SERVER_NAME" %}
{% set setting_value = "www.example.com" %}
{% set setting_data = {"type": "text", "id": "SERVER_NAME", "regex": "^.+$"} %}
{% set required = true %}
{% include "models/input_setting.html" %}
</div>
<div class="col-12 col-sm-6 pb-6">
<div class="d-flex justify-content-between align-items-center">
<label id="label-username"
for="username"
class="form-label fw-semibold text-truncate">UI Host</label>
<div class="d-flex align-items-center">
{% if ui_host %}
<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"
data-bs-original-title="From UI_HOST environment variable">
<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"
data-bs-original-title="The host that will be used for the reverse proxy">
<span class="bx bx-question-mark bx-xs"></span>
</span>
</div>
</div>
{% set setting = "REVERSE_PROXY_HOST" %}
{% set setting_value = ui_host %}
{% set setting_data = {"type": "text", "id": "REVERSE_PROXY_HOST", "regex": "^.+$"} %}
{% set required = true %}
{% include "models/input_setting.html" %}
</div>
<div class="col-12 col-sm-6 pb-6">
<div class="d-flex justify-content-between align-items-center">
<label id="label-username"
for="username"
class="form-label fw-semibold text-truncate">UI URL</label>
<div class="d-flex align-items-center">
<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"
data-bs-original-title="The URL that will be used for the reverse proxy">
<span class="bx bx-question-mark bx-xs"></span>
</span>
</div>
</div>
{% set setting = "REVERSE_PROXY_URL" %}
{% set setting_value = random_url %}
{% set setting_data = {"type": "text", "id": "REVERSE_PROXY_URL", "regex": "^.+$"} %}
{% set required = false %}
{% include "models/input_setting.html" %}
</div>
<div class="col-6 col-sm-3 pb-3">
<div class="d-flex justify-content-between align-items-center">
<label id="label-username"
for="username"
class="form-label fw-semibold text-truncate">
Auto Let's Encrypt
</label>
{% if auto_lets_encrypt == "yes" %}
<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"
data-bs-original-title="From global configuration">
<span class="bx bx-globe bx-xs"></span>
</span>
{% endif %}
<div class="d-flex align-items-center">
<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"
data-bs-original-title="Whether to automatically request a Let's Encrypt certificate">
<span class="bx bx-question-mark bx-xs"></span>
</span>
</div>
</div>
{% set setting = "AUTO_LETS_ENCRYPT" %}
{% set setting_value = auto_lets_encrypt %}
{% set setting_data = {"id": "AUTO_LETS_ENCRYPT"} %}
{% include "models/checkbox_setting.html" %}
</div>
<div class="col-6 col-sm-3 pb-3">
<div class="d-flex justify-content-between align-items-center">
<label id="label-username"
for="username"
class="form-label fw-semibold text-truncate">
Use Let's Encrypt Staging
</label>
<div class="d-flex align-items-center">
{% if lets_encrypt_staging == "yes" %}
<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"
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"
data-bs-original-title="Whether to use the Let's Encrypt staging environment">
<span class="bx bx-question-mark bx-xs"></span>
</span>
</div>
</div>
{% set setting = "USE_LETS_ENCRYPT_STAGING" %}
{% set setting_value = lets_encrypt_staging %}
{% set setting_data = {"id": "USE_LETS_ENCRYPT_STAGING"} %}
{% include "models/checkbox_setting.html" %}
</div>
<div class="col-sm-6 pb-3">
<div class="d-flex justify-content-between align-items-center">
<label id="label-username"
for="username"
class="form-label fw-semibold text-truncate">
Email for Let's Encrypt
</label>
<div class="d-flex align-items-center">
{% if email_lets_encrypt %}
<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"
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"
data-bs-original-title="The email address that will be used for Let's Encrypt">
<span class="bx bx-question-mark bx-xs"></span>
</span>
</div>
</div>
{% set setting = "EMAIL_LETS_ENCRYPT" %}
{% set setting_value = email_lets_encrypt %}
{% set setting_data = {"type": "text", "id": "EMAIL_LETS_ENCRYPT", "regex": "^.*$"} %}
{% include "models/input_setting.html" %}
</div>
</div>
{% else %}
<p class="text-center relative w-full p-2 text-primary rounded-lg fw-bold">
↪️ A reverse proxy is already configured for the web interface, please proceed to the next step
</p>
{% endif %}
</div>
<div id="navs-steps-3"
class="ps-2 pe-2 tab-pane fade"
role="tabpanel"
data-step="3"
aria-labelledby="navs-steps-3-tab">
<div class="pt-1 pb-4">
<h5 class="mb-1 fw-bold">Overview</h5>
<p class="card-subtitle text-muted">Review your settings</p>
</div>
<div class="row pb-0 mb-4">
{% if not ui_user %}
<h6 class="mt-2 mb-1 fw-bold">Admin User</h6>
{% set input_readonly = true %}
<div class="col-12 col-sm-6 pb-3">
<div class="d-flex justify-content-between align-items-center">
<label id="label-overview_username"
for="overview_username"
class="form-label fw-semibold text-truncate">
Admin Username
</label>
</div>
{% set setting = "username" %}
{% set setting_data = {"type": "text", "id": "overview_username", "regex": "^.{1,256}$"} %}
{% include "models/input_setting.html" %}
</div>
<div class="col-12 col-sm-6 pb-3">
<div class="d-flex justify-content-between align-items-center">
<label id="label-overview_password"
for="overview_password"
class="form-label fw-semibold text-truncate">
Admin Password
</label>
</div>
{% set setting = "password" %}
{% set setting_data = {"type": "password", "id": "overview_password", "regex": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[ -~]).{8,}$"} %}
{% include "models/input_setting.html" %}
</div>
{% endif %}
{% if not ui_reverse_proxy %}
<h6 class="mt-2 mb-1 fw-bold">UI specific Settings</h6>
<div class="col-12 col-sm-6 pb-3">
<div class="d-flex justify-content-between align-items-center">
<label id="label-overview_service_url"
for="overview_service_url"
class="form-label fw-semibold text-truncate">
BunkerWeb UI final URL
</label>
</div>
{% set setting = "SERVER_NAME + REVERSE_PROXY_HOST" %}
{% set setting_data = {"type": "text", "id": "overview_service_url", "regex": "^.+$"} %}
{% include "models/input_setting.html" %}
</div>
<div class="col-sm-6 pb-3">
<div class="d-flex justify-content-between align-items-center">
<label id="label-overview_email_lets_encrypt"
for="overview_email_lets_encrypt"
class="form-label fw-semibold text-truncate">
Email for Let's Encrypt
</label>
<div class="d-flex align-items-center">
<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"
data-bs-original-title="The email address that will be used for Let's Encrypt">
<span class="bx bx-question-mark bx-xs"></span>
</span>
</div>
</div>
{% set setting = "EMAIL_LETS_ENCRYPT" %}
{% set setting_data = {"type": "text", "id": "overview_email_lets_encrypt", "regex": "^.*$"} %}
{% include "models/input_setting.html" %}
</div>
<div class="text-center pt-2 mb-6">
<h6 class="mb-1 fw-bold">Check server name DNS</h6>
<button id="check-dns" class="btn btn-primary">Check DNS</button>
<p class="mt-1 mb-0">
In case of issues, you can also click <a id="check_url"
class="fw-semibold"
href="https://www.example.com/setup/check"
target="_blank"
data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-original-title='If the shown text is "ok", that means that the server name is available'>here</a> to perform a manual check.
</p>
</div>
{% endif %}
<div class="row p-0">
<div class="col-md-{% if not ui_user %}4{% else %}6{% endif %}">
<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"
target="_blank">
<div class="mb-3">
<input type="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"
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>
<div class="col-md-{% if not ui_user %}4{% else %}6{% endif %} d-flex align-items-center justify-content-center">
<div class="p-5 fs-5 fw-semibold border border-primary">
<ul class="list-unstyled mb-0" id="password-requirements">
{% if not ui_user %}
<li>
<i class="bx bx-check text-success"></i> Secure password
</li>
<li id="overview-2fa-enabled" class="mt-1">
<i class="bx bx-question-mark text-warning"></i> 2FA enabled
</li>
{% endif %}
{% if not ui_reverse_proxy %}
<li id="overview-unique-server-name" class="mt-1">
<i class="bx bx-question-mark text-warning"></i> Unique Server Name
</li>
{% endif %}
</ul>
</div>
</div>
{% if not ui_user %}
<div class="col-md-4">
<h5 class="mb-3 text-dark">2FA Setup</h5>
{% set input_readonly = false %}
<div class="d-flex justify-content-between align-items-center">
<label id="label-2fa_code"
for="2fa_code"
class="form-label fw-semibold text-truncate">
2FA Code
</label>
<div class="d-flex align-items-center">
<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"
data-bs-original-title="The 6-digit code generated by your 2FA app">
<span class="bx bx-question-mark bx-xs"></span>
</span>
</div>
</div>
{% set setting = "2FA Code" %}
{% set setting_data = {"type": "number", "id": "2fa_code", "regex": "^[0-9]{6}$"} %}
{% include "models/input_setting.html" %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-between mt-2">
<button class="btn btn-primary btn-prev previous-step disabled">
<i class="bx bx-chevron-left bx-sm ms-sm-n2"></i>
<span class="align-middle d-sm-inline-block d-none">Previous</span>
</button>
<button type="button" class="btn btn-outline-bw-green save-settings d-none">
<i class="bx bx-save bx-sm ms-sm-n2"></i>
<span class="align-middle d-sm-inline-block d-none ms-sm-1">Setup</span>
</button>
<button class="btn btn-primary btn-next next-step">
<span class="align-middle d-sm-inline-block d-none me-sm-1">Next</span>
<i class="bx bx-chevron-right bx-sm me-sm-n2"></i>
</button>
</div>
</div>
<!-- /Setup -->
</div>
</div>
</div>
</div>
<div id="feedback-toast"
class="bs-toast toast fade bg-primary text-white"
role="alert"
aria-live="assertive"
aria-atomic="true"
data-bs-autohide="true"
data-bs-delay="8000">
<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">BunkerWeb Forever</span>
<small class="text-body-secondary">just now</small>
<button type="button"
class="btn-close"
data-bs-dismiss="toast"
aria-label="Close"></button>
</div>
<div class="toast-body">If you read this, it means that you're curious 👀</div>
</div>
<div class="modal fade"
id="loadingModal"
data-bs-backdrop="static"
tabindex="-1"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content bg-transparent border-0 shadow-none">
<div class="modal-body text-center">
<div class="spinner-border text-bw-green" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3 text-white">Setting up your BunkerWeb, please wait...</p>
</div>
</div>
</div>
</div>
<!-- / Content -->
{% endblock %}

View file

@ -56,11 +56,9 @@
<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">
method="POST">
<div class="mb-3">
<input type="email"
id="newsletter-email"
name="EMAIL"
class="form-control"
placeholder="John.doe@example.com"
@ -69,7 +67,6 @@
<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">

View file

@ -77,11 +77,9 @@
<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">
method="POST">
<div class="mb-3">
<input type="email"
id="newsletter-email"
name="EMAIL"
class="form-control"
placeholder="John.doe@example.com"
@ -90,7 +88,6 @@
<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">

View file

@ -20,7 +20,7 @@ LIB_DIR = Path(sep, "var", "lib", "bunkerweb")
LOGGER = setup_logger("UI", getenv("CUSTOM_LOG_LEVEL", getenv("LOG_LEVEL", "INFO")))
USER_PASSWORD_RX = re_compile(r"^(?=.*?\p{Lowercase_Letter})(?=.*?\p{Uppercase_Letter})(?=.*?\d)(?=.*?[ !\"#$%&'()*+,./:;<=>?@[\\\]^_`{|}~-]).{8,}$")
USER_PASSWORD_RX = re_compile(r"^(?=.*?\p{Lowercase_Letter})(?=.*?\p{Uppercase_Letter})(?=.*?\d)(?=.*?[ -~]).{8,}$")
PLUGIN_NAME_RX = re_compile(r"^[\w.-]{4,64}$")

View file

@ -205,7 +205,11 @@ def on_starting(server):
LOGGER.error(f"Couldn't create the admin user in the database: {ret}")
exit(1)
latest_release = get_latest_stable_release()
latest_release = None
try:
latest_release = get_latest_stable_release()
except BaseException as e:
LOGGER.error(f"Exception while fetching latest release information: {e}")
if not latest_release:
LOGGER.error("Failed to fetch latest release information")