From e6592f41af073258ebaed6aa4f31306afddbe3ee Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Tue, 22 Sep 2026 09:57:37 +0000 Subject: [PATCH] fix(login): restore i18n placeholders and keep passkey-only logins working Gate follow-up for #7056. Two defects in the conditional password form: 1. The {{LOGIN_INVALID_PW}} / {{LOGIN_CONN_FAILED}} .replace() calls were deleted with the moved block, but those placeholders live on the
tag which ALWAYS renders. login.js reads data-invalid-pw back, and the literal is non-empty so its || fallback never fired: a wrong password displayed the raw text {{LOGIN_INVALID_PW}} in all 14 locales, even with password auth ENABLED. Proven by a clean-master control (test_login_locale 3/3 on master, 2/3 on the PR). 2. is_password_auth_enabled() also gated the PASSKEY button, but passkeys are an independent auth method (auth.py is_auth_enabled, and settings expose passwordless_enabled = passkeys AND not password). A passwordless-passkey instance lost its only login control. login.js additionally bailed at 'if (!form || !input) return' before ever wiring the passkey handler. --- api/routes.py | 22 +++++++++++++++++++--- static/login.js | 22 +++++++++++++++------- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/api/routes.py b/api/routes.py index 1645084a..2e716b36 100644 --- a/api/routes.py +++ b/api/routes.py @@ -13638,15 +13638,27 @@ def handle_get(handler, parsed) -> bool: # the server — every submit. The OIDC SSO entry point stays the # sole path. ``is_password_auth_enabled`` is the same predicate # ``/api/auth/status`` reports as ``password_auth_enabled``. - from api.auth import is_password_auth_enabled + from api.auth import are_passkeys_enabled, is_password_auth_enabled + + # The password INPUT is gated on a configured password, but the passkey + # button must survive a passwordless-passkey deployment: settings expose + # ``passwordless_enabled = passkeys registered AND not password_auth_enabled`` + # (routes.py ~14059) and ``is_auth_enabled()`` counts passkeys as an + # independent auth method, so hiding the button when no password is set + # would remove the ONLY working login affordance for those instances. + _passkey_button_html = ( + '' + ) if is_password_auth_enabled(): _password_form_html = ( f'' f'' - '' + f'{_passkey_button_html}' ) + elif are_passkeys_enabled(): + _password_form_html = _passkey_button_html else: _password_form_html = "" version_token = quote(WEBUI_VERSION, safe="") @@ -13658,6 +13670,10 @@ def handle_get(handler, parsed) -> bool: .replace("{{LOGIN_TITLE}}", _html.escape(_login_strings["title"])) .replace("{{LOGIN_SUBTITLE}}", _html.escape(_login_strings["subtitle"])) .replace("{{PASSWORD_FORM_HTML}}", _password_form_html) + .replace("{{LOGIN_INVALID_PW}}", _html.escape(_login_strings["invalid_pw"])) + .replace( + "{{LOGIN_CONN_FAILED}}", _html.escape(_login_strings["conn_failed"]) + ) .replace("{{OIDC_LOGIN_HTML}}", _oidc_login_html(parsed)) ) return t(handler, _page, content_type="text/html; charset=utf-8") diff --git a/static/login.js b/static/login.js index 3bba3bb1..d702ca67 100644 --- a/static/login.js +++ b/static/login.js @@ -7,7 +7,11 @@ document.addEventListener('DOMContentLoaded', function () { var input = document.getElementById('pw'); var passkeyBtn = document.getElementById('passkey-login'); - if (!form || !input) return; + // #7056: the password input is absent on a passwordless deployment (OIDC-only, + // or passkey-only). The form itself still renders and carries the i18n data + // attributes, and the passkey button below must still be wired up, so only the + // form is required here — every password-specific path is guarded individually. + if (!form) return; var invalidPw = form.getAttribute('data-invalid-pw') || 'Invalid password'; var connFailed = form.getAttribute('data-conn-failed') || 'Connection failed'; @@ -59,6 +63,8 @@ document.addEventListener('DOMContentLoaded', function () { async function doLogin(e) { e.preventDefault(); + // No password input on a passwordless deployment: nothing to submit. + if (!input) return; var pw = input.value; hideErr(); try { @@ -147,12 +153,14 @@ document.addEventListener('DOMContentLoaded', function () { passkeyBtn.addEventListener('click', doPasskeyLogin); } - input.addEventListener('keydown', function (e) { - if (e.key === 'Enter') { - e.preventDefault(); - doLogin(e); - } - }); + if (input) { + input.addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + doLogin(e); + } + }); + } // On page load, probe the server so we can distinguish "can't reach server" // (Tailscale off, wrong network) from "session expired / need to log in".