Merge branch 'ui' into dev
This commit is contained in:
commit
68a8f8eb0f
|
@ -28,6 +28,7 @@ from model import (
|
|||
Jobs_cache,
|
||||
Custom_configs,
|
||||
Selects,
|
||||
Users,
|
||||
Metadata,
|
||||
)
|
||||
|
||||
|
@ -1504,3 +1505,26 @@ class Database:
|
|||
return None
|
||||
|
||||
return page.template_file
|
||||
|
||||
def get_ui_user(self) -> Optional[dict]:
|
||||
"""Get ui user."""
|
||||
with self.__db_session() as session:
|
||||
user = session.query(Users).with_entities(Users.username, Users.password).filter_by(id=1).first()
|
||||
if not user:
|
||||
return None
|
||||
return {"username": user.username, "password_hash": user.password.encode("utf-8")}
|
||||
|
||||
def create_ui_user(self, username: str, password: bytes) -> str:
|
||||
"""Create ui user."""
|
||||
with self.__db_session() as session:
|
||||
if self.get_ui_user():
|
||||
return "User already exists"
|
||||
|
||||
session.add(Users(id=1, username=username, password=password.decode("utf-8")))
|
||||
|
||||
try:
|
||||
session.commit()
|
||||
except BaseException:
|
||||
return format_exc()
|
||||
|
||||
return ""
|
||||
|
|
|
@ -251,6 +251,14 @@ class Instances(Base):
|
|||
server_name = Column(String(256), nullable=False)
|
||||
|
||||
|
||||
class Users(Base):
|
||||
__tablename__ = "bw_ui_users"
|
||||
|
||||
id = Column(Integer, primary_key=True, default=1)
|
||||
username = Column(String(256), nullable=False, unique=True)
|
||||
password = Column(String(60), nullable=False)
|
||||
|
||||
|
||||
class Metadata(Base):
|
||||
__tablename__ = "bw_metadata"
|
||||
|
||||
|
|
139
src/ui/main.py
139
src/ui/main.py
|
@ -52,8 +52,7 @@ from jinja2 import Template
|
|||
from kubernetes import client as kube_client
|
||||
from kubernetes import config as kube_config
|
||||
from kubernetes.client.exceptions import ApiException as kube_ApiException
|
||||
from re import compile as re_compile
|
||||
from regex import match as regex_match
|
||||
from regex import compile as re_compile, match as regex_match
|
||||
from requests import get
|
||||
from shutil import move, rmtree
|
||||
from signal import SIGINT, signal, SIGTERM
|
||||
|
@ -122,24 +121,10 @@ gunicorn_logger = getLogger("gunicorn.error")
|
|||
app.logger.handlers = gunicorn_logger.handlers
|
||||
app.logger.setLevel(gunicorn_logger.level)
|
||||
|
||||
if not getenv("ADMIN_USERNAME"):
|
||||
app.logger.error("ADMIN_USERNAME is not set")
|
||||
stop(1)
|
||||
elif not getenv("ADMIN_PASSWORD"):
|
||||
app.logger.error("ADMIN_PASSWORD is not set")
|
||||
stop(1)
|
||||
|
||||
if not getenv("FLASK_DEBUG", False) and not regex_match(
|
||||
r"^(?=.*?\p{Lowercase_Letter})(?=.*?\p{Uppercase_Letter})(?=.*?\d)(?=.*?[ !\"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]).{8,}$",
|
||||
getenv("ADMIN_PASSWORD", "changeme"),
|
||||
):
|
||||
app.logger.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 (#@?!$%^&*-).")
|
||||
stop(1)
|
||||
|
||||
login_manager = LoginManager()
|
||||
login_manager.init_app(app)
|
||||
login_manager.login_view = "login"
|
||||
user = User(getenv("ADMIN_USERNAME", "admin"), getenv("ADMIN_PASSWORD", "changeme"))
|
||||
PLUGIN_KEYS = [
|
||||
"id",
|
||||
"name",
|
||||
|
@ -192,6 +177,26 @@ while not db.is_initialized():
|
|||
)
|
||||
sleep(5)
|
||||
|
||||
USER = db.get_ui_user()
|
||||
USER_PASSWORD_RX = re_compile(r"^(?=.*?\p{Lowercase_Letter})(?=.*?\p{Uppercase_Letter})(?=.*?\d)(?=.*?[ !\"#$%&'()*+,./:;<=>?@[\\\]^_`{|}~-]).{8,}$")
|
||||
|
||||
if USER:
|
||||
USER = User(**USER)
|
||||
elif getenv("ADMIN_USERNAME") and getenv("ADMIN_PASSWORD"):
|
||||
if len(getenv("ADMIN_USERNAME", "admin")) > 256:
|
||||
app.logger.error("The admin username is too long. It must be less than 256 characters.")
|
||||
stop(1)
|
||||
if not getenv("FLASK_DEBUG", False) and not USER_PASSWORD_RX.match(getenv("ADMIN_PASSWORD", "changeme")):
|
||||
app.logger.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 (#@?!$%^&*-).")
|
||||
stop(1)
|
||||
|
||||
USER = User(getenv("ADMIN_USERNAME", "admin"), getenv("ADMIN_PASSWORD", "changeme"))
|
||||
ret = db.create_ui_user(USER.get_id(), USER.password_hash)
|
||||
|
||||
if ret:
|
||||
app.logger.error(f"Couldn't create the admin user in the database: {ret}")
|
||||
stop(1)
|
||||
|
||||
app.logger.info("Database is ready")
|
||||
Path(sep, "var", "tmp", "bunkerweb", "ui.healthy").write_text("ok", encoding="utf-8")
|
||||
bw_version = Path(sep, "usr", "share", "bunkerweb", "VERSION").read_text(encoding="utf-8").strip()
|
||||
|
@ -204,7 +209,7 @@ try:
|
|||
CONFIG=Config(db),
|
||||
CONFIGFILES=ConfigFiles(app.logger, db),
|
||||
WTF_CSRF_SSL_STRICT=False,
|
||||
USER=user,
|
||||
USER=USER,
|
||||
SEND_FILE_MAX_AGE_DEFAULT=86400,
|
||||
PLUGIN_ARGS={},
|
||||
RELOADING=False,
|
||||
|
@ -226,6 +231,7 @@ csrf = CSRFProtect()
|
|||
csrf.init_app(app)
|
||||
|
||||
LOG_RX = re_compile(r"^(?P<date>\d+/\d+/\d+\s\d+:\d+:\d+)\s\[(?P<level>[a-z]+)\]\s\d+#\d+:\s(?P<message>[^\n]+)$")
|
||||
REVERSE_PROXY_PATH = re_compile(r"^(?P<host>https?://[a-zA-Z0-9.-]{1,255}(:((6553[0-5])|(655[0-2]\d)|(65[0-4]\d{2})|(6[0-4]\d{3})|([1-5]\d{4})|([0-5]{0,5})|(\d{1,4})))?)(?P<url>/.*)?$")
|
||||
|
||||
|
||||
def manage_bunkerweb(method: str, *args, operation: str = "reloads"):
|
||||
|
@ -307,7 +313,7 @@ def set_csp_header(response):
|
|||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User(user_id, getenv("ADMIN_PASSWORD", "changeme"))
|
||||
return app.config["USER"] if app.config["USER"] and user_id == app.config["USER"].get_id() else None
|
||||
|
||||
|
||||
@app.errorhandler(CSRFError)
|
||||
|
@ -325,7 +331,11 @@ def handle_csrf_error(_):
|
|||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return redirect(url_for("login"))
|
||||
if app.config["USER"]:
|
||||
if current_user.is_authenticated: # type: ignore
|
||||
return redirect(url_for("home"))
|
||||
return redirect(url_for("login"), 301)
|
||||
return redirect(url_for("setup"))
|
||||
|
||||
|
||||
@app.route("/loading")
|
||||
|
@ -340,6 +350,93 @@ def loading():
|
|||
)
|
||||
|
||||
|
||||
@app.route("/setup", methods=["GET", "POST"])
|
||||
def setup():
|
||||
if app.config["USER"]:
|
||||
if current_user.is_authenticated: # type: ignore
|
||||
return redirect(url_for("home"))
|
||||
return redirect(url_for("login"), 301)
|
||||
|
||||
if request.method == "POST":
|
||||
if not request.form:
|
||||
flash("Missing form data.", "error")
|
||||
return redirect(url_for("setup"))
|
||||
|
||||
if not any(key in request.form for key in ("admin_username", "admin_password", "admin_password_check", "server_name", "hostname")):
|
||||
flash("Missing either admin_username, admin_password, server_name or hostname parameter.", "error")
|
||||
return redirect(url_for("setup"))
|
||||
|
||||
error = False
|
||||
|
||||
if len(request.form["admin_username"]) > 256:
|
||||
flash("The admin username is too long. It must be less than 256 characters.", "error")
|
||||
error = True
|
||||
|
||||
if request.form["admin_password"] != request.form["admin_password_check"]:
|
||||
flash("The passwords do not match.", "error")
|
||||
error = True
|
||||
|
||||
if not USER_PASSWORD_RX.match(request.form["admin_password"]):
|
||||
flash("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 (#@?!$%^&*-).", "error")
|
||||
error = True
|
||||
|
||||
db_config = app.config["CONFIG"].get_config(methods=False)
|
||||
|
||||
server_names = db_config["SERVER_NAME"].split(" ")
|
||||
if request.form["server_name"] in server_names:
|
||||
flash(f"The hostname {request.form['server_name']} is already in use.", "error")
|
||||
error = True
|
||||
else:
|
||||
for server_name in server_names:
|
||||
if request.form["server_name"] in db_config[f"{server_name}_SERVER_NAME"].split(" "):
|
||||
flash(f"The hostname {request.form['server_name']} is already in use.", "error")
|
||||
error = True
|
||||
break
|
||||
|
||||
if not (hostname := REVERSE_PROXY_PATH.search(request.form["hostname"])):
|
||||
flash("The hostname is not valid.", "error")
|
||||
error = True
|
||||
|
||||
if error:
|
||||
return redirect(url_for("setup"))
|
||||
|
||||
assert hostname
|
||||
|
||||
app.config["USER"] = User(request.form["admin_username"], request.form["admin_password"])
|
||||
|
||||
ret = db.create_ui_user(app.config["USER"].get_id(), app.config["USER"].password_hash)
|
||||
if ret:
|
||||
flash(f"Couldn't create the admin user in the database: {ret}", "error")
|
||||
return redirect(url_for("setup"))
|
||||
|
||||
flash("The admin user was created successfully", "success")
|
||||
|
||||
app.config["RELOADING"] = True
|
||||
app.config["LAST_RELOAD"] = time()
|
||||
Thread(
|
||||
target=manage_bunkerweb,
|
||||
name="Reloading instances",
|
||||
args=(
|
||||
"services",
|
||||
{
|
||||
"SERVER_NAME": request.form["server_name"],
|
||||
"USE_UI": "yes",
|
||||
"USE_REVERSE_PROXY": "yes",
|
||||
"REVERSE_PROXY_HOST": hostname.group("host"),
|
||||
"REVERSE_PROXY_URL": hostname.group("url") or "/",
|
||||
"INTERCEPTED_ERROR_CODES": "400 404 405 413 429 500 501 502 503 504",
|
||||
},
|
||||
request.form["server_name"],
|
||||
request.form["server_name"],
|
||||
),
|
||||
kwargs={"operation": "new"},
|
||||
).start()
|
||||
|
||||
return redirect(url_for("loading", next=url_for("services"), message=f"Creating service {request.form['server_name']} for the web UI"))
|
||||
|
||||
return render_template("setup.html", username=getenv("ADMIN_USERNAME", ""), password=getenv("ADMIN_PASSWORD", ""))
|
||||
|
||||
|
||||
@app.route("/home")
|
||||
@login_required
|
||||
def home():
|
||||
|
@ -1418,7 +1515,9 @@ def login():
|
|||
401,
|
||||
)
|
||||
|
||||
if current_user.is_authenticated:
|
||||
if not app.config["USER"]:
|
||||
return redirect(url_for("setup"))
|
||||
elif current_user.is_authenticated: # type: ignore
|
||||
return redirect(url_for("home"))
|
||||
return render_template("login.html")
|
||||
|
||||
|
|
|
@ -1,22 +1,30 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from functools import cached_property
|
||||
from typing import Optional
|
||||
from flask_login import UserMixin
|
||||
from bcrypt import checkpw, hashpw, gensalt
|
||||
|
||||
|
||||
class User(UserMixin):
|
||||
def __init__(self, _id, password):
|
||||
self.__id = _id
|
||||
self.__password = hashpw(password.encode("utf-8"), gensalt())
|
||||
def __init__(self, username: str, password: Optional[str] = None, password_hash: Optional[bytes] = None):
|
||||
self.id = username
|
||||
|
||||
def get_id(self):
|
||||
"""
|
||||
Get the id of the user
|
||||
:return: The id of the user
|
||||
"""
|
||||
return self.__id
|
||||
if not password:
|
||||
assert password_hash, "Either password or password_hash must be provided"
|
||||
|
||||
def check_password(self, password):
|
||||
self.__password = password_hash or hashpw(password.encode("utf-8"), gensalt()) # type: ignore
|
||||
|
||||
@cached_property
|
||||
def password_hash(self) -> bytes:
|
||||
"""
|
||||
Get the password hash
|
||||
|
||||
:return: The password hash
|
||||
"""
|
||||
return self.__password
|
||||
|
||||
def check_password(self, password: str):
|
||||
"""
|
||||
Check if the password is correct by hashing it and comparing it to the stored hash
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -6,7 +6,7 @@
|
|||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>BunkerWeb UI</title>
|
||||
<title>BunkerWeb UI | Log in</title>
|
||||
<link href="images/favicon.ico" rel="icon" type="image/x-icon" />
|
||||
<link rel="stylesheet" href="css/dashboard.css" />
|
||||
<link rel="stylesheet" href="css/login.css" />
|
||||
|
@ -69,7 +69,9 @@
|
|||
<div
|
||||
class="mx-4 lg:mx-0 col-span-2 lg:col-span-1 bg-none lg:bg-gray-50 h-full flex flex-col items-center justify-center"
|
||||
>
|
||||
<div class="bg-gray-50 rounded px-12 py-16 w-full max-w-[400px]">
|
||||
<div
|
||||
class="bg-gray-50 rounded px-4 sm:px-12 py-16 w-full max-w-[400px]"
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<img
|
||||
class="lg:hidden max-w-60 max-h-30 mb-6"
|
||||
|
@ -97,6 +99,7 @@
|
|||
>
|
||||
Username
|
||||
</h5>
|
||||
<label class="sr-only" for="username">username</label>
|
||||
<input
|
||||
type="username"
|
||||
id="username"
|
||||
|
@ -115,6 +118,7 @@
|
|||
>
|
||||
Password
|
||||
</h5>
|
||||
<label class="sr-only" for="password">password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
|
|
|
@ -0,0 +1,291 @@
|
|||
<!DOCTYPE html>
|
||||
{% block content %}
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>BunkerWeb UI | Setup</title>
|
||||
<link href="images/favicon.ico" rel="icon" type="image/x-icon" />
|
||||
<link rel="stylesheet" href="css/dashboard.css" />
|
||||
<link rel="stylesheet" href="css/login.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
data-loader
|
||||
class="fixed z-[10000] transition duration-300 h-screen w-screen bg-primary flex justify-center align-middle items-center"
|
||||
>
|
||||
<img
|
||||
data-loader-img
|
||||
src="images/logo-menu-2.png"
|
||||
class="duration-300 w-40 h-12 sm:w-50 sm:h-14 md:w-60 md:h-16 lg:w-80 lg:h-24 inline transition-all"
|
||||
alt="main logo"
|
||||
/>
|
||||
</div>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %} {% if
|
||||
messages %}
|
||||
<!-- flash message-->
|
||||
{% for category, message in messages %}
|
||||
<div
|
||||
data-flash-message
|
||||
class="p-4 mb-1 md:mb-3 md:mr-3 z-[1001] flex flex-col fixed bottom-0 right-0 w-full md:w-1/2 max-w-[300px] min-h-20 bg-white rounded-lg dark:brightness-110 hover:scale-102 transition shadow-md break-words dark:bg-slate-850 dark:shadow-dark-xl bg-clip-border"
|
||||
>
|
||||
<button
|
||||
data-close-flash-message
|
||||
role="close alert message"
|
||||
type="button"
|
||||
class="absolute right-7 top-1.5"
|
||||
>
|
||||
<svg
|
||||
class="cursor-pointer fill-gray-600 dark:fill-gray-300 dark:opacity-80 absolute h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 320 512"
|
||||
>
|
||||
<path
|
||||
d="M310.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L160 210.7 54.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L114.7 256 9.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 301.3 265.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L205.3 256 310.6 150.6z"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
{% if category == 'error' %}
|
||||
<h5 class="text-lg mb-0 text-red-500">Error</h5>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-0 text-sm">
|
||||
{{ message|safe }}
|
||||
</p>
|
||||
{% else %}
|
||||
<h5 class="text-lg mb-0 text-green-500">Success</h5>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-0 text-sm">
|
||||
{{ message|safe }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<!-- end flash message-->
|
||||
{% endif %} {% endwith %}
|
||||
<!-- end flash message-->
|
||||
<!--content -->
|
||||
<main class="grid grid-cols-2 align-middle items-center min-h-screen">
|
||||
<!--form -->
|
||||
<div
|
||||
class="mx-4 lg:mx-0 col-span-2 h-full flex flex-col items-center justify-center"
|
||||
>
|
||||
<div
|
||||
class="bg-gray-50 rounded px-4 sm:px-12 pt-10 pb-4 w-full max-w-[400px]"
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<img
|
||||
class="max-w-60 max-h-30 mb-6"
|
||||
src="images/BUNKERWEB-print-hd.png"
|
||||
alt="logo"
|
||||
class="logo"
|
||||
/>
|
||||
</div>
|
||||
<h1 class="block text-center font-bold dark:text-white mb-8 text-3xl">
|
||||
Setup BunkerWeb
|
||||
</h1>
|
||||
<form id="setup-form" action="setup" method="POST" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input
|
||||
type="hidden"
|
||||
name="next"
|
||||
value="{{ request.values.get('next', '') }}"
|
||||
/>
|
||||
<!-- username inpt-->
|
||||
<div class="flex flex-col relative col-span-12 my-3">
|
||||
<h5
|
||||
class="text-lg my-1 transition duration-300 ease-in-out dark:opacity-90 text-md font-bold m-0 dark:text-gray-300"
|
||||
>
|
||||
Username
|
||||
</h5>
|
||||
<label class="sr-only" for="admin_username">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
id="admin_username"
|
||||
name="admin_username"
|
||||
class="col-span-12 dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 disabled:opacity-75 focus:valid:border-green-500 focus:invalid:border-red-500 outline-none focus:border-primary text-sm leading-5.6 ease block w-full appearance-none rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-4 py-2 font-normal text-gray-700 transition-all placeholder:text-gray-500"
|
||||
placeholder="enter username"
|
||||
value="{{ username }}"
|
||||
pattern="(.*?)"
|
||||
maxlength="256"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<!-- end username inpt-->
|
||||
<!-- password inpt-->
|
||||
<div class="flex flex-col relative col-span-12 my-3">
|
||||
<h5
|
||||
class="text-lg my-1 transition duration-300 ease-in-out dark:opacity-90 text-md font-bold m-0 dark:text-gray-300"
|
||||
>
|
||||
Password
|
||||
</h5>
|
||||
<label class="sr-only" for="admin_password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="admin_password"
|
||||
name="admin_password"
|
||||
class="col-span-12 dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 disabled:opacity-75 focus:valid:border-green-500 focus:invalid:border-red-500 outline-none focus:border-primary text-sm leading-5.6 ease block w-full appearance-none rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-4 py-2 font-normal text-gray-700 transition-all placeholder:text-gray-500"
|
||||
placeholder="enter password"
|
||||
value="{{ password }}"
|
||||
pattern="^(?=.*?\d)(?=.*?[ !\u0022#$%&'\(\)*+,.\/:;<=>?@\[\\\]^_`\u007B\u007C\u007D\u007E\u002D]).{8,}$"
|
||||
minlength="8"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<!-- end password inpt-->
|
||||
<!-- password inpt-->
|
||||
<div class="flex flex-col relative col-span-12 my-3">
|
||||
<h5
|
||||
class="text-lg my-1 transition duration-300 ease-in-out dark:opacity-90 text-md font-bold m-0 dark:text-gray-300"
|
||||
>
|
||||
Confirm Password
|
||||
</h5>
|
||||
<label class="sr-only" for="admin_password_check"
|
||||
>Confirm Password</label
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
id="admin_password_check"
|
||||
name="admin_password_check"
|
||||
class="col-span-12 dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 disabled:opacity-75 focus:valid:border-green-500 focus:invalid:border-red-500 outline-none focus:border-primary text-sm leading-5.6 ease block w-full appearance-none rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-4 py-2 font-normal text-gray-700 transition-all placeholder:text-gray-500"
|
||||
placeholder="confirm password"
|
||||
value="{{ password }}"
|
||||
pattern="^(?=.*?\d)(?=.*?[ !\u0022#$%&'\(\)*+,.\/:;<=>?@\[\\\]^_`\u007B\u007C\u007D\u007E\u002D]).{8,}$"
|
||||
minlength="8"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<!-- end password inpt-->
|
||||
<!-- hostname-->
|
||||
<div class="flex flex-col relative col-span-12 my-3">
|
||||
<h5
|
||||
class="text-lg my-1 transition duration-300 ease-in-out dark:opacity-90 text-md font-bold m-0 dark:text-gray-300"
|
||||
>
|
||||
Hostname
|
||||
</h5>
|
||||
<label class="sr-only" for="hostname">Hostname</label>
|
||||
<input
|
||||
type="text"
|
||||
id="hostname"
|
||||
name="hostname"
|
||||
class="col-span-12 dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 disabled:opacity-75 focus:valid:border-green-500 focus:invalid:border-red-500 outline-none focus:border-primary text-sm leading-5.6 ease block w-full appearance-none rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-4 py-2 font-normal text-gray-700 transition-all placeholder:text-gray-500"
|
||||
placeholder="enter hostname"
|
||||
pattern="^https?:\/\/([a-zA-Z0-9.\u002D]{1,255}(:((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4})))?)(\/.*)?$"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<!-- end hostname-->
|
||||
<!-- server name-->
|
||||
<div class="flex flex-col relative col-span-12 my-3">
|
||||
<h5
|
||||
class="text-lg my-1 transition duration-300 ease-in-out dark:opacity-90 text-md font-bold m-0 dark:text-gray-300"
|
||||
>
|
||||
Server name
|
||||
</h5>
|
||||
<label class="sr-only" for="server_names">server name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="server_name"
|
||||
name="server_name"
|
||||
class="col-span-12 dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 disabled:opacity-75 focus:valid:border-green-500 focus:invalid:border-red-500 outline-none focus:border-primary text-sm leading-5.6 ease block w-full appearance-none rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-4 py-2 font-normal text-gray-700 transition-all placeholder:text-gray-500"
|
||||
placeholder="app1.example.com"
|
||||
minlength="1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<!-- end server name-->
|
||||
<div class="flex justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
id="setup-button"
|
||||
name="setup-button"
|
||||
value="setup"
|
||||
class="tracking-wide my-4 dark:brightness-90 inline-block px-6 py-3 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer bg-primary hover:bg-primary/80 focus:bg-primary/80 leading-normal text-sm ease-in shadow-xs bg-150 bg-x-25 hover:-translate-y-px active:opacity-85 hover:shadow-md"
|
||||
>
|
||||
Setup
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- end form -->
|
||||
</main>
|
||||
<script>
|
||||
class Loader {
|
||||
constructor() {
|
||||
this.menuContainer = document.querySelector("[data-menu-container]");
|
||||
this.logoContainer = document.querySelector("[data-loader]");
|
||||
this.logoEl = document.querySelector("[data-loader-img]");
|
||||
this.isLoading = true;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.loading();
|
||||
window.addEventListener("load", (e) => {
|
||||
setTimeout(() => {
|
||||
this.logoContainer.classList.add("opacity-0");
|
||||
}, 350);
|
||||
|
||||
setTimeout(() => {
|
||||
this.isLoading = false;
|
||||
this.logoContainer.classList.add("hidden");
|
||||
}, 650);
|
||||
|
||||
setTimeout(() => {
|
||||
this.logoContainer.remove();
|
||||
}, 800);
|
||||
});
|
||||
}
|
||||
|
||||
loading() {
|
||||
if ((this.isLoading = true)) {
|
||||
setTimeout(() => {
|
||||
this.logoEl.classList.toggle("scale-105");
|
||||
this.loading();
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FlashMsg {
|
||||
constructor() {
|
||||
this.delayBeforeRemove = 8000;
|
||||
this.init();
|
||||
}
|
||||
|
||||
//remove flash message after this.delay if exist
|
||||
init() {
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
try {
|
||||
const flashEl = document.querySelector("[data-flash-message]");
|
||||
setTimeout(() => {
|
||||
try {
|
||||
flashEl.remove();
|
||||
} catch (err) {}
|
||||
}, this.delayBeforeRemove);
|
||||
} catch (err) {}
|
||||
});
|
||||
|
||||
window.addEventListener("click", (e) => {
|
||||
try {
|
||||
if (
|
||||
e.target
|
||||
.closest("button")
|
||||
.hasAttribute("data-close-flash-message")
|
||||
) {
|
||||
const closeBtn = e.target.closest("button");
|
||||
const flashEl = closeBtn.closest("[data-flash-message]");
|
||||
flashEl.remove();
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const setLoader = new Loader();
|
||||
const setFlash = new FlashMsg();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
{% endblock %}
|
Loading…
Reference in New Issue