PlotDirector/PlotLine/Views/Account/TwoFactor.cshtml

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>
}