diff --git a/portals/admin/src/main/webapp/site/public/conf/settings.json b/portals/admin/src/main/webapp/site/public/conf/settings.json index 8a27f1ecd6e..86cd0a550f0 100644 --- a/portals/admin/src/main/webapp/site/public/conf/settings.json +++ b/portals/admin/src/main/webapp/site/public/conf/settings.json @@ -21,5 +21,10 @@ }, "logoutSessionStateAppender" : "" , "docUrl": "https://apim.docs.wso2.com/en/4.7.0/" + }, + "sessionTimeout": { + "enable": true, + "idleWarningTimeout": 160, + "idleTimeout": 180 } } diff --git a/portals/admin/src/main/webapp/site/public/locales/en.json b/portals/admin/src/main/webapp/site/public/locales/en.json index eaca4097854..c4f0357e87d 100644 --- a/portals/admin/src/main/webapp/site/public/locales/en.json +++ b/portals/admin/src/main/webapp/site/public/locales/en.json @@ -1007,6 +1007,10 @@ "RolePermissions.TreeView.PermissionsSelector.update.scope.error": "Something went wrong while updating the permission", "RolePermissions.TreeView.PermissionsSelector.update.scope.success": "Update permissions for {role} successfully", "ScopeAssignments.List.search.default": "Search by Role Name", + "SessionTimeout.dialog.label.cancel": "Logout", + "SessionTimeout.dialog.label.ok": "Stay Logged In", + "SessionTimeout.dialog.message": "Your session is about to expire due to inactivity. To keep your session active, click \"Stay Logged In\". If no action is taken, you will be logged out automatically in {time} seconds, for security reasons.", + "SessionTimeout.dialog.title": "Are you still there?", "Settings.Advanced.TenantConf.edit.success": "Advanced Configuration saved successfully", "Settings.Advanced.TenantConfSave.form.cancel": "Cancel", "Settings.Advanced.TenantConfSave.form.save": "Save", diff --git a/portals/admin/src/main/webapp/site/public/locales/fr.json b/portals/admin/src/main/webapp/site/public/locales/fr.json index 449206f7c98..429f64423fb 100644 --- a/portals/admin/src/main/webapp/site/public/locales/fr.json +++ b/portals/admin/src/main/webapp/site/public/locales/fr.json @@ -1007,6 +1007,10 @@ "RolePermissions.TreeView.PermissionsSelector.update.scope.error": "Something went wrong while updating the permission", "RolePermissions.TreeView.PermissionsSelector.update.scope.success": "Update permissions for {role} successfully", "ScopeAssignments.List.search.default": "Search by Role Name", + "SessionTimeout.dialog.label.cancel": "Logout", + "SessionTimeout.dialog.label.ok": "Stay Logged In", + "SessionTimeout.dialog.message": "Your session is about to expire due to inactivity. To keep your session active, click \"Stay Logged In\". If no action is taken, you will be logged out automatically in {time} seconds, for security reasons.", + "SessionTimeout.dialog.title": "Are you still there?", "Settings.Advanced.TenantConf.edit.success": "Advanced Configuration saved successfully", "Settings.Advanced.TenantConfSave.form.cancel": "Cancel", "Settings.Advanced.TenantConfSave.form.save": "Save", diff --git a/portals/admin/src/main/webapp/source/src/app/ProtectedApp.jsx b/portals/admin/src/main/webapp/source/src/app/ProtectedApp.jsx index 0f37a545529..277cee9cd1e 100644 --- a/portals/admin/src/main/webapp/source/src/app/ProtectedApp.jsx +++ b/portals/admin/src/main/webapp/source/src/app/ProtectedApp.jsx @@ -29,6 +29,7 @@ import { import Hidden from '@mui/material/Hidden'; import Configurations from 'Config'; import Themes from 'Themes'; +import SessionTimeout from 'AppComponents/SessionTimeout'; import ResourceNotFound from './components/Base/Errors/ResourceNotFound'; import User from './data/User'; import Utils from './data/Utils'; @@ -200,6 +201,7 @@ class Protected extends Component { + {settings ? ( diff --git a/portals/admin/src/main/webapp/source/src/app/components/SessionTimeout.jsx b/portals/admin/src/main/webapp/source/src/app/components/SessionTimeout.jsx new file mode 100644 index 00000000000..49e8a0d89d1 --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/components/SessionTimeout.jsx @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS + * OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState, useEffect, useRef } from 'react'; +import { injectIntl, FormattedMessage } from 'react-intl'; +import Configurations from 'Config'; +import ConfirmDialog from 'AppComponents/Shared/ConfirmDialog'; + +const SessionTimeout = () => { + const [openDialog, setOpenDialog] = useState(false); + const [remainingTime, setRemainingTime] = useState(0); // To track remaining time + const openDialogRef = useRef(openDialog); // Create a ref to track openDialog state + + // Use refs to hold values that are mutated from closures and should persist + const idleTimeoutRef = useRef(0); + const idleWarningTimeoutRef = useRef(0); + const idleSecondsCounterRef = useRef(0); + + const handleTimeOut = (idleSecondsCount) => { + // Only update the remaining time if the warning timeout is reached + if (idleSecondsCount >= idleWarningTimeoutRef.current) { + setRemainingTime(idleTimeoutRef.current - idleSecondsCount); // Update remaining time + } + if (idleSecondsCount === idleWarningTimeoutRef.current) { + setOpenDialog(true); // Open dialog when warning timeout is reached + } + if (idleSecondsCount === idleTimeoutRef.current) { + // Logout if the idle timeout is reached + setOpenDialog(false); // Close dialog if it was open + window.location = Configurations.app.context + '/services/logout'; + } + }; + + useEffect(() => { + openDialogRef.current = openDialog; // Update the ref whenever openDialog changes + }, [openDialog]); + + useEffect(() => { + if (!(Configurations.sessionTimeout && Configurations.sessionTimeout.enable)) { + return () => { }; + } + + idleTimeoutRef.current = Configurations.sessionTimeout.idleTimeout; + idleWarningTimeoutRef.current = Configurations.sessionTimeout.idleWarningTimeout; + + const resetIdleTimer = () => { + if (!openDialogRef.current) { + idleSecondsCounterRef.current = 0; + } + }; + + document.addEventListener('click', resetIdleTimer); + document.addEventListener('mousemove', resetIdleTimer); + document.addEventListener('keydown', resetIdleTimer); + + const worker = new Worker(new URL('../webWorkers/timer.worker.js', import.meta.url)); + worker.onmessage = () => { + // increment the ref and pass the new value + idleSecondsCounterRef.current += 1; + handleTimeOut(idleSecondsCounterRef.current); + }; + + // Cleanup function to remove event listeners and terminate the worker + return () => { + document.removeEventListener('click', resetIdleTimer); + document.removeEventListener('mousemove', resetIdleTimer); + document.removeEventListener('keydown', resetIdleTimer); + worker.terminate(); + }; + }, []); + + const handleConfirmDialog = (res) => { + if (res) { + setOpenDialog(false); + idleSecondsCounterRef.current = 0; // Reset the idle timer stored in ref + } else { + window.location = Configurations.app.context + '/services/logout'; + } + }; + + return ( +
+ + )} + title={( + + )} + message={( + + )} + labelOk={( + + )} + callback={handleConfirmDialog} + open={openDialog} + /> +
+ ); +}; + +export default injectIntl(SessionTimeout); diff --git a/portals/admin/src/main/webapp/source/src/app/webWorkers/timer.worker.js b/portals/admin/src/main/webapp/source/src/app/webWorkers/timer.worker.js new file mode 100644 index 00000000000..81cc929b61b --- /dev/null +++ b/portals/admin/src/main/webapp/source/src/app/webWorkers/timer.worker.js @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// This is a web worker that will be used to run the timer. +// This timer will be used to execute the session timeout +// functionality in the background without blocking the main thread. + +// This is the timer interval in milliseconds. +const TIMER_INTERVAL = 1000; + +// This is the timer function that will be executed in the background. +setInterval(() => { + // disable following rule because linter is unaware of the worker source + // eslint-disable-next-line no-restricted-globals + self.postMessage(''); +}, TIMER_INTERVAL); + +export default {}; diff --git a/portals/devportal/src/main/webapp/site/public/locales/en.json b/portals/devportal/src/main/webapp/site/public/locales/en.json index 9bc9ee4ce3a..322459dcff6 100644 --- a/portals/devportal/src/main/webapp/site/public/locales/en.json +++ b/portals/devportal/src/main/webapp/site/public/locales/en.json @@ -721,6 +721,10 @@ "LoginDenied.message": "You don't have sufficient privileges to access the Developer Portal.", "LoginDenied.title": "Error 403 : Forbidden", "Pgb3Xj": "Subscribed", + "SessionTimeout.dialog.label.cancel": "Logout", + "SessionTimeout.dialog.label.ok": "Stay Logged In", + "SessionTimeout.dialog.message": "Your session is about to expire due to inactivity. To keep your session active, click \"Stay Logged In\". If no action is taken, you will be logged out automatically in {time} seconds, for security reasons.", + "SessionTimeout.dialog.title": "Are you still there?", "Settings.ChangePasswordForm.Cancel.Button.text": "Cancel", "Settings.ChangePasswordForm.Save.Button.text": "Save", "Settings.ChangePasswordForm.confirm.new.password": "Confirm new Password", diff --git a/portals/devportal/src/main/webapp/site/public/theme/settings.json b/portals/devportal/src/main/webapp/site/public/theme/settings.json index c94dd913c89..2f78c1800a1 100644 --- a/portals/devportal/src/main/webapp/site/public/theme/settings.json +++ b/portals/devportal/src/main/webapp/site/public/theme/settings.json @@ -59,5 +59,10 @@ "showBusinessDetails": false, "showTechnicalDetails": false } + }, + "sessionTimeout": { + "enable": true, + "idleWarningTimeout": 160, + "idleTimeout": 180 } } diff --git a/portals/devportal/src/main/webapp/source/src/app/AppRouts.jsx b/portals/devportal/src/main/webapp/source/src/app/AppRouts.jsx index fdc1587f960..f5e5e4a5454 100644 --- a/portals/devportal/src/main/webapp/source/src/app/AppRouts.jsx +++ b/portals/devportal/src/main/webapp/source/src/app/AppRouts.jsx @@ -26,6 +26,7 @@ import RedirectToLogin from 'AppComponents/Login/RedirectToLogin'; import Progress from 'AppComponents/Shared/Progress'; import PortalModeRouteGuard from 'AppComponents/Shared/PortalModeRouteGuard'; import { useTheme } from '@mui/material'; +import SessionTimeout from 'AppComponents/SessionTimeout'; import { usePortalMode, PORTAL_MODES } from './utils/PortalModeUtils'; const Apis = lazy(() => import('AppComponents/Apis/Apis' /* webpackChunkName: "Apis" */)); @@ -71,6 +72,7 @@ function AppRouts(props) { return ( }> + {isAuthenticated && } diff --git a/portals/devportal/src/main/webapp/source/src/app/components/SessionTimeout.jsx b/portals/devportal/src/main/webapp/source/src/app/components/SessionTimeout.jsx new file mode 100644 index 00000000000..9ce39de5c8c --- /dev/null +++ b/portals/devportal/src/main/webapp/source/src/app/components/SessionTimeout.jsx @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS + * OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState, useEffect, useRef } from 'react'; +import { injectIntl, FormattedMessage } from 'react-intl'; +import Settings from 'Settings'; +import ConfirmDialog from 'AppComponents/Shared/ConfirmDialog'; + +const SessionTimeout = () => { + const [openDialog, setOpenDialog] = useState(false); + const [remainingTime, setRemainingTime] = useState(0); // To track remaining time + const openDialogRef = useRef(openDialog); // Create a ref to track openDialog state + + // Use refs to hold values that are mutated from closures and should persist + const idleTimeoutRef = useRef(0); + const idleWarningTimeoutRef = useRef(0); + const idleSecondsCounterRef = useRef(0); + + const handleTimeOut = (idleSecondsCount) => { + // Only update the remaining time if the warning timeout is reached + if (idleSecondsCount >= idleWarningTimeoutRef.current) { + setRemainingTime(idleTimeoutRef.current - idleSecondsCount); // Update remaining time + } + if (idleSecondsCount === idleWarningTimeoutRef.current) { + setOpenDialog(true); // Open dialog when warning timeout is reached + } + if (idleSecondsCount === idleTimeoutRef.current) { + // Logout if the idle timeout is reached + setOpenDialog(false); // Close dialog if it was open + window.location = Settings.app.context + '/services/logout'; + } + }; + + useEffect(() => { + openDialogRef.current = openDialog; // Update the ref whenever openDialog changes + }, [openDialog]); + + useEffect(() => { + if (!(Settings.sessionTimeout && Settings.sessionTimeout.enable)) { + return () => { }; + } + + idleTimeoutRef.current = Settings.sessionTimeout.idleTimeout; + idleWarningTimeoutRef.current = Settings.sessionTimeout.idleWarningTimeout; + + const resetIdleTimer = () => { + if (!openDialogRef.current) { + idleSecondsCounterRef.current = 0; + } + }; + + document.addEventListener('click', resetIdleTimer); + document.addEventListener('mousemove', resetIdleTimer); + document.addEventListener('keydown', resetIdleTimer); + + const worker = new Worker(new URL('../webWorkers/timer.worker.js', import.meta.url)); + worker.onmessage = () => { + // increment the ref and pass the new value + idleSecondsCounterRef.current += 1; + handleTimeOut(idleSecondsCounterRef.current); + }; + + // Cleanup function to remove event listeners and terminate the worker + return () => { + document.removeEventListener('click', resetIdleTimer); + document.removeEventListener('mousemove', resetIdleTimer); + document.removeEventListener('keydown', resetIdleTimer); + worker.terminate(); + }; + }, []); + + const handleConfirmDialog = (res) => { + if (res) { + setOpenDialog(false); + idleSecondsCounterRef.current = 0; // Reset the idle timer stored in ref + } else { + window.location = Settings.app.context + '/services/logout'; + } + }; + + return ( +
+ + )} + title={( + + )} + message={( + + )} + labelOk={( + + )} + callback={handleConfirmDialog} + open={openDialog} + /> +
+ ); +}; + +export default injectIntl(SessionTimeout); diff --git a/portals/devportal/src/main/webapp/source/src/app/webWorkers/timer.worker.js b/portals/devportal/src/main/webapp/source/src/app/webWorkers/timer.worker.js new file mode 100644 index 00000000000..81cc929b61b --- /dev/null +++ b/portals/devportal/src/main/webapp/source/src/app/webWorkers/timer.worker.js @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// This is a web worker that will be used to run the timer. +// This timer will be used to execute the session timeout +// functionality in the background without blocking the main thread. + +// This is the timer interval in milliseconds. +const TIMER_INTERVAL = 1000; + +// This is the timer function that will be executed in the background. +setInterval(() => { + // disable following rule because linter is unaware of the worker source + // eslint-disable-next-line no-restricted-globals + self.postMessage(''); +}, TIMER_INTERVAL); + +export default {}; diff --git a/portals/publisher/src/main/webapp/site/public/conf/settings.json b/portals/publisher/src/main/webapp/site/public/conf/settings.json index bf81d9f41b4..c7501ad746a 100644 --- a/portals/publisher/src/main/webapp/site/public/conf/settings.json +++ b/portals/publisher/src/main/webapp/site/public/conf/settings.json @@ -1,69 +1,69 @@ { - "app": { - "context": "/publisher", - "customUrl": { - "enabled": false, - "forwardedHeader": "X-Forwarded-For" - }, - "origin": { - "host": "localhost" - }, - "feedback": { - "enable": false, - "serviceURL": "" - }, - "singleLogout": { - "enabled": true, - "timeout": 4000 - }, - "logoutSessionStateAppender" : "" , - "throttlingPolicyLimit": 80, - "operationPolicyCount": 500, - "documentCount": 1000, - "propertyDisplaySuffix": "__display", - "markdown": { - "skipHtml": true, - "syntaxHighlighterProps": { - "showLineNumbers": false - }, - "syntaxHighlighterDarkTheme": false, - "template": { - "howTo": "", - "sample": "", - "other": "" - } - }, - "reactHTMLParser": { - "decodeEntries": true, - "tagsNotAllowed": [] - }, - "workflows": { - "limit": 30 - }, - "loadDefaultLocales": false, - "supportedDocTypes": "application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/msword, application/pdf, text/plain, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.oasis.opendocument.text, application/vnd.oasis.opendocument.spreadsheet, application/json, application/x-yaml, .yaml, .md", - "docUrl": "https://apim.docs.wso2.com/en/4.7.0/", - "environmentsPolling": { - "maxDurationMs": 120000, - "intervalMs": 3000 - } + "app": { + "context": "/publisher", + "customUrl": { + "enabled": false, + "forwardedHeader": "X-Forwarded-For" + }, + "origin": { + "host": "localhost" + }, + "feedback": { + "enable": false, + "serviceURL": "" + }, + "singleLogout": { + "enabled": true, + "timeout": 4000 + }, + "logoutSessionStateAppender": "", + "throttlingPolicyLimit": 80, + "operationPolicyCount": 500, + "documentCount": 1000, + "propertyDisplaySuffix": "__display", + "markdown": { + "skipHtml": true, + "syntaxHighlighterProps": { + "showLineNumbers": false + }, + "syntaxHighlighterDarkTheme": false, + "template": { + "howTo": "", + "sample": "", + "other": "" + } }, - "serviceCatalogDefinitionTypes": { - "OAS2": "Swagger", - "OAS3": "Open API V3", - "WSDL1": "WSDL 1", - "WSDL2": "WSDL 2", - "GRAPHQL_SDL": "GraphQL SDL", - "ASYNC_API": "AsyncAPI" + "reactHTMLParser": { + "decodeEntries": true, + "tagsNotAllowed": [] }, - "serviceCatalogSecurityTypes": { - "BASIC": "Basic", - "DIGEST": "Digest", - "OAUTH2": "OAuth2", - "NONE": "None", - "X509": "X509", - "API_KEY": "API Key" + "workflows": { + "limit": 30 }, + "loadDefaultLocales": false, + "supportedDocTypes": "application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/msword, application/pdf, text/plain, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.oasis.opendocument.text, application/vnd.oasis.opendocument.spreadsheet, application/json, application/x-yaml, .yaml, .md", + "docUrl": "https://apim.docs.wso2.com/en/4.7.0/", + "environmentsPolling": { + "maxDurationMs": 120000, + "intervalMs": 3000 + } + }, + "serviceCatalogDefinitionTypes": { + "OAS2": "Swagger", + "OAS3": "Open API V3", + "WSDL1": "WSDL 1", + "WSDL2": "WSDL 2", + "GRAPHQL_SDL": "GraphQL SDL", + "ASYNC_API": "AsyncAPI" + }, + "serviceCatalogSecurityTypes": { + "BASIC": "Basic", + "DIGEST": "Digest", + "OAUTH2": "OAuth2", + "NONE": "None", + "X509": "X509", + "API_KEY": "API Key" + }, "apis": { "alwaysShowDeploySampleButton": true, "showMultiVersionPolicies": false, @@ -101,6 +101,10 @@ }, "maxSubscriptionLimit": 2000, "maxScopeCount": 2000 + }, + "sessionTimeout": { + "enable": true, + "idleWarningTimeout": 160, + "idleTimeout": 180 } } - diff --git a/portals/publisher/src/main/webapp/site/public/locales/en.json b/portals/publisher/src/main/webapp/site/public/locales/en.json index ed5b38c200e..507f24e58ae 100644 --- a/portals/publisher/src/main/webapp/site/public/locales/en.json +++ b/portals/publisher/src/main/webapp/site/public/locales/en.json @@ -2529,6 +2529,10 @@ "ServiceCatalog.ServicesTableView.ServicesTableView.service.url": "Service URL", "ServiceCatalog.ServicesTableView.ServicesTableView.usage": "Number of Usages", "ServiceCatalog.ServicesTableView.ServicesTableView.version": "Version", + "SessionTimeout.dialog.label.cancel": "Logout", + "SessionTimeout.dialog.label.ok": "Stay Logged In", + "SessionTimeout.dialog.message": "Your session is about to expire due to inactivity. To keep your session active, click “Stay Logged In”. If no action is taken, you will be logged out automatically in {time} seconds, for security reasons.", + "SessionTimeout.dialog.title": "Are you still there?", "Subscription.SubscriptionCreation.SubscriptionCreation.subscription.creation.search.default": "Search by API Name, Application Name, Subscriber etc", "Subscription.SubscriptionCreation.SubscriptionCreation.subscription.creation.title": "Subscription Creation - Approval Tasks", "Subscription.SubscriptionUpdate.SubscriptionUpdate.subscription.update.search.default": "Search by API Name, Application Name, Subscriber etc", diff --git a/portals/publisher/src/main/webapp/source/src/app/ProtectedApp.jsx b/portals/publisher/src/main/webapp/source/src/app/ProtectedApp.jsx index a815ae534e5..b06409b30b3 100644 --- a/portals/publisher/src/main/webapp/source/src/app/ProtectedApp.jsx +++ b/portals/publisher/src/main/webapp/source/src/app/ProtectedApp.jsx @@ -44,6 +44,7 @@ import Scopes from 'AppComponents/Scopes/Scopes'; import Subscription from 'AppComponents/Subscription/Subscription'; import CommonPolicies from 'AppComponents/CommonPolicies/CommonPolicies'; import GlobalPolicies from 'AppComponents/GlobalPolicies/GlobalPolicies'; +import SessionTimeout from 'AppComponents/SessionTimeout'; import merge from 'lodash/merge'; import User from './data/User'; import Utils from './data/Utils'; @@ -212,6 +213,7 @@ export default class Protected extends Component { + { + const [openDialog, setOpenDialog] = useState(false); + const [remainingTime, setRemainingTime] = useState(0); // To track remaining time + const openDialogRef = useRef(openDialog); // Create a ref to track openDialog state + + // Use refs to hold values that are mutated from closures and should persist + const idleTimeoutRef = useRef(0); + const idleWarningTimeoutRef = useRef(0); + const idleSecondsCounterRef = useRef(0); + + const handleTimeOut = (idleSecondsCount) => { + // Only update the remaining time if the warning timeout is reached + if (idleSecondsCount >= idleWarningTimeoutRef.current) { + setRemainingTime(idleTimeoutRef.current - idleSecondsCount); // Update remaining time + } + if (idleSecondsCount === idleWarningTimeoutRef.current) { + setOpenDialog(true); // Open dialog when warning timeout is reached + } + if (idleSecondsCount === idleTimeoutRef.current) { + // Logout if the idle timeout is reached + setOpenDialog(false); // Close dialog if it was open + window.location = Configurations.app.context + '/services/logout'; + } + }; + + useEffect(() => { + openDialogRef.current = openDialog; // Update the ref whenever openDialog changes + }, [openDialog]); + + useEffect(() => { + if (!(Configurations.sessionTimeout && Configurations.sessionTimeout.enable)) { + return () => { }; + } + + idleTimeoutRef.current = Configurations.sessionTimeout.idleTimeout; + idleWarningTimeoutRef.current = Configurations.sessionTimeout.idleWarningTimeout; + + const resetIdleTimer = () => { + if (!openDialogRef.current) { + idleSecondsCounterRef.current = 0; + } + }; + + document.addEventListener('click', resetIdleTimer); + document.addEventListener('mousemove', resetIdleTimer); + document.addEventListener('keydown', resetIdleTimer); + + const worker = new Worker(new URL('../webWorkers/timer.worker.js', import.meta.url)); + worker.onmessage = () => { + // increment the ref and pass the new value + idleSecondsCounterRef.current += 1; + handleTimeOut(idleSecondsCounterRef.current); + }; + + // Cleanup function to remove event listeners and terminate the worker + return () => { + document.removeEventListener('click', resetIdleTimer); + document.removeEventListener('mousemove', resetIdleTimer); + document.removeEventListener('keydown', resetIdleTimer); + worker.terminate(); + }; + }, []); + + const handleConfirmDialog = (res) => { + if (res) { + setOpenDialog(false); + idleSecondsCounterRef.current = 0; // Reset the idle timer stored in ref + } else { + window.location = Configurations.app.context + '/services/logout'; + } + }; + + return ( +
+ } + title={} + message={ + + } + labelOk={} + callback={handleConfirmDialog} + open={openDialog} + confirmPrimary + /> +
+ ); +}; + +export default injectIntl(SessionTimeout); diff --git a/portals/publisher/src/main/webapp/source/src/app/webWorkers/timer.worker.js b/portals/publisher/src/main/webapp/source/src/app/webWorkers/timer.worker.js new file mode 100644 index 00000000000..81cc929b61b --- /dev/null +++ b/portals/publisher/src/main/webapp/source/src/app/webWorkers/timer.worker.js @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// This is a web worker that will be used to run the timer. +// This timer will be used to execute the session timeout +// functionality in the background without blocking the main thread. + +// This is the timer interval in milliseconds. +const TIMER_INTERVAL = 1000; + +// This is the timer function that will be executed in the background. +setInterval(() => { + // disable following rule because linter is unaware of the worker source + // eslint-disable-next-line no-restricted-globals + self.postMessage(''); +}, TIMER_INTERVAL); + +export default {};