Fix login remember-me submit state

Refs #1531
This commit is contained in:
rcourtman 2026-07-06 22:41:27 +01:00
parent 1d7fa6c4bf
commit 6f9f310ba6
2 changed files with 44 additions and 7 deletions

View file

@ -188,14 +188,16 @@ export const Login: Component<LoginProps> = (props) => {
setError('');
setLoading(true);
// Read values directly from the form DOM to handle password manager autofill
// Password managers may fill fields without triggering input events,
// leaving the SolidJS signals empty while the DOM has the actual values
// Read values directly from the form DOM to handle password manager and
// browser form-state restores that do not trigger Solid input/change events.
const form = e.currentTarget as HTMLFormElement;
const usernameInput = form.querySelector('#username') as HTMLInputElement;
const passwordInput = form.querySelector('#password') as HTMLInputElement;
const rememberInput = form.querySelector('#remember-me') as HTMLInputElement;
const usernameValue = usernameInput?.value || username();
const passwordValue = passwordInput?.value || password();
const rememberLogin = rememberInput?.checked ?? rememberMe();
setRememberMe(rememberLogin);
// Validate that we have credentials before attempting login
if (!usernameValue || !passwordValue) {
@ -215,7 +217,7 @@ export const Login: Component<LoginProps> = (props) => {
body: JSON.stringify({
username: usernameValue,
password: passwordValue,
rememberMe: rememberMe(),
rememberMe: rememberLogin,
}),
skipAuth: true,
});
@ -229,7 +231,7 @@ export const Login: Component<LoginProps> = (props) => {
} catch (_err) {
// Ignore storage failures (private browsing, etc.)
}
persistRememberedUsername(usernameValue, rememberMe());
persistRememberedUsername(usernameValue, rememberLogin);
props.onLogin();
} else if (response.status === 403) {
// Account is locked
@ -241,7 +243,7 @@ export const Login: Component<LoginProps> = (props) => {
setError(data.message || 'Account temporarily locked due to too many failed attempts.');
}
// Clear the input fields
if (!rememberMe()) setUsername('');
if (!rememberLogin) setUsername('');
setPassword('');
} else if (response.status === 429) {
// Rate limited
@ -258,7 +260,7 @@ export const Login: Component<LoginProps> = (props) => {
setError(data.message || 'Invalid username or password');
}
// Clear the input fields
if (!rememberMe()) setUsername('');
if (!rememberLogin) setUsername('');
setPassword('');
} else {
setError(data.message || 'Server error. Please try again.');

View file

@ -242,6 +242,41 @@ describe('Login', () => {
);
});
it('uses the submitted checkbox state when browser restore does not fire change', async () => {
const mockOnLogin = vi.fn();
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
const securityStatus = {
hasAuthentication: true,
hideLocalLogin: false,
ssoEnabled: false,
};
render(() => (
<Login onLogin={mockOnLogin} hasAuth={true} securityStatus={securityStatus as any} />
));
fireEvent.input(await screen.findByPlaceholderText('Username'), {
target: { value: 'johannes' },
});
fireEvent.input(screen.getByPlaceholderText('Password'), { target: { value: 'secret' } });
(screen.getByLabelText('Remember me') as HTMLInputElement).checked = true;
fireEvent.click(screen.getByRole('button', { name: /sign in to pulse/i }));
await waitFor(() => expect(mockOnLogin).toHaveBeenCalledOnce());
expect(mockLocalStorage.setItem).toHaveBeenCalledWith(
STORAGE_KEYS.REMEMBERED_LOGIN_USERNAME,
'johannes',
);
expect(mockLocalStorage.removeItem).not.toHaveBeenCalledWith(
STORAGE_KEYS.REMEMBERED_LOGIN_USERNAME,
);
});
it('clears a previously remembered username after a successful non-remembered login', async () => {
mockLocalStorageData[STORAGE_KEYS.REMEMBERED_LOGIN_USERNAME] = 'old-user';
const mockOnLogin = vi.fn();