chore: Update web UI setup wizard to handle when a reverse proxy already exists but no UI user were configured

This commit is contained in:
Théophile Diot 2024-07-05 11:50:16 +01:00
parent d665661012
commit 15fba7e026
No known key found for this signature in database
GPG key ID: FA995104A0BA376A
4 changed files with 229 additions and 183 deletions

View file

@ -38,6 +38,17 @@ location /setup/check {
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
default_type 'text/plain';
content_by_lua_block {
-- Override CSP header
ngx.header["Content-Security-Policy"] = "default-src 'none'; img-src 'self'; require-trusted-types-for 'script';"
-- Remove server header
ngx.header["Server"] = nil
-- Override HSTS header
if ngx.var.scheme == "https" then
ngx.header["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload"
end
local logger = require "bunkerweb.logger":new("UI")
local args, err = ngx.req.get_uri_args(1)
if err == "truncated" or not args["server_name"] or args["server_name"] == "" then

View file

@ -213,7 +213,7 @@ def manage_bunkerweb(method: str, *args, operation: str = "reloads", is_draft: b
elif operation == "delete":
operation, error = app.config["CONFIG"].delete_service(args[2], check_changes=(was_draft != is_draft or not is_draft))
elif method == "global_config":
operation, error = app.config["CONFIG"].edit_global_conf(args[0])
operation, error = app.config["CONFIG"].edit_global_conf(args[0], check_changes=True)
if operation == "reload":
operation = app.config["INSTANCES"].reload_instance(args[0])
@ -544,23 +544,29 @@ def check():
@app.route("/setup", methods=["GET", "POST"])
def setup():
db_config = app.config["CONFIG"].get_config(methods=False, filtered_settings=("SERVER_NAME", "USE_UI", "UI_HOST"))
for server_name in db_config["SERVER_NAME"].split(" "):
if db_config.get(f"{server_name}_USE_UI", "no") == "yes":
return redirect(url_for("login"), 301)
db_config = app.config["CONFIG"].get_config(methods=False, filtered_settings=("SERVER_NAME", "MULTISITE", "USE_UI", "UI_HOST", "AUTO_LETS_ENCRYPT"))
admin_user = app.config["DB"].get_ui_user()
if admin_user:
admin_user = User(**admin_user)
ui_reverse_proxy = False
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":
if admin_user:
return redirect(url_for("login"), 301)
ui_reverse_proxy = True
break
if request.method == "POST":
if app.config["DB"].readonly:
return redirect_flash_error("Database is in read-only mode", "setup")
is_request_form("setup")
required_keys = ["server_name", "ui_host", "ui_url"]
required_keys = []
if not ui_reverse_proxy:
required_keys.extend(["server_name", "ui_host", "ui_url"])
if not admin_user:
required_keys.extend(["admin_username", "admin_password", "admin_password_check"])
@ -580,18 +586,6 @@ def setup():
"setup",
)
server_names = db_config["SERVER_NAME"].split(" ")
if request.form["server_name"] in server_names:
return redirect_flash_error(f"The hostname {request.form['server_name']} is already in use.", "setup")
else:
for server_name in server_names:
if request.form["server_name"] in db_config.get(f"{server_name}_SERVER_NAME", "").split(" "):
return redirect_flash_error(f"The hostname {request.form['server_name']} is already in use.", "setup")
if not REVERSE_PROXY_PATH.match(request.form["ui_host"]):
return redirect_flash_error("The hostname is not valid.", "setup")
if not admin_user:
admin_user = User(request.form["admin_username"], request.form["admin_password"], method="ui")
ret = app.config["DB"].create_ui_user(request.form["admin_username"], admin_user.password_hash, method="ui")
@ -600,44 +594,62 @@ def setup():
flash("The admin user was created successfully", "success")
ui_data = get_ui_data()
ui_data["RELOADING"] = True
ui_data["LAST_RELOAD"] = time()
if not ui_reverse_proxy:
server_names = db_config["SERVER_NAME"].split(" ")
if request.form["server_name"] in server_names:
return redirect_flash_error(f"The hostname {request.form['server_name']} is already in use.", "setup")
else:
for server_name in server_names:
if request.form["server_name"] in db_config.get(f"{server_name}_SERVER_NAME", "").split(" "):
return redirect_flash_error(f"The hostname {request.form['server_name']} is already in use.", "setup")
config = {
"SERVER_NAME": request.form["server_name"],
"USE_UI": "yes",
"USE_REVERSE_PROXY": "yes",
"REVERSE_PROXY_HOST": request.form["ui_host"],
"REVERSE_PROXY_URL": request.form["ui_url"] or "/",
"INTERCEPTED_ERROR_CODES": "400 404 405 413 429 500 501 502 503 504",
"MAX_CLIENT_SIZE": "50m",
}
if not REVERSE_PROXY_PATH.match(request.form["ui_host"]):
return redirect_flash_error("The hostname is not valid.", "setup")
if request.form.get("auto_lets_encrypt", "no") == "yes":
config["AUTO_LETS_ENCRYPT"] = "yes"
else:
config["GENERATE_SELF_SIGNED_SSL"] = "yes"
ui_data = get_ui_data()
ui_data["RELOADING"] = True
ui_data["LAST_RELOAD"] = time()
# deepcode ignore MissingAPI: We don't need to check to wait for the thread to finish
Thread(
target=manage_bunkerweb,
name="Reloading instances",
args=("services", config, request.form["server_name"], request.form["server_name"]),
kwargs={"operation": "new", "threaded": True},
).start()
config = {
"SERVER_NAME": request.form["server_name"],
"USE_UI": "yes",
"USE_REVERSE_PROXY": "yes",
"REVERSE_PROXY_HOST": request.form["ui_host"],
"REVERSE_PROXY_URL": request.form["ui_url"] or "/",
"INTERCEPTED_ERROR_CODES": "400 404 405 413 429 500 501 502 503 504",
"MAX_CLIENT_SIZE": "50m",
}
with LOCK:
TMP_DATA_FILE.write_text(dumps(ui_data), encoding="utf-8")
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']}/"
if not config.get("MULTISITE", "no") == "yes":
app.config["CONFIG"].edit_global_conf({"MULTISITE": "yes"}, check_changes=False)
# deepcode ignore MissingAPI: We don't need to check to wait for the thread to finish
Thread(
target=manage_bunkerweb,
name="Reloading instances",
args=("services", config, request.form["server_name"], request.form["server_name"]),
kwargs={"operation": "new", "threaded": True},
).start()
with LOCK:
TMP_DATA_FILE.write_text(dumps(ui_data), encoding="utf-8")
return Response(status=200)
return render_template(
"setup.html",
ui_user=admin_user,
ui_reverse_proxy=ui_reverse_proxy,
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",
random_url=f"/{''.join(choice(ascii_letters + digits) for _ in range(10))}",
)

View file

@ -226,7 +226,7 @@ class Config:
return ret, 1
return f"Configuration for {old_server_name_splitted[0]} has been edited.", 0
def edit_global_conf(self, variables: dict) -> Tuple[str, int]:
def edit_global_conf(self, variables: dict, *, check_changes: bool = True) -> Tuple[str, int]:
"""Edits the global conf
Parameters
@ -239,7 +239,7 @@ class Config:
str
the confirmation message
"""
ret = self.__gen_conf(self.get_config(methods=False) | variables, self.get_services(methods=False))
ret = self.__gen_conf(self.get_config(methods=False) | variables, self.get_services(methods=False), check_changes=check_changes)
if isinstance(ret, str):
return ret, 1
return "The global configuration has been edited.", 0

View file

@ -180,105 +180,117 @@
{% else %}
<h6 class="col-span-12 block text-left font-bold mb-4 mt-2">🧑‍🚀 An admin user already exists</h6>
{% endif %}
<h2 class="col-span-12 block text-left font-bold mb-4 mt-2 text-2xl">Settings</h2>
<!-- ui host-->
<div class="flex flex-col relative col-span-12 my-3 mx-2 max-w-[400px] w-full">
<h5 class="text-base my-1 transition duration-300 ease-in-out text-md font-bold m-0">
UI Host (REVERSE_PROXY_HOST)<strong class="required-mark">*</strong>
</h5>
<label class="sr-only" for="ui_host">ui host</label>
<input tabindex="1"
type="text"
id="ui_host"
name="ui_host"
value="{{ ui_host }}"
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="http://[hostname](:[port])"
pattern="^https?:\/\/.{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 ui host-->
<!-- ui url-->
<div class="flex flex-col relative col-span-12 my-3 mx-2 max-w-[400px] w-full">
<h5 class="text-base my-1 transition duration-300 ease-in-out text-md font-bold m-0">
UI URL (REVERSE_PROXY_URL)<strong class="required-mark">*</strong>
</h5>
<label class="sr-only" for="ui_url">ui url</label>
<input tabindex="1"
type="text"
id="ui_url"
name="ui_url"
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-zA-Z0-9\-]{0,255})$"
required />
</div>
<!-- end ui url-->
<!-- server name-->
<div class="flex flex-col relative col-span-12 my-3 mx-2 max-w-[400px] w-full">
<h5 class="text-base my-1 transition duration-300 ease-in-out text-md font-bold m-0">
Server name<strong class="required-mark">*</strong>
</h5>
<label class="sr-only" for="server_name">server name</label>
<input tabindex="1"
type="text"
id="server_name"
name="server_name"
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="www.example.com"
value="www.example.com"
pattern=".*"
minlength="1"
required />
</div>
<!-- end server name-->
<div class="flex flex-col relative col-span-12 my-3 mx-2 max-w-[400px] w-full">
<h5 class="text-base my-1 transition duration-300 ease-in-out text-md font-bold m-0">Check server name DNS</h5>
<div class="flex justify-start items-center">
<button tabindex="1"
id="check-server-name"
class="flex justify-start w-fit tracking-wide 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-xs ease-in shadow-xs bg-150 bg-x-25 hover:-translate-y-px active:opacity-85 hover:shadow-md">
<span>check</span>
<span aria-check-color
class="block ml-2 rounded-full w-4 h-4 bg-gray-300"
aria-description="show state of server name"></span>
<span class="sr-only" aria-check-result></span>
</button>
</div>
<p class="mt-4">In case of issues, you can also click <a id="check_url" class="privacy-link" href="https://www.example.com/setup/check" target="_blank">here</a> to perform a manual check.</p>
</div>
<!-- auto let's encrypt-->
<div class="flex flex-col relative col-span-12 my-3 mx-2 max-w-[400px] w-full">
<h5 class="text-base my-1 transition duration-300 ease-in-out text-md font-bold m-0">Auto let's encrypt</h5>
<label class="sr-only" for="auto_lets_encrypt">auto let's encrypt</label>
<div data-checkbox-handler="auto_lets_encrypt"
class="relative mb-7 md:mb-0 z-10">
{% if not ui_reverse_proxy %}
<h2 class="col-span-12 block text-left font-bold mb-4 mt-2 text-2xl">Settings</h2>
<!-- ui host-->
<div class="flex flex-col relative col-span-12 my-3 mx-2 max-w-[400px] w-full">
<h5 class="text-base my-1 transition duration-300 ease-in-out text-md font-bold m-0">
UI Host (REVERSE_PROXY_HOST)<strong class="required-mark">*</strong>
</h5>
<label class="sr-only" for="ui_host">ui host</label>
<input tabindex="1"
data-check
type="checkbox"
id="auto_lets_encrypt"
name="auto_lets_encrypt"
data-checked="false"
class="checkbox"
value="no" />
<svg data-checkbox-handler="auto_lets_encrypt"
class="pointer-events-none absolute fill-white left-0 top-0 translate-x-1 translate-y-2 h-3 w-3"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512">
<path class="pointer-events-none" d="M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7 425.4 105.4c12.5-12.5 32.8-12.5 45.3 0z">
</path>
</svg>
type="text"
id="ui_host"
name="ui_host"
value="{{ ui_host }}"
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="http://[hostname](:[port])"
pattern="^https?:\/\/.{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>
</div>
<!-- end auto let's encrypt-->
<div class="p-2 col-span-12 bg-gray-200 mt-4 md:mt-6 mb-1 py-2 px-2 rounded flex flex-col justify-center items-center w-full max-w-[400px] overflow-hidden">
<h5 class="text-sm md:text-base text-center mt-1 mb-2 transition duration-300 ease-in-out text-md font-bold m-0">
Your BunkerWeb UI final URL will be
</h5>
<p class="family-text text-center text-sm md:text-base break-words w-full px-4"
data-resume>https://</p>
</div>
<!-- end ui host-->
<!-- ui url-->
<div class="flex flex-col relative col-span-12 my-3 mx-2 max-w-[400px] w-full">
<h5 class="text-base my-1 transition duration-300 ease-in-out text-md font-bold m-0">
UI URL (REVERSE_PROXY_URL)<strong class="required-mark">*</strong>
</h5>
<label class="sr-only" for="ui_url">ui url</label>
<input tabindex="1"
type="text"
id="ui_url"
name="ui_url"
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-zA-Z0-9\-]{0,255})$"
required />
</div>
<!-- end ui url-->
<!-- server name-->
<div class="flex flex-col relative col-span-12 my-3 mx-2 max-w-[400px] w-full">
<h5 class="text-base my-1 transition duration-300 ease-in-out text-md font-bold m-0">
Server name<strong class="required-mark">*</strong>
</h5>
<label class="sr-only" for="server_name">server name</label>
<input tabindex="1"
type="text"
id="server_name"
name="server_name"
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="www.example.com"
value="www.example.com"
pattern=".*"
minlength="1"
required />
</div>
<!-- end server name-->
<div class="flex flex-col relative col-span-12 my-3 mx-2 max-w-[400px] w-full">
<h5 class="text-base my-1 transition duration-300 ease-in-out text-md font-bold m-0">Check server name DNS</h5>
<div class="flex justify-start items-center">
<button tabindex="1"
id="check-server-name"
class="flex justify-start w-fit tracking-wide 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-xs ease-in shadow-xs bg-150 bg-x-25 hover:-translate-y-px active:opacity-85 hover:shadow-md">
<span>check</span>
<span aria-check-color
class="block ml-2 rounded-full w-4 h-4 bg-gray-300"
aria-description="show state of server name"></span>
<span class="sr-only" aria-check-result></span>
</button>
</div>
<p class="mt-4">
In case of issues, you can also click <a id="check_url"
class="privacy-link"
href="https://www.example.com/setup/check"
target="_blank">here</a> to perform a manual check.
</p>
</div>
<!-- auto let's encrypt-->
<div class="flex flex-col relative col-span-12 my-3 mx-2 max-w-[400px] w-full">
<h5 class="text-base my-1 transition duration-300 ease-in-out text-md font-bold m-0">Auto let's encrypt</h5>
<label class="sr-only" for="auto_lets_encrypt">auto let's encrypt</label>
<div data-checkbox-handler="auto_lets_encrypt"
class="relative mb-7 md:mb-0 z-10">
<input tabindex="1"
data-check
type="checkbox"
id="auto_lets_encrypt"
name="auto_lets_encrypt"
data-checked="{% if auto_lets_encrypt %}true{% else %}false{% endif %}"
{% if auto_lets_encrypt %}checked{% endif %}
class="checkbox"
value="no" />
<svg data-checkbox-handler="auto_lets_encrypt"
class="pointer-events-none absolute fill-white left-0 top-0 translate-x-1 translate-y-2 h-3 w-3"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512">
<path class="pointer-events-none" d="M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7 425.4 105.4c12.5-12.5 32.8-12.5 45.3 0z">
</path>
</svg>
</div>
</div>
<!-- end auto let's encrypt-->
<div class="p-2 col-span-12 bg-gray-200 mt-4 md:mt-6 mb-1 py-2 px-2 rounded flex flex-col justify-center items-center w-full max-w-[400px] overflow-hidden">
<h5 class="text-sm md:text-base text-center mt-1 mb-2 transition duration-300 ease-in-out text-md font-bold m-0">
Your BunkerWeb UI final URL will be
</h5>
<p class="family-text text-center text-sm md:text-base break-words w-full px-4"
data-resume>https://</p>
</div>
{% else %}
<h6 class="col-span-12 block text-left font-bold mb-4 mt-2">
↪️ A reverse proxy is already configured for the web interface
</h6>
{% endif %}
<h2 class="col-span-12 block text-left font-bold my-4 text-2xl">Newsletter</h2>
<!-- email inpt-->
<div class="flex flex-col relative col-span-12 mx-2 max-w-[400px] w-full">
@ -350,6 +362,7 @@
</form>
</main>
<script nonce="{{ script_nonce }}">
{% if not ui_reverse_proxy %}
class ServerCheck {
constructor() {
this.servInp = document.querySelector("#server_name");
@ -472,41 +485,6 @@
}
}
class Loader {
constructor() {
this.menuContainer = document.querySelector("[data-menu-container]");
this.loaderContainer = document.querySelector("[data-loader]");
this.logoEl = document.querySelector("[data-loader-img]");
this.isLoading = true;
this.init();
}
init() {
this.loaderContainer.setAttribute("aria-hidden", "false");
this.loading();
window.addEventListener("load", (e) => {
setTimeout(() => {
this.loaderContainer.classList.add("opacity-0");
}, 550);
setTimeout(() => {
this.isLoading = false;
this.loaderContainer.classList.add("hidden");
this.loaderContainer.setAttribute("aria-hidden", "true");
}, 850);
});
}
loading() {
if ((this.isLoading = true)) {
setTimeout(() => {
this.logoEl.classList.toggle("scale-105");
this.loading();
}, 300);
}
}
}
class Checkbox {
constructor() {
this.init();
@ -549,18 +527,56 @@
});
}
}
{% endif %}
class Loader {
constructor() {
this.menuContainer = document.querySelector("[data-menu-container]");
this.loaderContainer = document.querySelector("[data-loader]");
this.logoEl = document.querySelector("[data-loader-img]");
this.isLoading = true;
this.init();
}
init() {
this.loaderContainer.setAttribute("aria-hidden", "false");
this.loading();
window.addEventListener("load", (e) => {
setTimeout(() => {
this.loaderContainer.classList.add("opacity-0");
}, 550);
setTimeout(() => {
this.isLoading = false;
this.loaderContainer.classList.add("hidden");
this.loaderContainer.setAttribute("aria-hidden", "true");
}, 850);
});
}
loading() {
if ((this.isLoading = true)) {
setTimeout(() => {
this.logoEl.classList.toggle("scale-105");
this.loading();
}, 300);
}
}
}
class SubmitForm {
constructor() {
this.formEl = document.querySelector("#setup-form");
{% if not ui_user %}
this.pwEl = document.querySelector("#admin_password");
this.pwCheckEl = document.querySelector("#admin_password_check");
this.pwAlertEl = document.querySelector("[data-pw-alert]");
{% endif %}
this.formEl = document.querySelector("#setup-form");
{% if not ui_reverse_proxy %}
this.servInp = document.querySelector("#server_name");
this.sslCheck = document.querySelector("#auto_lets_encrypt");
this.urlInp = document.querySelector("#ui_url");
{% endif %}
this.newsForm = document.querySelector("#newsletter-form");
this.emailInp = document.querySelector("#newsletter-email")
this.checkEmailInp = document.querySelector("#newsletter-check");
@ -623,7 +639,8 @@
}
// send form and wait for response
let api = `https://${this.servInp.value}`;
{% if not ui_reverse_proxy %}
var api = `https://${this.servInp.value}`;
if (!this.urlInp.value.startsWith("/")) {
api = `${api}/`;
}
@ -631,6 +648,10 @@
if (!api.endsWith("/")) {
api = `${api}/`;
}
var redirect = `https://${this.servInp.value}/setup/loading?target_uri=${this.urlInp.value}`;
{% else %}
var redirect = window.location.href.replace("setup", "login");
{% endif %}
fetch(window.location.href, {
method: "POST",
@ -639,7 +660,7 @@
})
.then((res) => {
if (res.status === 200) {
window.location.href = `https://${this.servInp.value}/setup/loading?target_uri=${this.urlInp.value}`;
window.location.href = redirect;
}
})
.catch((err) => {
@ -759,11 +780,13 @@
}
}
const setFlash = new FlashMsg()
const setLoader = new Loader();
const setFlash = new FlashMsg();
{% if not ui_reverse_proxy %}
const setResume = new Resume();
const setCheck = new Checkbox();
const setStateServ = new ServerCheck();
{% endif %}
const setLoader = new Loader();
const setSubmit = new SubmitForm();
</script>
</body>