Skip to content

Commit b880273

Browse files
Merge pull request #4439 from OneCommunityGlobal/Sanjeev_fix_force_logout_when_permissions_change
Sanjeev: Fix Force Logout When Permissions Change
2 parents ee20fb5 + 8be1ee7 commit b880273

6 files changed

Lines changed: 2988 additions & 3020 deletions

File tree

src/actions/__tests__/authActions.js.test.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
setCurrentUser, // Import setCurrentUser action
1414
setHeaderData, // Import setHeaderData action
1515
} from '../authActions'; // Import actions from authActions
16-
import { SET_CURRENT_USER, SET_HEADER_DATA } from '../../constants/auth'; // Import constants
16+
import { SET_CURRENT_USER, SET_HEADER_DATA, STOP_FORCE_LOGOUT } from '../../constants/auth'; // Import constants
1717

1818

1919
const middlewares = [thunk]; // Define middlewares
@@ -35,7 +35,10 @@ describe('authActions', () => {
3535
httpService.post.mockResolvedValue({ data: { token } }); // Mock the httpService post method
3636
jwtDecode.mockReturnValue(decodedToken); // Mock the jwtDecode function
3737

38-
const expectedActions = [{ type: SET_CURRENT_USER, payload: decodedToken }]; // Define expected actions
38+
const expectedActions = [
39+
{ type: STOP_FORCE_LOGOUT }, // Ensure any existing timers are cleared
40+
{ type: SET_CURRENT_USER, payload: decodedToken },
41+
]; // Define expected actions
3942

4043
await store.dispatch(loginUser(credentials)); // Dispatch the loginUser action
4144
expect(store.getActions()).toEqual(expectedActions); // Assert the actions
@@ -68,7 +71,10 @@ describe('authActions', () => {
6871

6972
it('creates SET_CURRENT_USER with null when logoutUser is called', () => {
7073
const store = mockStore({}); // Create a mock store
71-
const expectedActions = [{ type: SET_CURRENT_USER, payload: null }]; // Define expected actions
74+
const expectedActions = [
75+
{ type: STOP_FORCE_LOGOUT }, // Clear any active force-logout timer
76+
{ type: SET_CURRENT_USER, payload: null },
77+
]; // Define expected actions
7278

7379
store.dispatch(logoutUser()); // Dispatch the logoutUser action
7480
expect(store.getActions()).toEqual(expectedActions); // Assert the actions

src/actions/authActions.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
SET_CURRENT_USER,
99
SET_HEADER_DATA,
1010
START_FORCE_LOGOUT,
11+
STOP_FORCE_LOGOUT,
1112
} from '../constants/auth';
1213

1314
const { tokenKey } = config;
@@ -22,6 +23,21 @@ export const setHeaderData = data => ({
2223
payload: data,
2324
});
2425

26+
/**
27+
* Stops any active force logout timer and clears related state
28+
*/
29+
export const stopForceLogout = () => (dispatch, getState) => {
30+
const { auth } = getState();
31+
if (auth?.timerId) {
32+
try {
33+
clearTimeout(auth.timerId);
34+
} catch (e) {
35+
// Timer already cleared or invalid
36+
}
37+
}
38+
dispatch({ type: STOP_FORCE_LOGOUT });
39+
};
40+
2541
export const loginUser = credentials => dispatch => {
2642
return httpService
2743
.post(ENDPOINTS.LOGIN, credentials)
@@ -33,6 +49,8 @@ export const loginUser = credentials => dispatch => {
3349
localStorage.setItem(tokenKey, res.data.token);
3450
httpService.setjwt(res.data.token);
3551
const decoded = jwtDecode(res.data.token);
52+
// Ensure any existing timers from a previous session are cleared
53+
dispatch(stopForceLogout());
3654
dispatch(setCurrentUser(decoded));
3755
return { success: true };
3856
})
@@ -98,6 +116,8 @@ export const getHeaderData = userId => {
98116
};
99117

100118
export const logoutUser = () => dispatch => {
119+
// Clear any active force-logout timer before logging out
120+
dispatch(stopForceLogout());
101121
localStorage.removeItem(tokenKey);
102122
httpService.setjwt(false);
103123
dispatch(setCurrentUser(null));
@@ -134,6 +154,9 @@ export const startForceLogout = (delayMs = 20000) => (dispatch, getState) => {
134154
// eslint-disable-next-line no-console
135155
console.error('Error acknowledging permissions during force logout:', error);
136156
} finally {
157+
// Set flag to indicate user was force logged out due to permission changes
158+
// This helps distinguish "force logged out" vs "first login after permission change"
159+
sessionStorage.setItem('wasForceLoggedOut', 'true');
137160
dispatch(logoutUser());
138161
}
139162
}, delayMs);

src/components/Auth/PermissionWatcher.jsx

Lines changed: 125 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
22
import { useSelector, useDispatch } from 'react-redux';
33
import axios from 'axios';
44
import { ENDPOINTS } from '~/utils/URL';
5-
import { startForceLogout } from '../../actions/authActions';
5+
import { startForceLogout, stopForceLogout } from '../../actions/authActions';
66
import { useCountdown } from '../../hooks/useCountdown';
77
import PopUpBar from '../PopUpBar/PopUpBar';
88
import { getUserProfile } from '../../actions/userProfile';
@@ -11,27 +11,130 @@ function PermissionWatcher() {
1111
const dispatch = useDispatch();
1212
const { isAuthenticated, forceLogoutAt } = useSelector(state => state.auth || {});
1313
const userProfile = useSelector(state => state.userProfile);
14-
const isAcknowledged = userProfile?.permissions?.isAcknowledged !== false;
14+
const isAcknowledged = userProfile?.permissions?.isAcknowledged;
1515
const [isAckLoading, setIsAckLoading] = useState(false);
1616
// Get seconds remaining until force logout
1717
const secondsRemaining = useCountdown(forceLogoutAt);
18+
const [wasForceLoggedOut, setWasForceLoggedOut] = useState(false);
19+
const [flagReady, setFlagReady] = useState(false);
20+
// Track the initial acknowledged state when user first logs in
21+
const [initialAcknowledgedState, setInitialAcknowledgedState] = useState(null);
22+
// Track if user has just logged in (to distinguish from mid-session changes)
23+
const [isInitialLogin, setIsInitialLogin] = useState(false);
1824

19-
// Start the force logout countdown when conditions are met
25+
// On mount or when authentication changes, read flag from sessionStorage
2026
useEffect(() => {
21-
if (isAuthenticated && !isAcknowledged && !forceLogoutAt) {
22-
// eslint-disable-next-line no-console
23-
console.log('Starting force logout countdown due to unacknowledged permission changes');
24-
dispatch(startForceLogout(20000)); // 20 seconds countdown
27+
if (isAuthenticated) {
28+
try {
29+
const flag = sessionStorage.getItem('wasForceLoggedOut');
30+
setWasForceLoggedOut(flag === 'true');
31+
sessionStorage.removeItem('wasForceLoggedOut');
32+
} catch {
33+
// sessionStorage might not be available (private browsing, etc.)
34+
// Silently fail - component will work without the flag
35+
}
36+
37+
// Mark as initial login (initial state will be captured when profile loads)
38+
setIsInitialLogin(true);
39+
setInitialAcknowledgedState(null); // Reset to wait for profile load
40+
} else {
41+
// User logged out, reset state
42+
setIsInitialLogin(false);
43+
setInitialAcknowledgedState(null);
44+
setWasForceLoggedOut(false);
45+
}
46+
47+
setFlagReady(true);
48+
}, [isAuthenticated]);
49+
50+
// Track when user profile is first loaded after login and handle initial login cases
51+
useEffect(() => {
52+
if (!isAuthenticated || !flagReady) return;
53+
if (userProfile === null || userProfile === undefined) return; // Wait for profile to load
54+
if (!isInitialLogin) return; // Only handle initial login cases
55+
56+
// Capture the initial acknowledged state when profile is first loaded
57+
if (initialAcknowledgedState === null) {
58+
setInitialAcknowledgedState(isAcknowledged);
59+
return; // Wait for next render to check conditions
60+
}
61+
62+
// Edge Case 2: User permissions changed when logged out → show banner only on login
63+
// Detected by: user just logged in with unacknowledged permissions
64+
// AND was NOT force logged out (just normal logout with permission changes)
65+
const loggedInWithUnacknowledgedPermissions =
66+
!isAcknowledged && !forceLogoutAt && !wasForceLoggedOut;
67+
68+
if (loggedInWithUnacknowledgedPermissions) {
69+
setIsInitialLogin(false); // Mark as no longer initial login
70+
return;
71+
}
72+
73+
// Edge Case 3: User was force logged out → permissions change → user logs back in → show banner only
74+
// Detected by: user just logged in with unacknowledged permissions AND was force logged out
75+
const loggedInAfterForceLogout = !isAcknowledged && !forceLogoutAt && wasForceLoggedOut;
76+
77+
if (loggedInAfterForceLogout) {
78+
setIsInitialLogin(false); // Mark as no longer initial login
79+
return;
2580
}
26-
}, [isAuthenticated, isAcknowledged, forceLogoutAt, dispatch]);
81+
82+
// If initial login and permissions are acknowledged, mark as no longer initial
83+
if (isAcknowledged) {
84+
setIsInitialLogin(false);
85+
}
86+
}, [
87+
isAuthenticated,
88+
flagReady,
89+
isAcknowledged,
90+
forceLogoutAt,
91+
wasForceLoggedOut,
92+
dispatch,
93+
isInitialLogin,
94+
initialAcknowledgedState,
95+
userProfile,
96+
]);
97+
98+
// Handle mid-session permission changes (Edge Case 1)
99+
useEffect(() => {
100+
if (!isAuthenticated || !flagReady) return;
101+
if (userProfile === null || userProfile === undefined) return; // Wait for profile to load
102+
if (isInitialLogin) return; // Skip mid-session checks during initial login
103+
104+
// User permissions changed when logged in → start timer
105+
// Detected by: permissions were acknowledged (or was null/true), then became unacknowledged
106+
// AND user was already logged in (not initial login)
107+
const permissionsChangedMidSession =
108+
!isAcknowledged && !forceLogoutAt && initialAcknowledgedState !== false; // Was acknowledged or null before (not explicitly false)
109+
110+
if (permissionsChangedMidSession) {
111+
dispatch(startForceLogout(20000));
112+
return;
113+
}
114+
115+
// Case: permissions re-acknowledged → cancel timer
116+
if (isAcknowledged && forceLogoutAt) {
117+
dispatch(stopForceLogout());
118+
// Reset initial state since permissions are now acknowledged
119+
setInitialAcknowledgedState(true);
120+
}
121+
}, [
122+
isAuthenticated,
123+
flagReady,
124+
isAcknowledged,
125+
forceLogoutAt,
126+
dispatch,
127+
isInitialLogin,
128+
initialAcknowledgedState,
129+
userProfile,
130+
]);
131+
27132
// Handle acknowledgment of permission changes
28133
const handleAcknowledge = async () => {
29134
try {
30135
setIsAckLoading(true);
31136

32137
if (!userProfile || !userProfile._id) {
33-
// eslint-disable-next-line no-console
34-
//console.error('User profile not available');
35138
setIsAckLoading(false);
36139
return;
37140
}
@@ -49,6 +152,9 @@ function PermissionWatcher() {
49152
})
50153
.then(() => {
51154
setIsAckLoading(false);
155+
// Update initial state to reflect acknowledgment
156+
setInitialAcknowledgedState(true);
157+
setIsInitialLogin(false);
52158
dispatch(getUserProfile(_id));
53159
})
54160
.catch(error => {
@@ -63,21 +169,19 @@ function PermissionWatcher() {
63169
}
64170
};
65171

66-
// Only render the popup when a force logout is in progress
67-
if (!forceLogoutAt) {
68-
return null;
69-
}
70-
return (
71-
!isAcknowledged && (
172+
// Force logout timer running (mid-session permission change)
173+
if (forceLogoutAt && !isAcknowledged) {
174+
return (
72175
<PopUpBar
73-
message={`Permissions changed—logging out in ${secondsRemaining}s. Timer will be stopped; please restart after login.`}
176+
message={`Permissions changed—logging out in ${secondsRemaining}s unless acknowledged.`}
74177
onClickClose={handleAcknowledge}
75-
textColor="red"
76178
isLoading={isAckLoading}
77-
button={false}
179+
button
78180
/>
79-
)
80-
);
181+
);
182+
}
183+
184+
return null;
81185
}
82186

83187
export default PermissionWatcher;

src/constants/auth.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export const SET_CURRENT_USER = 'SET_CURRENT_USER';
22
export const SET_HEADER_DATA = 'SET_HEADER_DATA';
33
export const START_FORCE_LOGOUT = 'START_FORCE_LOGOUT';
4+
export const STOP_FORCE_LOGOUT = 'STOP_FORCE_LOGOUT';

src/reducers/authReducer.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { isEmpty } from 'lodash';
2-
import { SET_CURRENT_USER, SET_HEADER_DATA, START_FORCE_LOGOUT } from '../constants/auth';
2+
import {
3+
SET_CURRENT_USER,
4+
SET_HEADER_DATA,
5+
START_FORCE_LOGOUT,
6+
STOP_FORCE_LOGOUT,
7+
} from '../constants/auth';
38

49
const initialState = {
510
isAuthenticated: false,
@@ -47,6 +52,13 @@ export const authReducer = (auth = initialState, action) => {
4752
timerId: action.payload.timerId,
4853
};
4954
}
55+
if (action.type === STOP_FORCE_LOGOUT) {
56+
return {
57+
...auth,
58+
forceLogoutAt: null,
59+
timerId: null,
60+
};
61+
}
5062

5163
return auth;
5264
};

0 commit comments

Comments
 (0)