From 3ef87a8603e69da27d24e6227c1ed2e0a44070d4 Mon Sep 17 00:00:00 2001 From: Daniel Hiller Date: Sat, 8 Aug 2026 00:24:16 +0200 Subject: [PATCH] feat: Add configurable auto logout on inactivity Users can pick an inactivity timeout in their preferences. A warning appears 30 seconds before, and activity or a logout is synchronised across open tabs. --- client/src/actions/core.js | 14 + .../AutoLogoutWarningModal.jsx | 71 ++++++ .../common/AutoLogoutWarningModal/index.js | 8 + client/src/components/common/Core/Core.jsx | 2 + .../UserSettingsModal/PreferencesPane.jsx | 57 ++++- .../PreferencesPane.module.scss | 16 ++ client/src/constants/ActionTypes.js | 2 + client/src/constants/EntryActionTypes.js | 1 + client/src/constants/Enums.js | 9 + client/src/entry-actions/core.js | 6 + client/src/locales/de-DE/core.js | 11 + client/src/locales/en-US/core.js | 11 + client/src/models/User.js | 1 + client/src/reducers/core.js | 13 + client/src/sagas/core/index.js | 1 + client/src/sagas/core/services/auto-logout.js | 240 ++++++++++++++++++ client/src/sagas/core/services/index.js | 2 + client/src/selectors/core.js | 3 + client/src/utils/auto-logout-channel.js | 90 +++++++ server/api/controllers/users/update.js | 10 + server/api/models/User.js | 23 ++ ...260808120000_add_auto_logout_preference.js | 14 + 22 files changed, 603 insertions(+), 2 deletions(-) create mode 100644 client/src/components/common/AutoLogoutWarningModal/AutoLogoutWarningModal.jsx create mode 100644 client/src/components/common/AutoLogoutWarningModal/index.js create mode 100644 client/src/sagas/core/services/auto-logout.js create mode 100644 client/src/utils/auto-logout-channel.js create mode 100644 server/db/migrations/20260808120000_add_auto_logout_preference.js diff --git a/client/src/actions/core.js b/client/src/actions/core.js index 85182ffd..2ae879ba 100644 --- a/client/src/actions/core.js +++ b/client/src/actions/core.js @@ -98,10 +98,24 @@ logout.revokeAccessToken = () => ({ payload: {}, }); +const showAutoLogoutWarning = (expiresAt) => ({ + type: ActionTypes.AUTO_LOGOUT_WARNING_SHOW, + payload: { + expiresAt, + }, +}); + +const dismissAutoLogoutWarning = () => ({ + type: ActionTypes.AUTO_LOGOUT_WARNING_DISMISS, + payload: {}, +}); + export default { initializeCore, toggleFavorites, toggleEditMode, updateHomeView, logout, + showAutoLogoutWarning, + dismissAutoLogoutWarning, }; diff --git a/client/src/components/common/AutoLogoutWarningModal/AutoLogoutWarningModal.jsx b/client/src/components/common/AutoLogoutWarningModal/AutoLogoutWarningModal.jsx new file mode 100644 index 00000000..a9807cd7 --- /dev/null +++ b/client/src/components/common/AutoLogoutWarningModal/AutoLogoutWarningModal.jsx @@ -0,0 +1,71 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +import React, { useCallback, useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import { Button, Modal } from 'semantic-ui-react'; + +import selectors from '../../../selectors'; +import entryActions from '../../../entry-actions'; + +const AutoLogoutWarningModal = React.memo(() => { + const warning = useSelector(selectors.selectAutoLogoutWarning); + const dispatch = useDispatch(); + const [t] = useTranslation(); + + const computeRemaining = useCallback( + () => (warning ? Math.max(0, Math.ceil((warning.expiresAt - Date.now()) / 1000)) : 0), + [warning], + ); + + const [remainingSeconds, setRemainingSeconds] = useState(computeRemaining); + + useEffect(() => { + if (!warning) { + return undefined; + } + + setRemainingSeconds(computeRemaining()); + + const interval = setInterval(() => { + setRemainingSeconds(computeRemaining()); + }, 500); + + return () => clearInterval(interval); + }, [warning, computeRemaining]); + + const handleStayClick = useCallback(() => { + dispatch(entryActions.dismissAutoLogoutWarning()); + }, [dispatch]); + + const handleLogoutClick = useCallback(() => { + dispatch(entryActions.logout()); + }, [dispatch]); + + if (!warning) { + return null; + } + + return ( + + {t('common.autoLogoutWarning_title')} + +

{t('common.autoLogoutWarning_body', { seconds: remainingSeconds })}

+
+ + +