mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
Add setup wizard page and optimize a few pages
This commit is contained in:
parent
970874e983
commit
d7863f8df2
23 changed files with 1586 additions and 150 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
30
src/ui/app/static/img/icon-white.svg
Normal file
30
src/ui/app/static/img/icon-white.svg
Normal 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 |
34
src/ui/app/static/img/icon.svg
Normal file
34
src/ui/app/static/img/icon.svg
Normal 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 |
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
554
src/ui/app/static/js/pages/setup.js
Normal file
554
src/ui/app/static/js/pages/setup.js
Normal 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();
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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&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&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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}"
|
||||
|
|
|
|||
|
|
@ -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="············"
|
||||
|
|
@ -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="············" aria-label="New Password" autocomplete="off" required pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[ !"#$%&'()*+,./:;<=>?@[\\\]^_`{|}~-]).{8,}$"
|
||||
{% if is_readonly %}disabled{% endif %}
|
||||
/>
|
||||
<input class="form-control"
|
||||
type="password"
|
||||
id="new_password"
|
||||
name="new_password"
|
||||
placeholder="············"
|
||||
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="············"
|
||||
|
|
@ -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="············"
|
||||
|
|
@ -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="············"
|
||||
|
|
@ -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="············"
|
||||
|
|
@ -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="············"
|
||||
|
|
@ -554,8 +550,7 @@
|
|||
</strong>
|
||||
|
||||
<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'] }}"
|
||||
|
|
|
|||
|
|
@ -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" /> - {% 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" />
|
||||
-
|
||||
{% if report['country'] == "local" %}
|
||||
N/A
|
||||
{% else %}
|
||||
{{ report["country"] }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ report["method"] }}</td>
|
||||
<td>{{ report["url"] }}</td>
|
||||
<td>{{ report["status"] }}</td>
|
||||
|
|
|
|||
630
src/ui/app/templates/setup.html
Normal file
630
src/ui/app/templates/setup.html
Normal 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&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 %}
|
||||
|
|
@ -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&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">
|
||||
|
|
|
|||
|
|
@ -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&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">
|
||||
|
|
|
|||
|
|
@ -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}$")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Reference in a new issue