From 5651e9bff3f8f182cbdd11d718ea48250b22a226 Mon Sep 17 00:00:00 2001 From: Tag Howard Date: Sun, 13 Jul 2025 13:00:39 -0400 Subject: [PATCH] Improve the JS for SSO (#83) * Tweak code field error status * Add a toggle for SSO vs Code and show a proper error when code fails * Refactor SSO button handling and improve error message display * Update timeout warning message duration in UI injection --- .../auth_oidc/static/injection.js | 127 +++++++++++++++--- 1 file changed, 110 insertions(+), 17 deletions(-) diff --git a/custom_components/auth_oidc/static/injection.js b/custom_components/auth_oidc/static/injection.js index e6d0a4c..27ce458 100644 --- a/custom_components/auth_oidc/static/injection.js +++ b/custom_components/auth_oidc/static/injection.js @@ -1,45 +1,138 @@ function safeSetTextContent(element, value) { if (!element) return var textNode = Array.from(element.childNodes).find(node => node.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0) - if (!textNode) return + if (!textNode || textNode.textContent === value) return textNode.textContent = value } -function addSSOButton() { +const SSO_NAME = window.sso_name || "Single Sign-On" + +let firstFocus = true +let showCodeOverride = null + +function showCode() { + if (showCodeOverride !== null) return showCodeOverride + + const clientId = new URL(location.href).searchParams.get("client_id") + return clientId && clientId.startsWith("https://home-assistant.io/iOS") || clientId.startsWith("https://home-assistant.io/android") +} + +let ssoButton = null +let codeMessage = null +let codeToggle = null +let codeToggleText = null + +function update() { const loginHeader = document.querySelector(".card-content > ha-auth-flow > form > h1") - safeSetTextContent(loginHeader, "Log in to Home Assistant") - + const authForm = document.querySelector("ha-auth-form") const codeField = document.querySelector(".mdc-text-field__input[name=code]") const loginButton = document.querySelector("mwc-button:not(.sso)") + const errorAlert = document.querySelector("ha-auth-form ha-alert[alert-type=error]") + const loginOptionList = document.querySelector("ha-pick-auth-provider")?.shadowRoot?.querySelector("mwc-list") + + safeSetTextContent(loginHeader, "Log in to Home Assistant") if (codeField) { - codeField.placeholder = "One-time code" - codeField.autofocus = false - codeField.autocomplete = "off" - setTimeout(() => { - codeField.blur() - }, 0) + if (codeField.placeholder !== "One-time code") { + codeField.placeholder = "One-time code" + codeField.autofocus = false + codeField.autocomplete = "off" + if (firstFocus) { + firstFocus = false + if (document.activeElement === codeField) { + setTimeout(() => { + codeField.blur() + let check = setInterval(() => { + const helperText = document.querySelector("#helper-text") + const invalidTextField = document.querySelector(".mdc-text-field--invalid") + const validationMsg = document.querySelector(".mdc-text-field-helper-text--validation-msg") + if (helperText && invalidTextField && validationMsg) { + clearInterval(check) + safeSetTextContent(helperText, "") + invalidTextField.classList.remove("mdc-text-field--invalid") + validationMsg.classList.remove("mdc-text-field-helper-text--validation-msg") + } + }, 1) + }, 0) + } + } + } + if (errorAlert && errorAlert.textContent.trim().length === 0) { + errorAlert.setAttribute("title", "Invalid Code") + } + authForm.style.display = showCode() ? "" : "none" + loginButton.style.display = showCode() ? "" : "none" } - var ssoButton = document.querySelector("mwc-button.sso") - if (!ssoButton) { + if (authForm && !codeMessage) { + codeMessage = document.createElement("p") + codeMessage.innerText = `Login to Home Assistant in a web browser and enter the code you are given here` + authForm.parentElement.insertBefore(codeMessage, authForm) + } + if (codeMessage) { + codeMessage.style.display = showCode() ? "" : "none" + } + + if (loginOptionList && !codeToggle) { + codeToggle = document.createElement("ha-list-item") + codeToggle.setAttribute("hasmeta", "") + codeToggleText = document.createTextNode("") + codeToggle.appendChild(codeToggleText) + const codeToggleIcon = document.createElement("ha-icon-next") + codeToggleIcon.setAttribute("slot", "meta") + codeToggle.appendChild(codeToggleIcon) + + codeToggle.addEventListener("click", () => { + showCodeOverride = !showCode() + update() + }) + loginOptionList.appendChild(codeToggle) + } + + if (codeToggle) { + codeToggle.style.display = codeField ? "" : "none" + } + + if (codeToggleText) { + codeToggleText.textContent = showCode() ? SSO_NAME : "Login Code" + } + + if (loginButton && !ssoButton) { ssoButton = document.createElement("mwc-button") ssoButton.classList.add("sso") - ssoButton.innerText = "Log in with " + window.sso_name + ssoButton.innerText = "Log in with " + SSO_NAME ssoButton.setAttribute("raised", "") ssoButton.style.marginRight = "1em" - ssoButton.style.display = "none" ssoButton.addEventListener("click", () => { location.href = "/auth/oidc/redirect" }) loginButton.parentElement.prepend(ssoButton) } + if (ssoButton) { + ssoButton.style.display = (!showCode() && codeField) ? "" : "none" + } safeSetTextContent(loginButton, codeField ? "Log in with code" : "Log in") - ssoButton.style.display = codeField ? "" : "none" } +let ready = false +document.querySelector(".content").style.display = "none" + const observer = new MutationObserver((mutationsList, observer) => { - addSSOButton() + update() + + if (!ready) { + ready = Boolean(ssoButton && codeMessage && codeToggle && codeToggleText) + if (ready) document.querySelector(".content").style.display = "" + } }) -observer.observe(document.body, { childList: true, subtree: true }) \ No newline at end of file + +observer.observe(document.body, { childList: true, subtree: true }) + +setTimeout(() => { + if (!ready) { + console.warn("hass-oidc-auth: Document was not ready after 300ms seconds, force displaying. This may indicate a problem with the UI injection.") + } + document.querySelector(".content").style.display = ""; + update(); +}, 300) \ No newline at end of file