From bf969ee80d60f44f69fe9c247c0a5d0af611e343 Mon Sep 17 00:00:00 2001 From: jubnl <66769052+jubnl@users.noreply.github.com> Date: Mon, 15 Jun 2026 12:21:05 +0200 Subject: [PATCH] feat(auth): add "Remember me" checkbox to extend session lifetime (#1189) Adds a "Remember me" checkbox to the login form (single responsive page, covers mobile + desktop). Unchecked (default) issues the existing SESSION_DURATION JWT with a browser-session cookie (no maxAge); checked issues a longer-lived JWT plus a persistent cookie sized by the new SESSION_DURATION_REMEMBER env var (default 30d). The choice is threaded through the MFA verify leg so it survives the step-up. Register/demo logins keep their current persistent behaviour. --- client/src/pages/LoginPage.test.tsx | 32 ++++++++++++++++++ client/src/pages/LoginPage.tsx | 13 ++++++-- client/src/pages/login/useLogin.ts | 7 ++-- client/src/store/authStore.ts | 12 +++---- server/src/config.ts | 18 ++++++++++ .../src/nest/auth/auth-public.controller.ts | 4 +-- server/src/nest/auth/auth.service.ts | 2 +- server/src/services/authService.ts | 25 ++++++++++---- server/src/services/cookie.ts | 33 ++++++++++++++----- server/tests/e2e/auth.e2e.test.ts | 22 +++++++++++++ .../tests/unit/nest/auth.controller.test.ts | 5 +-- server/tests/unit/services/cookie.test.ts | 13 ++++++++ shared/src/auth/auth.schema.ts | 7 ++++ shared/src/i18n/ar/login.ts | 1 + shared/src/i18n/br/login.ts | 1 + shared/src/i18n/cs/login.ts | 1 + shared/src/i18n/de/login.ts | 1 + shared/src/i18n/en/login.ts | 1 + shared/src/i18n/es/login.ts | 1 + shared/src/i18n/fr/login.ts | 1 + shared/src/i18n/gr/login.ts | 1 + shared/src/i18n/hu/login.ts | 1 + shared/src/i18n/id/login.ts | 1 + shared/src/i18n/it/login.ts | 1 + shared/src/i18n/ja/login.ts | 1 + shared/src/i18n/ko/login.ts | 1 + shared/src/i18n/nl/login.ts | 1 + shared/src/i18n/pl/login.ts | 1 + shared/src/i18n/ru/login.ts | 1 + shared/src/i18n/tr/login.ts | 1 + shared/src/i18n/uk/login.ts | 1 + shared/src/i18n/zh-TW/login.ts | 1 + shared/src/i18n/zh/login.ts | 1 + wiki/Environment-Variables.md | 3 +- 34 files changed, 184 insertions(+), 32 deletions(-) diff --git a/client/src/pages/LoginPage.test.tsx b/client/src/pages/LoginPage.test.tsx index 5f5adc87..ae9d5818 100644 --- a/client/src/pages/LoginPage.test.tsx +++ b/client/src/pages/LoginPage.test.tsx @@ -103,6 +103,38 @@ describe('LoginPage', () => { }); }); + describe('FE-PAGE-LOGIN-007: Remember me sends remember_me to the API', () => { + it('renders an unchecked checkbox and forwards remember_me: true when ticked', async () => { + let capturedBody: Record | null = null; + server.use( + http.post('/api/auth/login', async ({ request }) => { + capturedBody = (await request.json()) as Record; + return HttpResponse.json({ user: { id: 1, username: 'test', email: 'test@example.com', role: 'user' } }); + }), + ); + + const user = userEvent.setup(); + render(); + + await waitFor(() => { + expect(screen.getByPlaceholderText(EMAIL_PLACEHOLDER)).toBeInTheDocument(); + }); + + const checkbox = screen.getByRole('checkbox', { name: /remember me/i }); + expect(checkbox).not.toBeChecked(); + + await user.type(screen.getByPlaceholderText(EMAIL_PLACEHOLDER), 'user@example.com'); + await user.type(screen.getByPlaceholderText(PASSWORD_PLACEHOLDER), 'password123'); + await user.click(checkbox); + expect(checkbox).toBeChecked(); + await user.click(screen.getByRole('button', { name: /sign in/i })); + + await waitFor(() => { + expect(capturedBody).toEqual(expect.objectContaining({ remember_me: true })); + }); + }); + }); + describe('FE-PAGE-LOGIN-005: Registration toggle visible', () => { it('shows a Register button to switch to registration mode', async () => { // Default appConfig has allow_registration: true, has_users: true diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx index 74e5bd1b..df74f226 100644 --- a/client/src/pages/LoginPage.tsx +++ b/client/src/pages/LoginPage.tsx @@ -9,7 +9,7 @@ export default function LoginPage(): React.ReactElement { const { navigate, mode, setMode, - username, setUsername, email, setEmail, password, setPassword, showPassword, setShowPassword, + username, setUsername, email, setEmail, password, setPassword, rememberMe, setRememberMe, showPassword, setShowPassword, isLoading, error, setError, appConfig, inviteToken, langDropdownOpen, setLangDropdownOpen, setLanguageLocal, showTakeoff, mfaStep, setMfaStep, mfaToken, setMfaToken, mfaCode, setMfaCode, @@ -572,7 +572,16 @@ export default function LoginPage(): React.ReactElement { {mode === 'login' && ( -
+
+