Files
cryptomon/internal/web/templates/login.html
T
2026-08-25 06:37:14 +01:00

112 lines
3.7 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sign in — Basis</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/style.css">
</head>
<body class="login-body">
<main class="login-main">
<section class="panel login-panel">
<h1 class="login-title">Basis</h1>
<p class="notice error" id="login-error" hidden></p>
<form id="login-form" class="form">
<label>Username <input type="text" name="username" autocomplete="username" required autofocus></label>
<label>Password <input type="password" name="password" autocomplete="current-password" required></label>
<button type="submit">Sign in</button>
</form>
</section>
</main>
<div class="modal-overlay" id="mfa-overlay" hidden>
<div class="modal">
<h2>Enter authenticator code</h2>
<p class="hint">Your account has MFA enabled — enter the 6-digit code from your authenticator app.</p>
<p class="notice error" id="mfa-error" hidden></p>
<form id="mfa-form" class="form">
<label>Code <input type="text" name="code" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" autocomplete="one-time-code" required></label>
<button type="submit">Verify</button>
</form>
</div>
</div>
<script>
(function () {
"use strict";
var loginForm = document.getElementById("login-form");
var loginError = document.getElementById("login-error");
var overlay = document.getElementById("mfa-overlay");
var mfaForm = document.getElementById("mfa-form");
var mfaError = document.getElementById("mfa-error");
var pendingToken = "";
function postForm(url, fields) {
var body = Object.keys(fields).map(function (k) {
return encodeURIComponent(k) + "=" + encodeURIComponent(fields[k]);
}).join("&");
return fetch(url, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: body,
}).then(function (r) { return r.json(); });
}
function showError(el, msg) {
el.textContent = msg;
el.hidden = false;
}
loginForm.addEventListener("submit", function (e) {
e.preventDefault();
loginError.hidden = true;
var fd = new FormData(loginForm);
postForm("/login", { username: fd.get("username"), password: fd.get("password") }).then(function (res) {
if (res.error) {
showError(loginError, res.error);
return;
}
if (res.mfa_required) {
pendingToken = res.pending_token;
overlay.hidden = false;
mfaForm.querySelector('input[name="code"]').focus();
return;
}
window.location.href = res.redirect || "/";
}).catch(function () {
showError(loginError, "Something went wrong — try again.");
});
});
mfaForm.addEventListener("submit", function (e) {
e.preventDefault();
mfaError.hidden = true;
var fd = new FormData(mfaForm);
postForm("/login/mfa", { pending_token: pendingToken, code: fd.get("code") }).then(function (res) {
if (res.error) {
showError(mfaError, res.error);
return;
}
window.location.href = res.redirect || "/";
}).catch(function () {
showError(mfaError, "Something went wrong — try again.");
});
});
overlay.addEventListener("click", function (e) {
if (e.target === overlay) {
overlay.hidden = true;
pendingToken = "";
mfaForm.reset();
mfaError.hidden = true;
}
});
})();
</script>
</body>
</html>