131 lines
4.8 KiB
Plaintext
131 lines
4.8 KiB
Plaintext
@model TwoFactorCodeViewModel
|
|
@{
|
|
ViewData["Title"] = "Two-factor authentication";
|
|
}
|
|
|
|
<div class="page-heading compact">
|
|
<div>
|
|
<p class="eyebrow">Account</p>
|
|
<h1>Two-factor authentication</h1>
|
|
<p class="lead-text">Enter the current 6-digit code from your authenticator app.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<section class="edit-panel two-factor-card">
|
|
<form asp-action="TwoFactor" method="post" class="asset-create-form two-factor-body" data-two-factor-form>
|
|
<input asp-for="ReturnUrl" type="hidden" />
|
|
<input asp-for="Code" type="hidden" data-two-factor-code />
|
|
<div asp-validation-summary="ModelOnly" class="alert alert-warning"></div>
|
|
|
|
<div class="two-factor-code-group">
|
|
<label class="form-label two-factor-label" for="twoFactorDigit1">Authenticator code</label>
|
|
<div class="two-factor-inputs" role="group" aria-label="Authenticator code">
|
|
@for (var i = 1; i <= 6; i++)
|
|
{
|
|
<input class="form-control two-factor-digit"
|
|
id="twoFactorDigit@(i)"
|
|
type="text"
|
|
inputmode="numeric"
|
|
pattern="[0-9]*"
|
|
maxlength="1"
|
|
autocomplete="one-time-code"
|
|
aria-label="Digit @i of 6"
|
|
data-two-factor-digit />
|
|
}
|
|
</div>
|
|
<span class="text-danger" asp-validation-for="Code"></span>
|
|
</div>
|
|
|
|
<div class="two-factor-actions">
|
|
<button class="btn btn-primary" type="submit">Continue</button>
|
|
<a class="btn btn-outline-secondary" asp-action="Login">Back to Login</a>
|
|
</div>
|
|
<p class="two-factor-help">
|
|
<span>Can't access your authenticator app?</span>
|
|
<a asp-action="TwoFactorRecovery" asp-route-returnUrl="@Model.ReturnUrl">Use a recovery code.</a>
|
|
</p>
|
|
</form>
|
|
</section>
|
|
|
|
@section Scripts {
|
|
<partial name="_ValidationScriptsPartial" />
|
|
<script>
|
|
(() => {
|
|
const form = document.querySelector("[data-two-factor-form]");
|
|
if (!form) {
|
|
return;
|
|
}
|
|
|
|
const hiddenCode = form.querySelector("[data-two-factor-code]");
|
|
const digits = Array.from(form.querySelectorAll("[data-two-factor-digit]"));
|
|
let hasSubmitted = false;
|
|
|
|
const codeValue = () => digits.map(input => input.value).join("");
|
|
const syncCode = () => {
|
|
hiddenCode.value = codeValue();
|
|
};
|
|
const isComplete = () => digits.every(input => input.value.length === 1);
|
|
const submitIfComplete = () => {
|
|
syncCode();
|
|
if (isComplete() && !hasSubmitted) {
|
|
hasSubmitted = true;
|
|
form.requestSubmit();
|
|
}
|
|
};
|
|
const fillFrom = (startIndex, value) => {
|
|
const numbers = value.replace(/\D/g, "").slice(0, digits.length - startIndex);
|
|
numbers.split("").forEach((number, offset) => {
|
|
digits[startIndex + offset].value = number;
|
|
});
|
|
|
|
const nextIndex = Math.min(startIndex + numbers.length, digits.length - 1);
|
|
digits[nextIndex].focus();
|
|
submitIfComplete();
|
|
};
|
|
|
|
digits.forEach((input, index) => {
|
|
input.addEventListener("input", () => {
|
|
if (input.value.length > 1) {
|
|
fillFrom(index, input.value);
|
|
return;
|
|
}
|
|
|
|
input.value = input.value.replace(/\D/g, "");
|
|
syncCode();
|
|
|
|
if (input.value && index < digits.length - 1) {
|
|
digits[index + 1].focus();
|
|
}
|
|
|
|
submitIfComplete();
|
|
});
|
|
|
|
input.addEventListener("keydown", event => {
|
|
if (event.key === "Backspace" && !input.value && index > 0) {
|
|
digits[index - 1].focus();
|
|
digits[index - 1].value = "";
|
|
syncCode();
|
|
}
|
|
});
|
|
|
|
input.addEventListener("paste", event => {
|
|
event.preventDefault();
|
|
fillFrom(index, event.clipboardData.getData("text"));
|
|
});
|
|
|
|
input.addEventListener("focus", () => input.select());
|
|
});
|
|
|
|
form.addEventListener("submit", () => {
|
|
syncCode();
|
|
});
|
|
|
|
digits.forEach(input => {
|
|
input.value = "";
|
|
});
|
|
syncCode();
|
|
digits[0].focus();
|
|
})();
|
|
</script>
|
|
}
|