mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
Merge pull request #827 from bunkerity/ui
Merge branch "ui" into branch "dev"
This commit is contained in:
commit
364a779b2a
18 changed files with 972 additions and 231 deletions
1
.gitleaksignore
Normal file
1
.gitleaksignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
src/ui/templates/profile.html:hashicorp-tf-password:343
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1674,18 +1674,43 @@ class Database:
|
|||
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()
|
||||
user = session.query(Users).with_entities(Users.username, Users.password, Users.is_two_factor_enabled, Users.secret_token, Users.method).filter_by(id=1).first()
|
||||
if not user:
|
||||
return None
|
||||
return {"username": user.username, "password_hash": user.password.encode("utf-8")}
|
||||
return {
|
||||
"username": user.username,
|
||||
"password_hash": user.password.encode("utf-8"),
|
||||
"is_two_factor_enabled": user.is_two_factor_enabled,
|
||||
"secret_token": user.secret_token,
|
||||
"method": user.method,
|
||||
}
|
||||
|
||||
def create_ui_user(self, username: str, password: bytes) -> str:
|
||||
def create_ui_user(self, username: str, password: bytes, *, secret_token: Optional[str] = None, method: str = "manual") -> 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")))
|
||||
session.add(Users(id=1, username=username, password=password.decode("utf-8"), secret_token=secret_token, method=method))
|
||||
|
||||
try:
|
||||
session.commit()
|
||||
except BaseException:
|
||||
return format_exc()
|
||||
|
||||
return ""
|
||||
|
||||
def update_ui_user(self, username: str, password: bytes, is_two_factor_enabled: bool = False, secret_token: Optional[str] = None) -> str:
|
||||
"""Update ui user."""
|
||||
with self.__db_session() as session:
|
||||
user = session.query(Users).filter_by(id=1).first()
|
||||
if not user:
|
||||
return "User not found"
|
||||
|
||||
user.username = username
|
||||
user.password = password.decode("utf-8")
|
||||
user.is_two_factor_enabled = is_two_factor_enabled
|
||||
user.secret_token = secret_token
|
||||
|
||||
try:
|
||||
session.commit()
|
||||
|
|
|
|||
|
|
@ -257,6 +257,9 @@ class Users(Base):
|
|||
id = Column(Integer, primary_key=True, default=1)
|
||||
username = Column(String(256), nullable=False, unique=True)
|
||||
password = Column(String(60), nullable=False)
|
||||
is_two_factor_enabled = Column(Boolean, nullable=False, default=False)
|
||||
secret_token = Column(String(32), nullable=True, unique=True)
|
||||
method = Column(METHODS_ENUM, nullable=False, default="manual")
|
||||
|
||||
|
||||
class Metadata(Base):
|
||||
|
|
|
|||
291
src/ui/main.py
291
src/ui/main.py
|
|
@ -37,15 +37,10 @@ from flask import (
|
|||
render_template,
|
||||
request,
|
||||
send_file,
|
||||
session,
|
||||
url_for,
|
||||
)
|
||||
from flask_login import (
|
||||
current_user,
|
||||
LoginManager,
|
||||
login_required,
|
||||
login_user,
|
||||
logout_user,
|
||||
)
|
||||
from flask_login import current_user, LoginManager, login_required, login_user, logout_user
|
||||
from flask_wtf.csrf import CSRFProtect, CSRFError, generate_csrf
|
||||
from glob import glob
|
||||
from hashlib import sha256
|
||||
|
|
@ -66,7 +61,6 @@ from threading import Thread
|
|||
from tempfile import NamedTemporaryFile
|
||||
from time import sleep, time
|
||||
from traceback import format_exc
|
||||
from typing import Optional
|
||||
from zipfile import BadZipFile, ZipFile
|
||||
|
||||
from src.Instances import Instances
|
||||
|
|
@ -75,7 +69,7 @@ from src.Config import Config
|
|||
from src.ReverseProxied import ReverseProxied
|
||||
from src.User import User
|
||||
|
||||
from utils import check_settings, path_to_dict
|
||||
from utils import check_settings, get_b64encoded_qr_image, path_to_dict
|
||||
from Database import Database # type: ignore
|
||||
from logging import getLogger
|
||||
|
||||
|
|
@ -113,14 +107,10 @@ app = Flask(
|
|||
static_folder="static",
|
||||
template_folder="templates",
|
||||
)
|
||||
app.secret_key = getenv("FLASK_SECRET", urandom(32))
|
||||
|
||||
PROXY_NUMBERS = int(getenv("PROXY_NUMBERS", "1"))
|
||||
app.wsgi_app = ReverseProxied(
|
||||
app.wsgi_app,
|
||||
x_for=PROXY_NUMBERS,
|
||||
x_proto=PROXY_NUMBERS,
|
||||
x_host=PROXY_NUMBERS,
|
||||
x_prefix=PROXY_NUMBERS,
|
||||
)
|
||||
app.wsgi_app = ReverseProxied(app.wsgi_app, x_for=PROXY_NUMBERS, x_proto=PROXY_NUMBERS, x_host=PROXY_NUMBERS, x_prefix=PROXY_NUMBERS)
|
||||
gunicorn_logger = getLogger("gunicorn.error")
|
||||
app.logger.handlers = gunicorn_logger.handlers
|
||||
app.logger.setLevel(gunicorn_logger.level)
|
||||
|
|
@ -129,14 +119,7 @@ app.logger.setLevel(gunicorn_logger.level)
|
|||
login_manager = LoginManager()
|
||||
login_manager.init_app(app)
|
||||
login_manager.login_view = "login"
|
||||
PLUGIN_KEYS = [
|
||||
"id",
|
||||
"name",
|
||||
"description",
|
||||
"version",
|
||||
"stream",
|
||||
"settings",
|
||||
]
|
||||
PLUGIN_KEYS = ["id", "name", "description", "version", "stream", "settings"]
|
||||
|
||||
INTEGRATION = "Linux"
|
||||
integration_path = Path(sep, "usr", "share", "bunkerweb", "INTEGRATION")
|
||||
|
|
@ -164,21 +147,13 @@ elif INTEGRATION == "Kubernetes":
|
|||
|
||||
db = Database(app.logger, ui=True)
|
||||
|
||||
if INTEGRATION in (
|
||||
"Swarm",
|
||||
"Kubernetes",
|
||||
"Autoconf",
|
||||
):
|
||||
if INTEGRATION in ("Swarm", "Kubernetes", "Autoconf"):
|
||||
while not db.is_autoconf_loaded():
|
||||
app.logger.warning(
|
||||
"Autoconf is not loaded yet in the database, retrying in 5s ...",
|
||||
)
|
||||
app.logger.warning("Autoconf is not loaded yet in the database, retrying in 5s ...")
|
||||
sleep(5)
|
||||
|
||||
while not db.is_initialized():
|
||||
app.logger.warning(
|
||||
"Database is not initialized, retrying in 5s ...",
|
||||
)
|
||||
app.logger.warning("Database is not initialized, retrying in 5s ...")
|
||||
sleep(5)
|
||||
|
||||
USER = "Error"
|
||||
|
|
@ -190,16 +165,37 @@ USER_PASSWORD_RX = re_compile(r"^(?=.*?\p{Lowercase_Letter})(?=.*?\p{Uppercase_L
|
|||
|
||||
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 getenv("ADMIN_USERNAME") or getenv("ADMIN_PASSWORD"):
|
||||
if USER.method == "manual":
|
||||
updated = False
|
||||
if getenv("ADMIN_USERNAME", "") and USER.get_id() != getenv("ADMIN_USERNAME", ""):
|
||||
USER.id = getenv("ADMIN_USERNAME", "")
|
||||
updated = True
|
||||
if getenv("ADMIN_PASSWORD", "") and not USER.check_password(getenv("ADMIN_PASSWORD", "")):
|
||||
USER.update_password(getenv("ADMIN_PASSWORD", ""))
|
||||
updated = True
|
||||
|
||||
if updated:
|
||||
ret = db.update_ui_user(USER.get_id(), USER.password_hash, USER.is_two_factor_enabled, app.config["USER"].secret_token)
|
||||
if ret:
|
||||
app.logger.error(f"Couldn't update the admin user in the database: {ret}")
|
||||
stop(1)
|
||||
app.logger.info("The admin user was updated successfully")
|
||||
else:
|
||||
app.logger.error("The admin user wasn't created manually. You can't change it from the environment variables.")
|
||||
elif getenv("FLASK_DEBUG", False) or getenv("ADMIN_USERNAME") and getenv("ADMIN_PASSWORD"):
|
||||
if not getenv("FLASK_DEBUG", False):
|
||||
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)
|
||||
elif 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_name = getenv("ADMIN_USERNAME", "admin")
|
||||
USER = User(user_name, getenv("ADMIN_PASSWORD", "changeme"))
|
||||
ret = db.create_ui_user(user_name, USER.password_hash)
|
||||
|
||||
if ret:
|
||||
app.logger.error(f"Couldn't create the admin user in the database: {ret}")
|
||||
|
|
@ -281,6 +277,7 @@ def manage_bunkerweb(method: str, *args, operation: str = "reloads"):
|
|||
# update changes in db
|
||||
ret = db.checked_changes(changes, value=True)
|
||||
if ret:
|
||||
app.logger.error(f"Couldn't set the changes to checked in the database: {ret}")
|
||||
app.config["TO_FLASH"].append({"content": f"An error occurred when setting the changes to checked in the database : {ret}", "type": "error"})
|
||||
if method == "global_config":
|
||||
operation = app.config["CONFIG"].edit_global_conf(args[0])
|
||||
|
|
@ -332,9 +329,28 @@ def handle_csrf_error(_):
|
|||
:param e: The exception object
|
||||
:return: A template with the error message and a 401 status code.
|
||||
"""
|
||||
session.clear()
|
||||
logout_user()
|
||||
flash("Wrong CSRF token !", "error")
|
||||
return render_template("login.html"), 403
|
||||
if not app.config["USER"]:
|
||||
return render_template("setup.html"), 403
|
||||
return render_template("login.html", is_totp=app.config["USER"].is_two_factor_enabled), 403
|
||||
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
if app.config["USER"] and current_user.is_authenticated:
|
||||
passed = True
|
||||
if not session.get("totp_validated", False) and app.config["USER"].is_two_factor_enabled and "/totp" not in request.path and not request.path.startswith(("/css", "/images", "/js", "/json", "/webfonts")):
|
||||
return redirect(url_for("totp", next=request.form.get("next")))
|
||||
elif session.get("ip") != request.remote_addr:
|
||||
passed = False
|
||||
elif session.get("user_agent") != request.headers.get("User-Agent"):
|
||||
passed = False
|
||||
|
||||
if not passed:
|
||||
logout_user()
|
||||
session.clear()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
|
|
@ -349,13 +365,7 @@ def index():
|
|||
@app.route("/loading")
|
||||
@login_required
|
||||
def loading():
|
||||
next_url: str = request.values.get("next", None) or url_for("home")
|
||||
message: Optional[str] = request.values.get("message", None)
|
||||
return render_template(
|
||||
"loading.html",
|
||||
message=message if message is not None else "Loading",
|
||||
next=next_url,
|
||||
)
|
||||
return render_template("loading.html", message=request.values.get("message", "Loading"), next=request.values.get("next", None) or url_for("home"))
|
||||
|
||||
|
||||
@app.route("/check", methods=["GET"])
|
||||
|
|
@ -416,10 +426,11 @@ def setup():
|
|||
if error:
|
||||
return redirect(url_for("setup"))
|
||||
|
||||
app.config["USER"] = User(request.form["admin_username"], request.form["admin_password"])
|
||||
app.config["USER"] = User(request.form["admin_username"], request.form["admin_password"], method="ui")
|
||||
|
||||
ret = db.create_ui_user(app.config["USER"].get_id(), app.config["USER"].password_hash)
|
||||
ret = db.create_ui_user(request.form["admin_username"], app.config["USER"].password_hash, method="ui")
|
||||
if ret:
|
||||
app.logger.error(f"Couldn't create the admin user in the database: {ret}")
|
||||
flash(f"Couldn't create the admin user in the database: {ret}", "error")
|
||||
return redirect(url_for("setup"))
|
||||
|
||||
|
|
@ -458,6 +469,31 @@ def setup():
|
|||
)
|
||||
|
||||
|
||||
@app.route("/totp", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def totp():
|
||||
if request.method == "POST":
|
||||
if not request.form:
|
||||
flash("Missing form data.", "error")
|
||||
return redirect(url_for("totp"))
|
||||
|
||||
if "totp_token" not in request.form:
|
||||
flash("Missing token parameter.", "error")
|
||||
return redirect(url_for("totp"))
|
||||
|
||||
if not app.config["USER"].check_otp(request.form["totp_token"]):
|
||||
flash("The token is invalid.", "error")
|
||||
return redirect(url_for("totp"))
|
||||
|
||||
session["totp_validated"] = True
|
||||
redirect(url_for("loading", next=request.form.get("next") or url_for("home")))
|
||||
|
||||
if app.config["USER"] and (not app.config["USER"].is_two_factor_enabled or session.get("totp_validated", False)):
|
||||
return redirect(url_for("home"))
|
||||
|
||||
return render_template("totp.html", dark_mode=app.config["DARK_MODE"])
|
||||
|
||||
|
||||
@app.route("/home")
|
||||
@login_required
|
||||
def home():
|
||||
|
|
@ -471,13 +507,8 @@ def home():
|
|||
services_number: the number of services
|
||||
posts: a list of posts
|
||||
"""
|
||||
|
||||
try:
|
||||
r = get(
|
||||
"https://github.com/bunkerity/bunkerweb/releases/latest",
|
||||
allow_redirects=True,
|
||||
timeout=5,
|
||||
)
|
||||
r = get("https://github.com/bunkerity/bunkerweb/releases/latest", allow_redirects=True, timeout=5)
|
||||
r.raise_for_status()
|
||||
except BaseException:
|
||||
r = None
|
||||
|
|
@ -531,37 +562,104 @@ def profile():
|
|||
flash("Missing form data.", "error")
|
||||
return redirect(url_for("profile"))
|
||||
|
||||
if not any(key in request.form for key in ("admin_username", "admin_password", "admin_password_check")):
|
||||
flash("Missing either admin_username, admin_password or admin_password_check.", "error")
|
||||
return redirect(url_for("profile"))
|
||||
|
||||
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 "curr_password" in request.form:
|
||||
if not app.config["USER"].check_password(request.form["curr_password"]):
|
||||
flash("The current password is incorrect.", "error")
|
||||
error = True
|
||||
|
||||
if request.form["admin_password"] != request.form["admin_password_check"]:
|
||||
flash("The passwords do not match.", "error")
|
||||
error = True
|
||||
if request.form.get("admin_username") and len(request.form["admin_username"]) > 256:
|
||||
flash("The admin username is too long. It must be less than 256 characters.", "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
|
||||
if request.form.get("admin_password"):
|
||||
if not request.form.get("admin_password_check"):
|
||||
flash("Missing admin_password_check parameter.", "error")
|
||||
error = True
|
||||
elif request.form["admin_password"] != request.form["admin_password_check"]:
|
||||
flash("The passwords do not match.", "error")
|
||||
error = True
|
||||
elif 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
|
||||
elif request.form.get("admin_password_check"):
|
||||
flash("Missing admin_password parameter.", "error")
|
||||
error = True
|
||||
|
||||
if error:
|
||||
if not error and not any(request.form.get(key) for key in ("admin_username", "admin_password")):
|
||||
flash("Nothing to update.")
|
||||
error = True
|
||||
|
||||
if error:
|
||||
return redirect(url_for("profile"))
|
||||
|
||||
app.config["USER"] = User(
|
||||
request.form.get("admin_username") or app.config["USER"].get_id(),
|
||||
request.form.get("admin_password") or request.form["curr_password"],
|
||||
is_two_factor_enabled=app.config["USER"].is_two_factor_enabled,
|
||||
secret_token=app.config["USER"].secret_token,
|
||||
method=app.config["USER"].method,
|
||||
)
|
||||
|
||||
session.clear()
|
||||
logout_user()
|
||||
elif "totp_password" in request.form:
|
||||
if "totp_token" not in request.form:
|
||||
flash("Missing totp_token parameter.", "error")
|
||||
return redirect(url_for("profile"))
|
||||
|
||||
if not app.config["USER"].check_password(request.form.get("totp_password", "")):
|
||||
flash("The current password is incorrect.", "error")
|
||||
error = True
|
||||
|
||||
if not app.config["USER"].check_otp(request.form["totp_token"]):
|
||||
flash("The token is invalid.", "error")
|
||||
error = True
|
||||
|
||||
app.logger.warning(request.form["totp_password"])
|
||||
|
||||
if error:
|
||||
return redirect(url_for("profile"))
|
||||
|
||||
app.logger.warning("TOTP validated")
|
||||
|
||||
session["totp_validated"] = not app.config["USER"].is_two_factor_enabled
|
||||
|
||||
if app.config["USER"].is_two_factor_enabled:
|
||||
app.config["USER"].secret_token = None
|
||||
|
||||
app.config["USER"].is_two_factor_enabled = session["totp_validated"]
|
||||
|
||||
app.logger.warning(app.config["USER"])
|
||||
else:
|
||||
flash("Missing form data.", "error")
|
||||
return redirect(url_for("profile"))
|
||||
|
||||
# TODO: Update username and password (if changed)
|
||||
return Response(status=200)
|
||||
ret = db.update_ui_user(
|
||||
app.config["USER"].get_id(),
|
||||
app.config["USER"].password_hash,
|
||||
app.config["USER"].is_two_factor_enabled,
|
||||
app.config["USER"].secret_token if app.config["USER"].is_two_factor_enabled else None,
|
||||
)
|
||||
if ret:
|
||||
app.logger.error(f"Couldn't update the admin user in the database: {ret}")
|
||||
flash(f"Couldn't update the admin user in the database: {ret}", "error")
|
||||
return redirect(url_for("profile"))
|
||||
|
||||
return render_template(
|
||||
"profile.html",
|
||||
title="Profile",
|
||||
username=getenv("ADMIN_USERNAME", ""),
|
||||
password=getenv("ADMIN_PASSWORD", ""),
|
||||
dark_mode=app.config["DARK_MODE"],
|
||||
)
|
||||
app.logger.warning("User updated")
|
||||
|
||||
return redirect(url_for("profile"))
|
||||
|
||||
secret_token = ""
|
||||
totp_qr_image = ""
|
||||
|
||||
if not app.config["USER"].is_two_factor_enabled:
|
||||
app.config["USER"].refresh_totp()
|
||||
secret_token = app.config["USER"].secret_token
|
||||
totp_qr_image = get_b64encoded_qr_image(app.config["USER"].get_authentication_setup_uri())
|
||||
|
||||
return render_template("profile.html", username=app.config["USER"].get_id(), is_totp=app.config["USER"].is_two_factor_enabled, secret_token=secret_token, totp_qr_image=totp_qr_image, dark_mode=app.config["DARK_MODE"])
|
||||
|
||||
|
||||
@app.route("/instances", methods=["GET", "POST"])
|
||||
|
|
@ -811,7 +909,7 @@ def configs():
|
|||
|
||||
if operation:
|
||||
flash(operation, "error")
|
||||
return redirect(url_for("loading", next=url_for("configs"))), 500
|
||||
return redirect(url_for("loading", next=url_for("configs")))
|
||||
|
||||
if request.form["operation"] in ("new", "edit"):
|
||||
if not app.config["CONFIGFILES"].check_name(variables["name"]):
|
||||
|
|
@ -897,7 +995,7 @@ def plugins():
|
|||
|
||||
if variables["external"] != "True":
|
||||
flash(f"Can't delete internal plugin {variables['name']}", "error")
|
||||
return redirect(url_for("loading", next=url_for("plugins"))), 500
|
||||
return redirect(url_for("loading", next=url_for("plugins")))
|
||||
|
||||
plugins = app.config["CONFIG"].get_plugins()
|
||||
for plugin in deepcopy(plugins):
|
||||
|
|
@ -1564,25 +1662,27 @@ def login():
|
|||
if request.method == "POST" and "username" in request.form and "password" in request.form:
|
||||
if app.config["USER"].get_id() == request.form["username"] and app.config["USER"].check_password(request.form["password"]):
|
||||
# log the user in
|
||||
next_url = request.form.get("next")
|
||||
login_user(app.config["USER"])
|
||||
session["ip"] = request.remote_addr
|
||||
session["user_agent"] = request.headers.get("User-Agent")
|
||||
session["totp_validated"] = False
|
||||
login_user(app.config["USER"], duration=timedelta(hours=1))
|
||||
|
||||
# redirect him to the page he originally wanted or to the home page
|
||||
return redirect(url_for("loading", next=next_url or url_for("home")))
|
||||
return redirect(url_for("loading", next=request.form.get("next") or url_for("home")))
|
||||
else:
|
||||
flash("Invalid username or password", "error")
|
||||
fail = True
|
||||
|
||||
if fail:
|
||||
return (
|
||||
render_template("login.html", error="Invalid username or password"),
|
||||
401,
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
kwargs = {
|
||||
"is_totp": app.config["USER"].is_two_factor_enabled,
|
||||
} | ({"error": "Invalid username or password"} if fail else {})
|
||||
|
||||
return render_template("login.html", **kwargs), 401 if fail else 200
|
||||
|
||||
|
||||
@app.route("/darkmode", methods=["POST"])
|
||||
|
|
@ -1622,5 +1722,6 @@ def check_reloading():
|
|||
@app.route("/logout")
|
||||
@login_required
|
||||
def logout():
|
||||
session.clear()
|
||||
logout_user()
|
||||
return redirect(url_for("login"))
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ Flask-Login==0.6.3
|
|||
Flask_WTF==1.2.1
|
||||
gunicorn[gthread]==21.2.0
|
||||
importlib-metadata==7.0.1
|
||||
pyotp==2.9.0
|
||||
python_dateutil==2.8.2
|
||||
qrcode==7.4.2
|
||||
regex==2023.12.25
|
||||
werkzeug==3.0.1
|
||||
|
|
|
|||
|
|
@ -151,10 +151,22 @@ packaging==23.2 \
|
|||
--hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \
|
||||
--hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7
|
||||
# via gunicorn
|
||||
pyotp==2.9.0 \
|
||||
--hash=sha256:346b6642e0dbdde3b4ff5a930b664ca82abfa116356ed48cc42c7d6590d36f63 \
|
||||
--hash=sha256:81c2e5865b8ac55e825b0358e496e1d9387c811e85bb40e71a3b29b288963612
|
||||
# via -r requirements.in
|
||||
pypng==0.20220715.0 \
|
||||
--hash=sha256:4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c \
|
||||
--hash=sha256:739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1
|
||||
# via qrcode
|
||||
python-dateutil==2.8.2 \
|
||||
--hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \
|
||||
--hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9
|
||||
# via -r requirements.in
|
||||
qrcode==7.4.2 \
|
||||
--hash=sha256:581dca7a029bcb2deef5d01068e39093e80ef00b4a61098a2182eac59d01643a \
|
||||
--hash=sha256:9dd969454827e127dbd93696b20747239e6d540e082937c90f14ac95b30f5845
|
||||
# via -r requirements.in
|
||||
regex==2023.12.25 \
|
||||
--hash=sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5 \
|
||||
--hash=sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770 \
|
||||
|
|
@ -258,6 +270,10 @@ soupsieve==2.5 \
|
|||
--hash=sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690 \
|
||||
--hash=sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7
|
||||
# via beautifulsoup4
|
||||
typing-extensions==4.9.0 \
|
||||
--hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \
|
||||
--hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd
|
||||
# via qrcode
|
||||
werkzeug==3.0.1 \
|
||||
--hash=sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc \
|
||||
--hash=sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10
|
||||
|
|
|
|||
|
|
@ -1,18 +1,34 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from typing import Optional
|
||||
from flask_login import UserMixin
|
||||
|
||||
from bcrypt import checkpw, hashpw, gensalt
|
||||
from flask_login import UserMixin
|
||||
from pyotp import random_base32
|
||||
from pyotp.totp import TOTP
|
||||
|
||||
|
||||
class User(UserMixin):
|
||||
def __init__(self, username: str, password: Optional[str] = None, password_hash: Optional[bytes] = None):
|
||||
def __init__(
|
||||
self,
|
||||
username: str,
|
||||
password: Optional[str] = None,
|
||||
*,
|
||||
is_two_factor_enabled: bool = False,
|
||||
password_hash: Optional[bytes] = None,
|
||||
secret_token: Optional[str] = None,
|
||||
method: str = "manual",
|
||||
):
|
||||
self.id = username
|
||||
|
||||
if not password:
|
||||
assert password_hash, "Either password or password_hash must be provided"
|
||||
|
||||
self.__password = password_hash or hashpw(password.encode("utf-8"), gensalt()) # type: ignore
|
||||
self.__password = password_hash or hashpw(password.encode("utf-8"), gensalt(rounds=13)) # type: ignore
|
||||
self.is_two_factor_enabled = is_two_factor_enabled
|
||||
self.secret_token = secret_token
|
||||
self.method = method
|
||||
self.__totp = None
|
||||
|
||||
@property
|
||||
def password_hash(self) -> bytes:
|
||||
|
|
@ -23,6 +39,14 @@ class User(UserMixin):
|
|||
"""
|
||||
return self.__password
|
||||
|
||||
def update_password(self, password: str):
|
||||
"""
|
||||
Set the password by hashing it
|
||||
|
||||
:param password: The password to be hashed
|
||||
"""
|
||||
self.__password = hashpw(password.encode("utf-8"), gensalt(rounds=13))
|
||||
|
||||
def check_password(self, password: str):
|
||||
"""
|
||||
Check if the password is correct by hashing it and comparing it to the stored hash
|
||||
|
|
@ -32,3 +56,27 @@ class User(UserMixin):
|
|||
the user is returned.
|
||||
"""
|
||||
return checkpw(password.encode("utf-8"), self.__password)
|
||||
|
||||
def get_authentication_setup_uri(self) -> str:
|
||||
if not self.__totp:
|
||||
return ""
|
||||
return self.__totp.provisioning_uri(name=self.id, issuer_name="BunkerWeb UI")
|
||||
|
||||
def refresh_totp(self):
|
||||
self.secret_token = random_base32()
|
||||
self.__totp = TOTP(self.secret_token)
|
||||
|
||||
def check_otp(self, otp: str) -> bool:
|
||||
"""
|
||||
Check if the otp is correct by comparing it to the stored secret token
|
||||
|
||||
:param otp: The otp to be checked
|
||||
:return: The otp is being checked against the secret token. If the otp is correct,
|
||||
the user is returned.
|
||||
"""
|
||||
if not self.__totp:
|
||||
return False
|
||||
return self.__totp.verify(otp, valid_window=3)
|
||||
|
||||
def __repr__(self):
|
||||
return f"User({self.id!r}, {self.__password!r}, {self.is_two_factor_enabled!r}, {self.secret_token!r}, {self.method!r})"
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,3 +1,5 @@
|
|||
import { Tabs, Popover } from "./utils/settings.js";
|
||||
|
||||
class SubmitProfile {
|
||||
constructor() {
|
||||
this.pwEl = document.querySelector("#admin_password");
|
||||
|
|
@ -41,7 +43,7 @@ class SubmitProfile {
|
|||
"focus:valid:!border-red-500",
|
||||
"active:!border-red-500",
|
||||
"active:valid:!border-red-500",
|
||||
"valid:!border-red-500",
|
||||
"valid:!border-red-500"
|
||||
);
|
||||
this.pwAlertEl.classList.add("opacity-0");
|
||||
this.pwAlertEl.setAttribute("aria-hidden", "true");
|
||||
|
|
@ -53,7 +55,7 @@ class SubmitProfile {
|
|||
"focus:valid:!border-red-500",
|
||||
"active:!border-red-500",
|
||||
"active:valid:!border-red-500",
|
||||
"valid:!border-red-500",
|
||||
"valid:!border-red-500"
|
||||
);
|
||||
this.pwAlertEl.classList.remove("opacity-0");
|
||||
this.pwAlertEl.setAttribute("aria-hidden", "false");
|
||||
|
|
@ -71,14 +73,14 @@ class PwBtn {
|
|||
const passwordContainer = e.target.closest("[data-input-group]");
|
||||
const inpEl = passwordContainer.querySelector("input");
|
||||
const invBtn = passwordContainer.querySelector(
|
||||
'[data-setting-password="invisible"]',
|
||||
'[data-setting-password="invisible"]'
|
||||
);
|
||||
const visBtn = passwordContainer.querySelector(
|
||||
'[data-setting-password="visible"]',
|
||||
'[data-setting-password="visible"]'
|
||||
);
|
||||
inpEl.setAttribute(
|
||||
"type",
|
||||
inpEl.getAttribute("type") === "password" ? "text" : "password",
|
||||
inpEl.getAttribute("type") === "password" ? "text" : "password"
|
||||
);
|
||||
|
||||
if (inpEl.getAttribute("type") === "password") {
|
||||
|
|
@ -96,3 +98,5 @@ class PwBtn {
|
|||
|
||||
const setPWBtn = new PwBtn();
|
||||
const setSubmit = new SubmitProfile();
|
||||
const setTabs = new Tabs();
|
||||
const setPopover = new Popover();
|
||||
|
|
|
|||
2
src/ui/templates/base.html
vendored
2
src/ui/templates/base.html
vendored
|
|
@ -31,7 +31,7 @@
|
|||
|
||||
<!-- info -->
|
||||
<main
|
||||
class="xl:pl-75 w-full px-2 sm:px-6 pb-0 pt-20 sm:pt-6 min-h-[80vh] h-full flex flex-col justify-between"
|
||||
class="xl:pl-75 w-full px-2 sm:px-6 pb-0 pt-20 sm:pt-6 min-h-[85vh] h-full flex flex-col justify-between"
|
||||
>
|
||||
<div
|
||||
class="max-w-[1920px] grid gap-y-4 gap-3 sm:gap-4 lg:gap-6 grid-cols-12 w-full"
|
||||
|
|
|
|||
2
src/ui/templates/head.html
vendored
2
src/ui/templates/head.html
vendored
|
|
@ -44,6 +44,6 @@
|
|||
{% elif current_endpoint == "jobs" %}
|
||||
<script type="module" src="./js/jobs.js"></script>
|
||||
{% elif current_endpoint == "profile" %}
|
||||
<script defer src="./js/profile.js"></script>
|
||||
<script type="module" src="./js/profile.js"></script>
|
||||
{% endif %}
|
||||
</head>
|
||||
|
|
|
|||
3
src/ui/templates/login.html
vendored
3
src/ui/templates/login.html
vendored
|
|
@ -102,7 +102,7 @@
|
|||
</h5>
|
||||
<label class="sr-only" for="username">username</label>
|
||||
<input
|
||||
type="username"
|
||||
type="text"
|
||||
id="username"
|
||||
name="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"
|
||||
|
|
@ -131,6 +131,7 @@
|
|||
/>
|
||||
</div>
|
||||
<!-- end password inpt-->
|
||||
<!-- totp -->
|
||||
<div class="flex justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
|
|
|
|||
504
src/ui/templates/profile.html
vendored
504
src/ui/templates/profile.html
vendored
|
|
@ -1,46 +1,146 @@
|
|||
{% extends "base.html" %} {% block content %} {% set current_endpoint =
|
||||
url_for(request.endpoint)[1:].split("/")[-1].strip() %}
|
||||
|
||||
<div
|
||||
class="w-full overflow-hidden overflow-y-auto overflow-x-auto max-h-100 sm:max-h-125 col-span-12 p-4 relative break-words bg-white shadow-xl dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border"
|
||||
>
|
||||
<h5 class="my-2 font-bold dark:text-white/90 mx-2">PROFILE</h5>
|
||||
<div class="grid grid-cols-12 justify-items-center w-full">
|
||||
<div
|
||||
class="flex flex-col relative col-span-12 md:col-span-6 lg:col-span-4 px-4 my-2 md:px-6 md:my-3 md:col-span-6 lg:px-6 lg:my-3 max-w-[400px] w-full"
|
||||
>
|
||||
<h5
|
||||
class="text-base my-1 transition duration-300 ease-in-out text-md font-bold m-0"
|
||||
>
|
||||
Username
|
||||
</h5>
|
||||
<label class="sr-only" for="profile_username">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
id="profile_username"
|
||||
name="profile_username"
|
||||
class="col-span-12 regular-input"
|
||||
placeholder="enter username"
|
||||
value="{{ username }}"
|
||||
disabled
|
||||
pattern="(.*?)"
|
||||
maxlength="256"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="w-full overflow-hidden overflow-y-auto overflow-x-auto max-h-100 sm:max-h-125 col-span-12 p-4 relative break-words bg-white shadow-xl dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border"
|
||||
data-service-content
|
||||
class="md:max-w-[600px] first-letter:w-full overflow-hidden overflow-y-auto overflow-x-auto col-span-12 p-4 relative break-words bg-white shadow-xl dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border"
|
||||
>
|
||||
<h5 class="my-2 font-bold dark:text-white/90 mx-2">EDIT PROFILE</h5>
|
||||
<h5 class="my-2 font-bold dark:text-white/90 mx-2">SETTINGS</h5>
|
||||
<!-- desktop tabs -->
|
||||
<div
|
||||
role="tablist"
|
||||
data-{{current_endpoint}}-tabs-desktop
|
||||
class="hidden md:block col-span-12 mb-4"
|
||||
>
|
||||
<!-- tabs -->
|
||||
<button
|
||||
role="tab"
|
||||
data-tab-handler="profile"
|
||||
class="active settings-tabs-tab-btn"
|
||||
>
|
||||
<span class="w-full flex justify-between items-center">
|
||||
<!-- text and icon -->
|
||||
<span class="settings-tabs-name"> User </span>
|
||||
<svg
|
||||
data-popover-btn="profile"
|
||||
class="fill-blue-500 h-5 w-5 mr-2 hover:brightness-95"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path
|
||||
d="M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0S0 114.6 0 256S114.6 512 256 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32s32 14.3 32 32s-14.3 32-32 32z"
|
||||
/>
|
||||
</svg>
|
||||
<!-- end text and icon -->
|
||||
<!-- popover -->
|
||||
<span
|
||||
data-popover-content="profile"
|
||||
class="settings-tabs-popover-container hidden"
|
||||
>
|
||||
<span class="settings-tabs-popover-text"
|
||||
>Update profile data (username, password...)</span
|
||||
>
|
||||
</span>
|
||||
<!-- end popover -->
|
||||
</span>
|
||||
</button>
|
||||
<button role="tab" data-tab-handler="totp" class="settings-tabs-tab-btn">
|
||||
<span class="w-full flex justify-between items-center">
|
||||
<!-- text and icon -->
|
||||
<span class="settings-tabs-name"> TOTP </span>
|
||||
<svg
|
||||
data-popover-btn="totp"
|
||||
class="fill-blue-500 h-5 w-5 mr-2 hover:brightness-95"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path
|
||||
d="M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0S0 114.6 0 256S114.6 512 256 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-144c-17.7 0-32-14.3-32-32s14.3-32 32-32s32 14.3 32 32s-14.3 32-32 32z"
|
||||
/>
|
||||
</svg>
|
||||
<!-- end text and icon -->
|
||||
<!-- popover -->
|
||||
<span
|
||||
data-popover-content="totp"
|
||||
class="settings-tabs-popover-container hidden"
|
||||
>
|
||||
<span class="settings-tabs-popover-text"
|
||||
>Enabled / Disabled TOTP
|
||||
</span>
|
||||
</span>
|
||||
<!-- end popover -->
|
||||
</span>
|
||||
</button>
|
||||
<!--end tabs-->
|
||||
</div>
|
||||
<!-- end desktop tabs -->
|
||||
<!-- mobile tabs -->
|
||||
<div class="md:hidden relative col-span-12 mb-4 mt-2 mx-2">
|
||||
<button
|
||||
data-tab-dropdown-btn
|
||||
aria-controls="tab-dropdown-mobile"
|
||||
class="settings-tabs-mobile-btn"
|
||||
>
|
||||
<span aria-description="current tab" class="settings-tabs-mobile-btn-text"
|
||||
>Profile
|
||||
</span>
|
||||
<!-- chevron -->
|
||||
<svg
|
||||
class="transition-transform h-4 w-4 fill-primary dark:fill-gray-300"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path
|
||||
d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"
|
||||
/>
|
||||
</svg>
|
||||
<!-- end chevron -->
|
||||
</button>
|
||||
<!-- dropdown-->
|
||||
<div
|
||||
id="tab-dropdown-mobile"
|
||||
role="listbox"
|
||||
data-tab-dropdown
|
||||
class="hidden z-100 absolute flex-col w-full overflow-hidden overflow-y-auto max-h-90"
|
||||
>
|
||||
<button
|
||||
role="option"
|
||||
data-tab-handler-mobile="profile"
|
||||
data-select="false"
|
||||
id="edit-{{current_endpoint}}-profile-tab"
|
||||
class="active first settings-tabs-mobile-dropdown-btn"
|
||||
>
|
||||
Profile
|
||||
</button>
|
||||
<button
|
||||
role="option"
|
||||
data-tab-handler-mobile="totp"
|
||||
data-select="false"
|
||||
id="edit-{{current_endpoint}}-totp-tab"
|
||||
class="settings-tabs-mobile-dropdown-btn rounded-b"
|
||||
>
|
||||
TOTP
|
||||
</button>
|
||||
</div>
|
||||
<!-- end dropdown-->
|
||||
</div>
|
||||
<!-- end mobile tabs -->
|
||||
<form
|
||||
data-plugin-item="profile"
|
||||
class="grid grid-cols-12 w-full justify-items-center"
|
||||
id="profile-form"
|
||||
action="profile"
|
||||
method="POST"
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="col-span-12">
|
||||
<h5
|
||||
class="text-xl my-1 transition duration-300 ease-in-out text-md font-bold m-0"
|
||||
>
|
||||
PROFILE
|
||||
</h5>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input
|
||||
type="hidden"
|
||||
|
|
@ -49,89 +149,89 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
|
|||
/>
|
||||
<!-- username inpt-->
|
||||
<div
|
||||
class="flex flex-col relative col-span-12 md:col-span-6 lg:col-span-4 px-4 my-2 md:px-6 md:my-3 md:col-span-6 lg:px-6 lg:my-3 max-w-[400px] w-full"
|
||||
class="flex flex-col relative col-span-12 px-4 my-2 md:px-6 md:my-3 lg:px-6 lg:my-3 max-w-[400px] w-full"
|
||||
>
|
||||
<h5
|
||||
class="text-base my-1 transition duration-300 ease-in-out text-md font-bold m-0"
|
||||
>
|
||||
New Username
|
||||
Username
|
||||
</h5>
|
||||
<label class="sr-only" for="admin_username">New Username</label>
|
||||
<label class="sr-only" for="admin_username">New username</label>
|
||||
<input
|
||||
type="text"
|
||||
id="admin_username"
|
||||
name="admin_username"
|
||||
class="col-span-12 regular-input"
|
||||
placeholder="enter username"
|
||||
value="{{ username }}"
|
||||
pattern="(.*?)"
|
||||
maxlength="256"
|
||||
required
|
||||
value="{{ username }}"
|
||||
/>
|
||||
</div>
|
||||
<!-- end username inpt-->
|
||||
<!-- password inpt-->
|
||||
<div
|
||||
data-input-group
|
||||
class="flex flex-col relative col-span-12 md:col-span-6 lg:col-span-4 px-4 my-2 md:px-6 md:my-3 md:col-span-6 lg:px-6 lg:my-3 max-w-[400px] w-full"
|
||||
>
|
||||
<h5
|
||||
class="text-base my-1 transition duration-300 ease-in-out text-md font-bold m-0"
|
||||
>
|
||||
Current password
|
||||
</h5>
|
||||
<label class="sr-only" for="curr_password">Current password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="curr_password"
|
||||
name="curr_password"
|
||||
class="col-span-12 regular-input"
|
||||
placeholder="enter password"
|
||||
value=""
|
||||
pattern="^(?=.*?\d)(?=.*?[ !\u0022#$%&'\(\)*+,.\/:;<=>?@\[\\\]^_`\u007B\u007C\u007D\u007E\u002D]).{8,}$"
|
||||
minlength="8"
|
||||
required
|
||||
/>
|
||||
<div
|
||||
data-setting-password-container
|
||||
class="absolute flex right-8 h-5 w-5 top-[60%] lg:top-11"
|
||||
>
|
||||
<button
|
||||
data-setting-password="visible"
|
||||
class="h-5 w-5 flex items-center align-middle dark:fill-blue-500 hover:brightness-75 transition-all"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="fill-primary pointer-events-none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 576 512"
|
||||
>
|
||||
<path
|
||||
d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM432 256c0 79.5-64.5 144-144 144s-144-64.5-144-144s64.5-144 144-144s144 64.5 144 144zM288 192c0 35.3-28.7 64-64 64c-11.5 0-22.3-3-31.6-8.4c-.2 2.8-.4 5.5-.4 8.4c0 53 43 96 96 96s96-43 96-96s-43-96-96-96c-2.8 0-5.6 .1-8.4 .4c5.3 9.3 8.4 20.1 8.4 31.6z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
data-setting-password="invisible"
|
||||
class="hidden -translate-y-0.2 scale-110 h-5 w-5 items-center align-middle"
|
||||
>
|
||||
<svg
|
||||
class="fill-primary pointer-events-none dark:fill-blue-500 hover:brightness-75 transition-all"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 640 512"
|
||||
>
|
||||
<path
|
||||
d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zM223.1 149.5C248.6 126.2 282.7 112 320 112c79.5 0 144 64.5 144 144c0 24.9-6.3 48.3-17.4 68.7L408 294.5c5.2-11.8 8-24.8 8-38.5c0-53-43-96-96-96c-2.8 0-5.6 .1-8.4 .4c5.3 9.3 8.4 20.1 8.4 31.6c0 10.2-2.4 19.8-6.6 28.3l-90.3-70.8zm223.1 298L373 389.9c-16.4 6.5-34.3 10.1-53 10.1c-79.5 0-144-64.5-144-144c0-6.9 .5-13.6 1.4-20.2L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- end password inpt-->
|
||||
<!-- password inpt-->
|
||||
<div
|
||||
data-input-group
|
||||
class="flex flex-col relative col-span-12 md:col-span-6 lg:col-span-4 px-4 my-2 md:px-6 md:my-3 md:col-span-6 lg:px-6 lg:my-3 max-w-[400px] w-full"
|
||||
class="flex flex-col relative col-span-12 px-4 my-2 md:px-6 md:my-3 lg:px-6 lg:my-3 max-w-[400px] w-full"
|
||||
>
|
||||
<h5
|
||||
class="text-base my-1 transition duration-300 ease-in-out text-md font-bold m-0"
|
||||
>
|
||||
Current password
|
||||
</h5>
|
||||
<label class="sr-only" for="curr_password">Current password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="curr_password"
|
||||
name="curr_password"
|
||||
class="col-span-12 regular-input"
|
||||
placeholder="enter current password"
|
||||
value=""
|
||||
pattern="^(?=.*?\d)(?=.*?[ !\u0022#$%&'\(\)*+,.\/:;<=>?@\[\\\]^_`\u007B\u007C\u007D\u007E\u002D]).{8,}$"
|
||||
minlength="8"
|
||||
required
|
||||
/>
|
||||
<div
|
||||
data-setting-password-container
|
||||
class="absolute flex right-8 h-5 w-5 top-[60%] lg:top-11"
|
||||
>
|
||||
<button
|
||||
data-setting-password="visible"
|
||||
class="h-5 w-5 flex items-center align-middle dark:fill-blue-500 hover:brightness-75 transition-all"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="fill-primary pointer-events-none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 576 512"
|
||||
>
|
||||
<path
|
||||
d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM432 256c0 79.5-64.5 144-144 144s-144-64.5-144-144s64.5-144 144-144s144 64.5 144 144zM288 192c0 35.3-28.7 64-64 64c-11.5 0-22.3-3-31.6-8.4c-.2 2.8-.4 5.5-.4 8.4c0 53 43 96 96 96s96-43 96-96s-43-96-96-96c-2.8 0-5.6 .1-8.4 .4c5.3 9.3 8.4 20.1 8.4 31.6z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
data-setting-password="invisible"
|
||||
class="hidden -translate-y-0.2 scale-110 h-5 w-5 items-center align-middle"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="fill-primary pointer-events-none dark:fill-blue-500 hover:brightness-75 transition-all"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 640 512"
|
||||
>
|
||||
<path
|
||||
d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zM223.1 149.5C248.6 126.2 282.7 112 320 112c79.5 0 144 64.5 144 144c0 24.9-6.3 48.3-17.4 68.7L408 294.5c5.2-11.8 8-24.8 8-38.5c0-53-43-96-96-96c-2.8 0-5.6 .1-8.4 .4c5.3 9.3 8.4 20.1 8.4 31.6c0 10.2-2.4 19.8-6.6 28.3l-90.3-70.8zm223.1 298L373 389.9c-16.4 6.5-34.3 10.1-53 10.1c-79.5 0-144-64.5-144-144c0-6.9 .5-13.6 1.4-20.2L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- end password inpt-->
|
||||
<!-- password inpt-->
|
||||
<div
|
||||
data-input-group
|
||||
class="flex flex-col relative col-span-12 px-4 my-2 md:px-6 md:my-3 lg:px-6 lg:my-3 max-w-[400px] w-full"
|
||||
>
|
||||
<h5
|
||||
class="text-base my-1 transition duration-300 ease-in-out text-md font-bold m-0"
|
||||
|
|
@ -144,15 +244,14 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
|
|||
id="admin_password"
|
||||
name="admin_password"
|
||||
class="col-span-12 regular-input"
|
||||
placeholder="enter password"
|
||||
placeholder="enter new password"
|
||||
value=""
|
||||
pattern="^(?=.*?\d)(?=.*?[ !\u0022#$%&'\(\)*+,.\/:;<=>?@\[\\\]^_`\u007B\u007C\u007D\u007E\u002D]).{8,}$"
|
||||
minlength="8"
|
||||
required
|
||||
/>
|
||||
<div
|
||||
data-setting-password-container
|
||||
class="absolute flex right-8 h-5 w-5 top-[60%] md:top-[45%] lg:top-11"
|
||||
class="absolute flex right-8 h-5 w-5 top-[60%] lg:top-11"
|
||||
>
|
||||
<button
|
||||
data-setting-password="visible"
|
||||
|
|
@ -172,6 +271,7 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
|
|||
<button
|
||||
data-setting-password="invisible"
|
||||
class="hidden -translate-y-0.2 scale-110 h-5 w-5 items-center align-middle"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="fill-primary pointer-events-none dark:fill-blue-500 hover:brightness-75 transition-all"
|
||||
|
|
@ -189,7 +289,7 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
|
|||
<!-- password inpt-->
|
||||
<div
|
||||
data-input-group
|
||||
class="flex flex-col relative col-span-12 md:col-span-6 lg:col-span-4 px-4 my-2 md:px-6 md:my-3 md:col-span-6 lg:px-6 lg:my-3 max-w-[400px] w-full"
|
||||
class="flex flex-col relative col-span-12 px-4 my-2 md:px-6 md:my-3 lg:px-6 lg:my-3 max-w-[400px] w-full"
|
||||
>
|
||||
<h5
|
||||
class="text-base my-1 transition duration-300 ease-in-out text-md font-bold m-0"
|
||||
|
|
@ -204,11 +304,10 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
|
|||
id="admin_password_check"
|
||||
name="admin_password_check"
|
||||
class="col-span-12 regular-input"
|
||||
placeholder="confirm password"
|
||||
placeholder="confirm new password"
|
||||
value=""
|
||||
pattern="^(?=.*?\d)(?=.*?[ !\u0022#$%&'\(\)*+,.\/:;<=>?@\[\\\]^_`\u007B\u007C\u007D\u007E\u002D]).{8,}$"
|
||||
minlength="8"
|
||||
required
|
||||
/>
|
||||
<div
|
||||
data-setting-password-container
|
||||
|
|
@ -232,6 +331,7 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
|
|||
<button
|
||||
data-setting-password="invisible"
|
||||
class="hidden -translate-y-0.2 scale-110 h-5 w-5 items-center align-middle"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="fill-primary pointer-events-none dark:fill-blue-500 hover:brightness-75 transition-all"
|
||||
|
|
@ -255,11 +355,211 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
|
|||
id="profile-button"
|
||||
name="profile-button"
|
||||
value="profile"
|
||||
class="valid-btn"
|
||||
class="edit-btn"
|
||||
>
|
||||
Save
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<form
|
||||
data-plugin-item="totp"
|
||||
class="hidden grid grid-cols-12 w-full justify-items-center"
|
||||
id="profile-form"
|
||||
action="profile"
|
||||
method="POST"
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="col-span-12">
|
||||
<h5
|
||||
class="text-xl my-1 transition duration-300 ease-in-out text-md font-bold m-0"
|
||||
>
|
||||
TOTP
|
||||
</h5>
|
||||
</div>
|
||||
<h5
|
||||
class="{% if not is_totp %} text-red-500 {% else %} text-green-500 {% endif %} uppercase col-span-12 text-sm my-2 font-bold dark:text-white/90 mx-2"
|
||||
>
|
||||
{% if not is_totp %} TOTP is currently off {% else %} TOTP is currently on
|
||||
{% endif %}
|
||||
</h5>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input
|
||||
type="hidden"
|
||||
name="next"
|
||||
value="{{ request.values.get('next', '') }}"
|
||||
/>
|
||||
{% if not is_totp %}
|
||||
<!-- qr secret -->
|
||||
<div
|
||||
data-input-group
|
||||
class="flex flex-col relative col-span-12 px-4 my-2 md:px-6 md:my-3 lg:px-6 lg:my-3 max-w-[400px] w-full"
|
||||
>
|
||||
<h5
|
||||
class="text-base my-1 transition duration-300 ease-in-out text-md font-bold m-0"
|
||||
>
|
||||
2FA QR CODE
|
||||
</h5>
|
||||
<div class="flex justify-center">
|
||||
<img
|
||||
src="data:image/png;base64, {{ totp_qr_image }}"
|
||||
alt="Secret Token"
|
||||
style="width: 200px; height: 200px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- end qr secret -->
|
||||
<!-- secret -->
|
||||
<div
|
||||
data-input-group
|
||||
class="flex flex-col relative col-span-12 px-4 my-2 md:px-6 md:my-3 lg:px-6 lg:my-3 max-w-[400px] w-full"
|
||||
>
|
||||
<h5
|
||||
class="text-base my-1 transition duration-300 ease-in-out text-md font-bold m-0"
|
||||
>
|
||||
Secret token
|
||||
</h5>
|
||||
<label class="sr-only" for="secret_token">secret token</label>
|
||||
<input
|
||||
type="password"
|
||||
id="secret_token"
|
||||
name="secret_token"
|
||||
class="col-span-12 regular-input"
|
||||
placeholder="secret token"
|
||||
value="{{ secret_token }}"
|
||||
readonly
|
||||
/>
|
||||
<div
|
||||
data-setting-password-container
|
||||
class="absolute flex right-8 h-5 w-5 top-[60%] lg:top-11"
|
||||
>
|
||||
<button
|
||||
data-setting-password="visible"
|
||||
class="h-5 w-5 flex items-center align-middle"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="fill-primary pointer-events-none dark:fill-blue-500 hover:brightness-75 transition-all"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 576 512"
|
||||
>
|
||||
<path
|
||||
d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM432 256c0 79.5-64.5 144-144 144s-144-64.5-144-144s64.5-144 144-144s144 64.5 144 144zM288 192c0 35.3-28.7 64-64 64c-11.5 0-22.3-3-31.6-8.4c-.2 2.8-.4 5.5-.4 8.4c0 53 43 96 96 96s96-43 96-96s-43-96-96-96c-2.8 0-5.6 .1-8.4 .4c5.3 9.3 8.4 20.1 8.4 31.6z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
data-setting-password="invisible"
|
||||
class="hidden -translate-y-0.2 scale-110 h-5 w-5 items-center align-middle"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="fill-primary pointer-events-none dark:fill-blue-500 hover:brightness-75 transition-all"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 640 512"
|
||||
>
|
||||
<path
|
||||
d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zM223.1 149.5C248.6 126.2 282.7 112 320 112c79.5 0 144 64.5 144 144c0 24.9-6.3 48.3-17.4 68.7L408 294.5c5.2-11.8 8-24.8 8-38.5c0-53-43-96-96-96c-2.8 0-5.6 .1-8.4 .4c5.3 9.3 8.4 20.1 8.4 31.6c0 10.2-2.4 19.8-6.6 28.3l-90.3-70.8zm223.1 298L373 389.9c-16.4 6.5-34.3 10.1-53 10.1c-79.5 0-144-64.5-144-144c0-6.9 .5-13.6 1.4-20.2L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- end secret -->
|
||||
{% endif %} {% if is_totp or not is_totp %}
|
||||
<!-- username inpt-->
|
||||
<div
|
||||
class="flex flex-col relative col-span-12 px-4 my-2 md:px-6 md:my-3 lg:px-6 lg:my-3 max-w-[400px] w-full"
|
||||
>
|
||||
<h5
|
||||
class="text-base my-1 transition duration-300 ease-in-out text-md font-bold m-0"
|
||||
>
|
||||
2FA code
|
||||
</h5>
|
||||
<label class="sr-only" for="totp_token">totp code</label>
|
||||
<input
|
||||
type="text"
|
||||
id="totp_token"
|
||||
name="totp_token"
|
||||
class="col-span-12 regular-input"
|
||||
placeholder="enter code"
|
||||
pattern="(.*?)"
|
||||
maxlength="256"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<!-- password inpt-->
|
||||
{% endif %}
|
||||
<div
|
||||
data-input-group
|
||||
class="flex flex-col relative col-span-12 px-4 my-2 md:px-6 md:my-3 lg:px-6 lg:my-3 max-w-[400px] w-full"
|
||||
>
|
||||
<h5
|
||||
class="text-base my-1 transition duration-300 ease-in-out text-md font-bold m-0"
|
||||
>
|
||||
Current password
|
||||
</h5>
|
||||
<label class="sr-only" for="totp_password">Current password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="totp_password"
|
||||
name="totp_password"
|
||||
class="col-span-12 regular-input"
|
||||
placeholder="enter password"
|
||||
value=""
|
||||
pattern="^(?=.*?\d)(?=.*?[ !\u0022#$%&'\(\)*+,.\/:;<=>?@\[\\\]^_`\u007B\u007C\u007D\u007E\u002D]).{8,}$"
|
||||
minlength="8"
|
||||
required
|
||||
/>
|
||||
<div
|
||||
data-setting-password-container
|
||||
class="absolute flex right-8 h-5 w-5 top-[60%] lg:top-11"
|
||||
>
|
||||
<button
|
||||
data-setting-password="visible"
|
||||
class="h-5 w-5 flex items-center align-middle dark:fill-blue-500 hover:brightness-75 transition-all"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="fill-primary pointer-events-none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 576 512"
|
||||
>
|
||||
<path
|
||||
d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM432 256c0 79.5-64.5 144-144 144s-144-64.5-144-144s64.5-144 144-144s144 64.5 144 144zM288 192c0 35.3-28.7 64-64 64c-11.5 0-22.3-3-31.6-8.4c-.2 2.8-.4 5.5-.4 8.4c0 53 43 96 96 96s96-43 96-96s-43-96-96-96c-2.8 0-5.6 .1-8.4 .4c5.3 9.3 8.4 20.1 8.4 31.6z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
data-setting-password="invisible"
|
||||
class="hidden -translate-y-0.2 scale-110 h-5 w-5 items-center align-middle"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="fill-primary pointer-events-none dark:fill-blue-500 hover:brightness-75 transition-all"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 640 512"
|
||||
>
|
||||
<path
|
||||
d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zM223.1 149.5C248.6 126.2 282.7 112 320 112c79.5 0 144 64.5 144 144c0 24.9-6.3 48.3-17.4 68.7L408 294.5c5.2-11.8 8-24.8 8-38.5c0-53-43-96-96-96c-2.8 0-5.6 .1-8.4 .4c5.3 9.3 8.4 20.1 8.4 31.6c0 10.2-2.4 19.8-6.6 28.3l-90.3-70.8zm223.1 298L373 389.9c-16.4 6.5-34.3 10.1-53 10.1c-79.5 0-144-64.5-144-144c0-6.9 .5-13.6 1.4-20.2L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- end password inpt-->
|
||||
|
||||
<div class="col-span-12 flex justify-center mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
id="profile-button"
|
||||
name="profile-button"
|
||||
value="profile"
|
||||
class="{% if not is_totp %}valid-btn{% else %}delete-btn{% endif %}"
|
||||
>
|
||||
{% if not is_totp %} enable totp {% else %} disable totp {% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
2
src/ui/templates/settings_plugins.html
vendored
2
src/ui/templates/settings_plugins.html
vendored
|
|
@ -290,7 +290,7 @@ data-plugin-item="{{plugin['id']}}"
|
|||
|
||||
{% if value['type'] == "password" %}
|
||||
<div data-setting-password-container class="absolute flex right-2 h-5 w-5">
|
||||
<button type="button"data- setting-password="visible" class="h-5 w-5 flex items-center align-middle" type="button">
|
||||
<button type="button" data-setting-password="visible" class="h-5 w-5 flex items-center align-middle" type="button">
|
||||
<svg class="fill-primary pointer-events-none dark:fill-blue-500 hover:brightness-75 transition-all" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM432 256c0 79.5-64.5 144-144 144s-144-64.5-144-144s64.5-144 144-144s144 64.5 144 144zM288 192c0 35.3-28.7 64-64 64c-11.5 0-22.3-3-31.6-8.4c-.2 2.8-.4 5.5-.4 8.4c0 53 43 96 96 96s96-43 96-96s-43-96-96-96c-2.8 0-5.6 .1-8.4 .4c5.3 9.3 8.4 20.1 8.4 31.6z"/></svg>
|
||||
</button>
|
||||
<button type="button" data-setting-password="invisible" class="hidden -translate-y-0.2 scale-110 h-5 w-5 items-center align-middle">
|
||||
|
|
|
|||
2
src/ui/templates/setup.html
vendored
2
src/ui/templates/setup.html
vendored
|
|
@ -264,7 +264,7 @@
|
|||
class="col-span-12 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="/admin"
|
||||
value="{{ random_url }}"
|
||||
pattern="\/(.*[a-z])"
|
||||
pattern="\/[a-zA-Z0-9-]{1,255}$"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
222
src/ui/templates/totp.html
vendored
Normal file
222
src/ui/templates/totp.html
vendored
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
<!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 | 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" />
|
||||
</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
|
||||
role="alert"
|
||||
aria-description="login message alert"
|
||||
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"
|
||||
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' or (message|safe).startswith("Please log in") %}
|
||||
<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 col-span-2 bg-none h-full flex flex-col items-center justify-center"
|
||||
>
|
||||
<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="max-w-60 max-h-30 mb-6"
|
||||
src="images/BUNKERWEB-print-hd.png"
|
||||
alt="logo"
|
||||
class="logo"
|
||||
/>
|
||||
</div>
|
||||
<h1 class="hidden text-center font-bold dark:text-white mb-8">
|
||||
Log in
|
||||
</h1>
|
||||
<form action="totp" method="POST" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input
|
||||
type="hidden"
|
||||
name="next"
|
||||
value="{{ request.values.get('next', '') }}"
|
||||
/>
|
||||
<!-- totp -->
|
||||
<div class="flex flex-col relative col-span-12 my-3">
|
||||
<h5
|
||||
class="my-1 transition duration-300 ease-in-out dark:opacity-90 text-md font-bold m-0 dark:text-gray-300"
|
||||
>
|
||||
2FA
|
||||
</h5>
|
||||
<label class="sr-only" for="totp_token">code totp</label>
|
||||
<input
|
||||
type="text"
|
||||
id="totp_token"
|
||||
name="totp_token"
|
||||
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 totp"
|
||||
pattern="(.*?)"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<!-- end totp-->
|
||||
<div class="flex justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
id="login"
|
||||
name="login"
|
||||
value="login"
|
||||
class="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 tracking-tight-rem shadow-xs bg-150 bg-x-25 hover:-translate-y-px active:opacity-85 hover:shadow-md"
|
||||
>
|
||||
Log in
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- end form -->
|
||||
<!-- particles -->
|
||||
<div class="-z-10 fixed bg-primary">
|
||||
<div id="particles-js" class="login-img [&>*]:bg-primary"></div>
|
||||
<div class="hidden lg:flex justify-center">
|
||||
<img
|
||||
class="max-w-60 max-h-30"
|
||||
src="images/BUNKERWEB-print-hd-blanc.png"
|
||||
alt="logo"
|
||||
class="images login-logo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script src="js/tsparticles.bundle.min.js"></script>
|
||||
<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();
|
||||
tsParticles.loadJSON("particles-js", "json/particles.json");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from base64 import b64encode
|
||||
from io import BytesIO
|
||||
from os.path import join
|
||||
from typing import List, Optional
|
||||
|
||||
from qrcode.main import QRCode
|
||||
|
||||
|
||||
def path_to_dict(
|
||||
path: str,
|
||||
|
|
@ -130,3 +134,13 @@ def path_to_dict(
|
|||
|
||||
def check_settings(settings: dict, check: str) -> bool:
|
||||
return any(setting["context"] == check for setting in settings.values())
|
||||
|
||||
|
||||
def get_b64encoded_qr_image(data: str):
|
||||
qr = QRCode(version=1, box_size=10, border=5)
|
||||
qr.add_data(data)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill_color="#0b5577", back_color="white")
|
||||
buffered = BytesIO()
|
||||
img.save(buffered)
|
||||
return b64encode(buffered.getvalue()).decode("utf-8")
|
||||
|
|
|
|||
Loading…
Reference in a new issue