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 <form>
   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.
This commit is contained in:
nesquena-hermes committed 2026-09-22 09:57:37 +00:00
1 parent c53226dcc4
commit e6592f41af
2 files changed
+34 -10

No files matched your search

+19 -3
View File
@@ -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 = (
'<button type="button" id="passkey-login" class="passkey-login" '
'style="display:none">Sign in with passkey</button>'
)
if is_password_auth_enabled():
_password_form_html = (
f'<input type="password" id="pw" '
f'placeholder="{_html.escape(_login_strings["placeholder"])}" autofocus>'
f'<button type="submit">{_html.escape(_login_strings["btn"])}</button>'
'<button type="button" id="passkey-login" class="passkey-login" '
'style="display:none">Sign in with passkey</button>'
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")
+15 -7
View File
@@ -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".