Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix redundant event listeners and session cleanup #488

Open
wants to merge 3 commits into
base: session-issues
Choose a base branch
from
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
43 changes: 26 additions & 17 deletions src/components/Auth/PrivateRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,21 @@ const PrivateRoute = ({ children }: { children?: React.ReactNode }): React.React
const [loading, setLoading] = useState(false);
const [tokenSentInSession, setTokenSentInSession,] = api.useClearOnClearSession(useSessionStorage('tokenSentInSession', null));
const [latestIsOnlineStatus, setLatestIsOnlineStatus,] = api.useClearOnClearSession(useSessionStorage('latestIsOnlineStatus', null));
const cachedUsers = keystore.getCachedUsers();
const changeIsLoggedIn = sessionStorage.getItem('changeIsLoggedIn') === 'false';

const location = useLocation();
const queryParams = new URLSearchParams(window.location.search);
const state = queryParams.get('state');

useEffect(() => {
// Detect if `latestIsLoggedIn` changes from true to false
if (changeIsLoggedIn === true && isLoggedIn === false) {
sessionStorage.setItem('changeIsLoggedIn', 'true');
} else if (isLoggedIn) {
sessionStorage.setItem('changeIsLoggedIn', 'false');
}
}, [isLoggedIn, changeIsLoggedIn]);

useEffect(() => {
const requestNotificationPermission = async () => {
if (!notificationApiIsSupported) {
Expand Down Expand Up @@ -230,25 +239,25 @@ const PrivateRoute = ({ children }: { children?: React.ReactNode }): React.React
setLatestIsOnlineStatus,
]);


const userExistsInCache = (state: string) => {
if (!state) return false;
try {
const decodedState = JSON.parse(atob(state));
return cachedUsers.some(user => user.userHandleB64u === decodedState.userHandleB64u);
} catch (error) {
console.error('Error decoding state:', error);
return false;
}
};

if (!isLoggedIn) {
const freshLogin = sessionStorage.getItem('freshLogin');
if (freshLogin) {
sessionStorage.removeItem('freshLogin');
window.history.replaceState(null, '', '/');
// If latestIsLoggedIn was true and isLoggedIn is now false, navigate directly to /login
if (changeIsLoggedIn) {
return <Navigate to="/login" replace />;
}

// Existing behavior: check state and user cache
const cachedUsers = keystore.getCachedUsers();
const userExistsInCache = (state: string) => {
if (!state) return false;
try {
const decodedState = JSON.parse(atob(state));
return cachedUsers.some(user => user.userHandleB64u === decodedState.userHandleB64u);
} catch (error) {
console.error('Error decoding state:', error);
return false;
}
};

if (state && userExistsInCache(state)) {
return <Navigate to="/login-state" state={{ from: location }} replace />;
} else {
Expand Down
24 changes: 17 additions & 7 deletions src/context/SessionContext.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { createContext, useContext } from 'react';
import React, { createContext, useContext, useEffect } from 'react';

import StatusContext from './StatusContext';
import { BackendApi, useApi } from '../api';
import { KeystoreEvent, useLocalStorageKeystore } from '../services/LocalStorageKeystore';
import type { LocalStorageKeystore } from '../services/LocalStorageKeystore';

import keystoreEvents from '../services/keystoreEvents';

type SessionContextValue = {
api: BackendApi,
Expand All @@ -23,17 +23,27 @@ const SessionContext: React.Context<SessionContextValue> = createContext({
export const SessionContextProvider = ({ children }) => {
const { isOnline } = useContext(StatusContext);
const api = useApi(isOnline);
const keystoreEvents = new EventTarget();
const keystore = useLocalStorageKeystore(keystoreEvents);

const logout = async () => {
// Clear URL parameters
sessionStorage.setItem('freshLogin', 'true');
const clearSession = async () => {
console.log('Clear Session')
api.clearSession();
};

const logout = async () => {
console.log('Logout')
await keystore.close();
};

keystoreEvents.addEventListener(KeystoreEvent.Close, logout, { once: true });
useEffect(() => {
// Add event listener
keystoreEvents.addEventListener(KeystoreEvent.CloseTabLocal, clearSession);

// Cleanup event listener to prevent duplicates
return () => {
keystoreEvents.removeEventListener(KeystoreEvent.CloseTabLocal, clearSession);
};
}, []);

const value: SessionContextValue = {
api,
Expand Down
44 changes: 28 additions & 16 deletions src/services/LocalStorageKeystore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ export type CachedUser = {
}

export enum KeystoreEvent {
/** The keystore has been closed. This event should be propagated to all tabs. */
Close = 'keystore.close',
/** The keystore has been closed. This event should not be propagated to other tabs. */
/** This event should be propagated to needed tabs which must clean SessionStorage. */
CloseTabLocal = 'keystore.closeTabLocal',
}

Expand Down Expand Up @@ -102,22 +100,23 @@ export function useLocalStorageKeystore(eventTarget: EventTarget): LocalStorageK
}, []));

const closeTabLocal = useCallback(
() => {
clearSessionStorage();
async (): Promise<void> => {
console.log('KeystoreEvent: closeTabLocal');
eventTarget.dispatchEvent(new CustomEvent(KeystoreEvent.CloseTabLocal));
console.log('KeystoreEvent.CloseTabLocal dispatched');
clearSessionStorage();
},
[clearSessionStorage],
[clearSessionStorage, eventTarget],
);

const close = useCallback(
async (): Promise<void> => {
console.log('Close');
await idb.destroy();
clearPrivateData();
clearGlobalUserHandleB64u();
closeTabLocal();
eventTarget.dispatchEvent(new CustomEvent(KeystoreEvent.Close));
},
[closeTabLocal, idb, clearGlobalUserHandleB64u, clearPrivateData],
[idb, clearGlobalUserHandleB64u, clearPrivateData],
);

useOnUserInactivity(close, config.INACTIVE_LOGOUT_MILLIS);
Expand All @@ -141,18 +140,31 @@ export function useLocalStorageKeystore(eventTarget: EventTarget): LocalStorageK
return cu;
}
}));

} else if (!privateData) {
// When user logs out in any tab, log out in all tabs
closeTabLocal();

} else if (userHandleB64u && globalUserHandleB64u && (userHandleB64u !== globalUserHandleB64u)) {
}
},
[closeTabLocal, privateData, userHandleB64u, globalUserHandleB64u, setCachedUsers],
);
useEffect(
() => {
if (userHandleB64u && globalUserHandleB64u && (userHandleB64u !== globalUserHandleB64u)) {
// When user logs in in any tab, log out in all other tabs
// that are logged in to a different account
console.log('closeTabLocal by globalUserHandleB64u')
closeTabLocal();
}
},
[closeTabLocal, userHandleB64u, globalUserHandleB64u, setCachedUsers],
);

useEffect(
() => {
if (!privateData) {
// When user logs out in any tab, log out in all tabs
console.log('closeTabLocal by privateData')
closeTabLocal();
}
},
[close, closeTabLocal, privateData, userHandleB64u, globalUserHandleB64u, setCachedUsers],
[closeTabLocal, privateData],
);

const openPrivateData = async (): Promise<[PrivateData, CryptoKey]> => {
Expand Down
3 changes: 3 additions & 0 deletions src/services/keystoreEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// keystoreEvents.js
const keystoreEvents = new EventTarget();
export default keystoreEvents;
Loading