fix(auth): render password reset links outside login error text (#8289)

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
This commit is contained in:
Thomas Vincent authored and GitHub committed 2026-10-10 20:24:49 -04:00
1 parent 54251fd2fc
commit bcb4aaf4ae
6 files changed
+50 -6

No files matched your search

+1
View File
@@ -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
+1 -1
View File
@@ -434,4 +434,4 @@ html_auth_footer('login', $error_message, "
});
</script>
");
", $error && $frv_realm <= 1);
+1 -1
View File
@@ -161,7 +161,7 @@ final class LocalAuthLoginProvider implements CredentialLoginProviderInterface {
if (!$error) {
$error = true;
$error_msg = __('Access Denied! Login Failed.') . ' <a href="auth_resetpassword.php">' . __('Reset password') . '</a>';
$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');
}
+8 -4
View File
@@ -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 {
?>
</table>
</div>
@@ -3653,6 +3654,9 @@ function html_auth_footer(string $section, string $error = '', string $html = ''
<hr />
<div class='cactiAuthErrors'>
<?php print htmle($error); ?>
<?php if ($password_reset) { ?>
<a href='<?php print html_escape_attr(cacti_url('auth_resetpassword.php')); ?>'><?php print __esc('Reset password'); ?></a>
<?php } ?>
</div>
<div class='versionInfo'>
<?php print __('Version %s | %s', CACTI_VERSION_BRIEF, COPYRIGHT_YEARS_SHORT); ?>
+8
View File
@@ -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 ""
+31
View File
@@ -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('<a');
const reset = errors.getByRole('link', { name: 'Reset password', exact: true });
await expect(reset).toHaveCount(1);
await expect(reset).toHaveAttribute('href', /auth_resetpassword\.php$/);
await reset.click();
await expect(page).toHaveURL(/auth_resetpassword\.php$/);
await expect(page.locator('form#auth')).toBeVisible();
});
}
test('login page renders form controls without browser errors', async ({ page }) => {
const errors: string[] = [];