feat: release design system engine, studio and component contracts
This commit is contained in:
1 parent
ff1065fb27
commit
3943b946d6
693 files changed
+175546
-3894
No files matched your search
@@ -415,6 +415,10 @@
|
||||
const challengeInput = form?.querySelector("[name=challengeId]");
|
||||
const radios = Array.from(form?.querySelectorAll("input[type=radio][name=method]") || []);
|
||||
const status = form?.querySelector("[data-auth-mfa-status]");
|
||||
const resendGroup = form?.querySelector("[data-auth-mfa-resend-group]");
|
||||
const resendButton = form?.querySelector("button[data-auth-mfa-resend]");
|
||||
const resendDeadlines = new Map();
|
||||
let resendTimer;
|
||||
const saved = readMfaState();
|
||||
const allowed = Array.isArray(saved?.requires?.mfa) ? saved.requires.mfa : [];
|
||||
const challenges = new Map();
|
||||
@@ -457,6 +461,19 @@
|
||||
};
|
||||
const method = () => radios.find((radio) => radio.checked && !radio.disabled)?.value;
|
||||
const isOtp = (value) => ["email-otp", "sms-otp"].includes(value);
|
||||
const updateResend = () => {
|
||||
window.clearTimeout(resendTimer);
|
||||
const selected = method();
|
||||
if (resendGroup) resendGroup.hidden = !isOtp(selected) || expired;
|
||||
if (!resendButton || !isOtp(selected) || expired || disposed) return;
|
||||
const seconds = Math.max(
|
||||
0,
|
||||
Math.ceil(((resendDeadlines.get(selected) || 0) - Date.now()) / 1000),
|
||||
);
|
||||
resendButton.disabled = pending.has(selected) || seconds > 0;
|
||||
resendButton.textContent = seconds > 0 ? "Resend code in " + seconds + "s" : "Resend code";
|
||||
if (seconds > 0) resendTimer = window.setTimeout(updateResend, 1000);
|
||||
};
|
||||
for (const radio of radios) {
|
||||
radio.disabled = allowed.length > 0 && !allowed.includes(radio.value);
|
||||
const label = radio.closest("label");
|
||||
@@ -468,8 +485,10 @@
|
||||
for (const radio of radios) radio.checked = radio === initial;
|
||||
restoreToken();
|
||||
|
||||
const sendCode = async (selected) => {
|
||||
const sendCode = async (selected, resend = false) => {
|
||||
if (disposed || expired || !isOtp(selected)) return;
|
||||
if (resend && (pending.has(selected) || (resendDeadlines.get(selected) || 0) > Date.now()))
|
||||
return;
|
||||
restoreToken();
|
||||
if (expired) return;
|
||||
if (!tokenInput?.value) {
|
||||
@@ -481,12 +500,13 @@
|
||||
const previous = challenges.get(selected);
|
||||
if (previous?.expiresAt && Number(previous.expiresAt) <= Date.now())
|
||||
challenges.delete(selected);
|
||||
if (!pending.has(selected) && !challenges.has(selected)) {
|
||||
if (!pending.has(selected) && (resend || !challenges.has(selected))) {
|
||||
pending.set(
|
||||
selected,
|
||||
json(element.dataset.mfaOtpAction || "/api/auth/mfa/otp", {
|
||||
mfaToken: tokenInput.value,
|
||||
method: selected,
|
||||
resend,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -495,16 +515,27 @@
|
||||
setMessage(status, "Sending a one-time code…");
|
||||
}
|
||||
try {
|
||||
const result = challenges.get(selected) || (await pending.get(selected));
|
||||
const result = (!resend && challenges.get(selected)) || (await pending.get(selected));
|
||||
if (disposed || expired) return;
|
||||
challenges.set(selected, result);
|
||||
if (!resendDeadlines.has(selected) || resend)
|
||||
resendDeadlines.set(selected, Date.now() + (Number(result.retryAfter) || 60) * 1000);
|
||||
if (method() !== selected) return;
|
||||
if (challengeInput) challengeInput.value = String(result.id || "");
|
||||
if (resend) {
|
||||
form?.querySelectorAll("[data-auth-mfa-pin] input, [name=code]").forEach((input) => {
|
||||
input.value = "";
|
||||
input.dispatchEvent(new window.Event("input", { bubbles: true }));
|
||||
});
|
||||
form?.querySelectorAll("[data-error]").forEach((error) => {
|
||||
error.textContent = "";
|
||||
});
|
||||
}
|
||||
setMessage(
|
||||
status,
|
||||
selected === "email-otp"
|
||||
? "A code has been sent to your email."
|
||||
: "A code has been sent to your mobile number.",
|
||||
? "An email code has been requested. Check your Inbox and Spam folder."
|
||||
: "An SMS code has been requested. It may take a moment to arrive.",
|
||||
);
|
||||
} catch (error) {
|
||||
if (!disposed && !expired && method() === selected)
|
||||
@@ -516,6 +547,7 @@
|
||||
} finally {
|
||||
pending.delete(selected);
|
||||
if (!disposed && !expired && method() === selected) setFormBusy(form, false);
|
||||
updateResend();
|
||||
}
|
||||
};
|
||||
const updateMethod = () => {
|
||||
@@ -528,6 +560,7 @@
|
||||
const error = form?.querySelector("[data-error=code]");
|
||||
if (error) error.textContent = "";
|
||||
setMessage(status, "");
|
||||
updateResend();
|
||||
for (const [selector, active] of [
|
||||
["[data-auth-mfa-pin]", selected !== "recovery-code"],
|
||||
["[data-auth-mfa-recovery]", selected === "recovery-code"],
|
||||
@@ -546,6 +579,10 @@
|
||||
const onMethodChange = (event) => {
|
||||
if (radios.includes(event.target)) updateMethod();
|
||||
};
|
||||
const onResend = (event) => {
|
||||
event.preventDefault();
|
||||
void sendCode(method(), true);
|
||||
};
|
||||
const onSubmit = (event) => {
|
||||
restoreToken();
|
||||
const code = form?.querySelector("[name=code]");
|
||||
@@ -573,11 +610,14 @@
|
||||
// Capture native change before the shared Radio emits its component output.
|
||||
form?.addEventListener("change", onMethodChange, true);
|
||||
form?.addEventListener("submit", onSubmit, true);
|
||||
resendButton?.addEventListener("click", onResend);
|
||||
element.addEventListener("wrn:success", onSuccess);
|
||||
element.addEventListener("wrn:error", onError);
|
||||
flowCleanups.set(element, () => {
|
||||
disposed = true;
|
||||
window.clearTimeout(expiryTimer);
|
||||
window.clearTimeout(resendTimer);
|
||||
resendButton?.removeEventListener("click", onResend);
|
||||
form?.removeEventListener("change", onMethodChange, true);
|
||||
form?.removeEventListener("submit", onSubmit, true);
|
||||
element.removeEventListener("wrn:success", onSuccess);
|
||||
|
||||
Reference in new issue
Block a user