diff --git a/CHANGELOG b/CHANGELOG index 2af0756e3..f04c6864a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -49,6 +49,7 @@ Cacti CHANGELOG -issue#8120: Do not replicate repository and tooling metadata files (.gitignore, .htaccess.dist, .mdl_style.rb, .mdlrc) to remote data collectors during file replication -issue#8116: Restore legacy unique_keys[] support in db_table_create() and db_update_table() so plugins that declare UNIQUE indexes with that definition style keep them across a schema refresh -issue#8105: Add a Force Uninstall action to remove an orphaned plugin whose directory is missing +-issue#8073: Render a separate password reset link on local login errors while keeping error text escaped -issue#7969: Restore excluded integration and mutation regression coverage -issue#7969: Align Rocky Linux compatibility checks with the supported PHP runtime -issue#8063: Support searching by graph ID on the Graphs list/preview and Aggregate Graphs pages diff --git a/auth_login.php b/auth_login.php index f2a99f542..8cecebf59 100644 --- a/auth_login.php +++ b/auth_login.php @@ -434,4 +434,4 @@ html_auth_footer('login', $error_message, " }); -"); +", $error && $frv_realm <= 1); diff --git a/lib/Auth/LocalAuthLoginProvider.php b/lib/Auth/LocalAuthLoginProvider.php index 6e5353c72..0546cd21f 100644 --- a/lib/Auth/LocalAuthLoginProvider.php +++ b/lib/Auth/LocalAuthLoginProvider.php @@ -161,7 +161,7 @@ final class LocalAuthLoginProvider implements CredentialLoginProviderInterface { if (!$error) { $error = true; - $error_msg = __('Access Denied! Login Failed.') . ' ' . __('Reset password') . ''; + $error_msg = __('Access Denied! Login Failed.'); cacti_log(sprintf('LOGIN FAILED: Local Login Failed for user %s from IP Address %s', $username, get_client_addr()), false, 'AUTH'); } diff --git a/lib/html.php b/lib/html.php index d3db1fd7a..dd152b2cf 100644 --- a/lib/html.php +++ b/lib/html.php @@ -3638,13 +3638,14 @@ function html_auth_header(string $section, string $browser_title, string $legend * including any error messages, version information, and additional HTML content. * It also triggers a plugin hook and includes the global session file. * - * @param string $section The section identifier used for the plugin hook. - * @param string $error Optional. The error message to display. Default is an empty string. - * @param string $html Optional. Additional HTML content to include. Default is an empty string. + * @param string $section The section identifier used for the plugin hook. + * @param string $error Optional. The error message to display. Default is an empty string. + * @param string $html Optional. Additional HTML content to include. Default is an empty string. + * @param bool $password_reset Show the local password-reset link separately from the error text. * * @return void */ -function html_auth_footer(string $section, string $error = '', string $html = '') : void { +function html_auth_footer(string $section, string $error = '', string $html = '', bool $password_reset = false) : void { ?> @@ -3653,6 +3654,9 @@ function html_auth_footer(string $section, string $error = '', string $html = ''
+ + '> +
diff --git a/locales/po/cacti.pot b/locales/po/cacti.pot index 065ec6f79..1e8ba5a3e 100644 --- a/locales/po/cacti.pot +++ b/locales/po/cacti.pot @@ -25459,3 +25459,11 @@ msgstr "" #: layouts.php msgid "Click 'Continue' to make the following Layouts Local to you." msgstr "" + +#: lib/html.php:3658 +msgid "Reset password" +msgstr "" + +#: lib/Auth/LocalAuthLoginProvider.php:164 +msgid "Access Denied! Login Failed." +msgstr "" diff --git a/tests/e2e/tests/login-smoke.spec.ts b/tests/e2e/tests/login-smoke.spec.ts index dcb626d51..ded38c810 100644 --- a/tests/e2e/tests/login-smoke.spec.ts +++ b/tests/e2e/tests/login-smoke.spec.ts @@ -15,6 +15,37 @@ import { test, expect } from '@playwright/test'; +for (const scenario of [ + { name: 'wrong password', username: 'admin', password: 'wrong-fixture-password' }, + { name: 'unknown user', username: 'missingfixtureuser', password: 'wrong-fixture-password' }, + { name: 'empty password', username: 'admin', password: '' }, +]) { + test(`local login ${scenario.name} renders a working reset link`, async ({ page }) => { + await page.goto('/'); + await expect(page.locator('.cactiAuthErrors a')).toHaveCount(0); + if (await page.locator('#realm').count()) { + await page.locator('#realm').selectOption({ label: 'Local' }); + } + await page.locator('#login_username').fill(scenario.username); + await page.locator('#login_password').fill(scenario.password); + if (!scenario.password) { + await page.locator('#login_password').evaluate((input: HTMLInputElement) => { input.required = false; }); + } + await Promise.all([ + page.waitForNavigation(), + page.locator('form#auth input[type="submit"], form#auth button[type="submit"]').click(), + ]); + const errors = page.locator('.cactiAuthErrors'); + await expect(errors).not.toContainText(' { const errors: string[] = [];