Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions portals/admin/src/main/webapp/site/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,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",
Expand Down
4 changes: 4 additions & 0 deletions portals/admin/src/main/webapp/site/public/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -200,6 +201,7 @@ class Protected extends Component {
<StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}>
<AppErrorBoundary>
<SessionTimeout />
{settings ? (
<AppContextProvider value={{ settings, user, isSuperTenant }}>
<Base header={header} leftMenu={leftMenu}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Copyright (c) 2025, 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 (
<div>
<ConfirmDialog
key='key-dialog'
labelCancel={(
<FormattedMessage
id='SessionTimeout.dialog.label.cancel'
defaultMessage='Logout'
/>
)}
title={(
<FormattedMessage
id='SessionTimeout.dialog.title'
defaultMessage='Are you still there?'
/>
)}
message={(
<FormattedMessage
id='SessionTimeout.dialog.message'
defaultMessage={
'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.'
}
values={{ time: remainingTime }} // Use remainingTime here
/>
)}
labelOk={(
<FormattedMessage
id='SessionTimeout.dialog.label.ok'
defaultMessage='Stay Logged In'
/>
)}
callback={handleConfirmDialog}
open={openDialog}
/>
</div>
);
};

export default injectIntl(SessionTimeout);
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2025, 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 {};
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" */));
Expand Down Expand Up @@ -71,6 +72,7 @@ function AppRouts(props) {

return (
<Suspense fallback={<Progress />}>
{isAuthenticated && <SessionTimeout />}
<Switch>
<Redirect exact from='/' to={getRedirectingPath(theme, portalMode)} />
<Route path='/home' component={Landing} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright (c) 2025, 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 (
<div>
<ConfirmDialog
key='key-dialog'
labelCancel={(
<FormattedMessage
id='SessionTimeout.dialog.label.cancel'
defaultMessage='Logout'
/>
)}
title={(
<FormattedMessage
id='SessionTimeout.dialog.title'
defaultMessage='Are you still there?'
/>
)}
message={(
<FormattedMessage
id='SessionTimeout.dialog.message'
defaultMessage={
'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.'
}
values={{ time: remainingTime }} // Use remainingTime here
/>
)}
labelOk={(
<FormattedMessage
id='SessionTimeout.dialog.label.ok'
defaultMessage='Stay Logged In'
/>
)}
callback={handleConfirmDialog}
open={openDialog}
/>
</div>
);
};

export default injectIntl(SessionTimeout);
Loading