Merge pull request #31466 from jescalada/31624-fix-autologout-bug
fix: auto-logout not working when closing tabs and reopening
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
Fix autologout not working correctly when closing all tabs
|
||||
@@ -114,6 +114,7 @@ const ConditionalAutoLogout = ({
|
||||
// Events will be rebound as long as `stopOnMount` is not set.
|
||||
setPromptOpen(false);
|
||||
setRemainingTimeCountdown(0);
|
||||
lastSeenOnlineStore.delete();
|
||||
identityApi.signOut();
|
||||
};
|
||||
|
||||
@@ -231,18 +232,37 @@ const parseConfig = (
|
||||
export const AutoLogout = (props: AutoLogoutProps): JSX.Element | null => {
|
||||
const identityApi = useApi(identityApiRef);
|
||||
const configApi = useApi(configApiRef);
|
||||
const [isLogged, setIsLogged] = useState(false);
|
||||
const [isLogged, setIsLogged] = useState<boolean | null>(null);
|
||||
const lastSeenOnlineStore: TimestampStore = useMemo(
|
||||
() => new DefaultTimestampStore(LAST_SEEN_ONLINE_STORAGE_KEY),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// if the user is not logged in, the autologout feature won't affect the app even if enabled
|
||||
async function isLoggedIn(identity: IdentityApi) {
|
||||
if ((await identity.getCredentials()).token) {
|
||||
setIsLogged(true);
|
||||
} else {
|
||||
let cancelled = false;
|
||||
|
||||
async function checkLogin(identity: IdentityApi) {
|
||||
try {
|
||||
const creds = await identity.getCredentials();
|
||||
if (cancelled) return;
|
||||
if (creds?.token) {
|
||||
setIsLogged(true);
|
||||
} else {
|
||||
setIsLogged(false);
|
||||
lastSeenOnlineStore.delete();
|
||||
}
|
||||
} catch (err) {
|
||||
if (cancelled) return;
|
||||
setIsLogged(false);
|
||||
lastSeenOnlineStore.delete();
|
||||
}
|
||||
}
|
||||
isLoggedIn(identityApi);
|
||||
}, [identityApi]);
|
||||
checkLogin(identityApi);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [lastSeenOnlineStore, identityApi]);
|
||||
|
||||
const {
|
||||
enabled,
|
||||
@@ -274,10 +294,6 @@ export const AutoLogout = (props: AutoLogoutProps): JSX.Element | null => {
|
||||
}
|
||||
}, [idleTimeoutMinutes, promptBeforeIdleSeconds]);
|
||||
|
||||
const lastSeenOnlineStore: TimestampStore = useMemo(
|
||||
() => new DefaultTimestampStore(LAST_SEEN_ONLINE_STORAGE_KEY),
|
||||
[],
|
||||
);
|
||||
const [promptOpen, setPromptOpen] = useState<boolean>(false);
|
||||
|
||||
const [remainingTimeCountdown, setRemainingTimeCountdown] =
|
||||
@@ -285,7 +301,8 @@ export const AutoLogout = (props: AutoLogoutProps): JSX.Element | null => {
|
||||
|
||||
useLogoutDisconnectedUserEffect({
|
||||
enableEffect: logoutIfDisconnected,
|
||||
autologoutIsEnabled: enabled && isLogged,
|
||||
autologoutIsEnabled: enabled,
|
||||
isLoggedIn: isLogged,
|
||||
idleTimeoutSeconds: idleTimeoutMinutes * 60,
|
||||
lastSeenOnlineStore,
|
||||
identityApi,
|
||||
|
||||
@@ -35,10 +35,29 @@ const mockTimestampStore = {
|
||||
};
|
||||
|
||||
describe('useLogoutDisconnectedUserEffect', () => {
|
||||
it('should not do anything if effect is not enabled', () => {
|
||||
it('should not do anything if isLoggedIn has not yet resolved', () => {
|
||||
const props: UseLogoutDisconnectedUserEffectProps = {
|
||||
enableEffect: true,
|
||||
autologoutIsEnabled: true,
|
||||
isLoggedIn: null,
|
||||
idleTimeoutSeconds: 300,
|
||||
lastSeenOnlineStore: mockTimestampStore,
|
||||
identityApi: mockIdentityApi,
|
||||
};
|
||||
|
||||
renderHook(() => useLogoutDisconnectedUserEffect(props));
|
||||
|
||||
expect(mockTimestampStore.get).not.toHaveBeenCalled();
|
||||
expect(mockTimestampStore.delete).not.toHaveBeenCalled();
|
||||
expect(mockTimestampStore.save).not.toHaveBeenCalled();
|
||||
expect(mockIdentityApi.signOut).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not do anything if effect is not enabled and isLoggedIn is false', () => {
|
||||
const props: UseLogoutDisconnectedUserEffectProps = {
|
||||
enableEffect: false,
|
||||
autologoutIsEnabled: true,
|
||||
isLoggedIn: false,
|
||||
idleTimeoutSeconds: 300,
|
||||
lastSeenOnlineStore: mockTimestampStore,
|
||||
identityApi: mockIdentityApi,
|
||||
@@ -54,6 +73,7 @@ describe('useLogoutDisconnectedUserEffect', () => {
|
||||
const props: UseLogoutDisconnectedUserEffectProps = {
|
||||
enableEffect: true,
|
||||
autologoutIsEnabled: false,
|
||||
isLoggedIn: true,
|
||||
idleTimeoutSeconds: 300,
|
||||
lastSeenOnlineStore: mockTimestampStore,
|
||||
identityApi: mockIdentityApi,
|
||||
@@ -64,12 +84,34 @@ describe('useLogoutDisconnectedUserEffect', () => {
|
||||
expect(mockTimestampStore.delete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete the store and sign out when idle timeout has passed', () => {
|
||||
const staleStore = {
|
||||
...mockTimestampStore,
|
||||
get: jest.fn().mockReturnValue(new Date(Date.now() - 2000)),
|
||||
};
|
||||
const props: UseLogoutDisconnectedUserEffectProps = {
|
||||
enableEffect: true,
|
||||
autologoutIsEnabled: true,
|
||||
isLoggedIn: true,
|
||||
idleTimeoutSeconds: 1,
|
||||
lastSeenOnlineStore: staleStore,
|
||||
identityApi: mockIdentityApi,
|
||||
};
|
||||
|
||||
renderHook(() => useLogoutDisconnectedUserEffect(props));
|
||||
|
||||
expect(staleStore.delete).toHaveBeenCalled();
|
||||
expect(mockIdentityApi.signOut).toHaveBeenCalled();
|
||||
expect(staleStore.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call signOut if idle timeout passed', () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const props: UseLogoutDisconnectedUserEffectProps = {
|
||||
enableEffect: true,
|
||||
autologoutIsEnabled: true,
|
||||
isLoggedIn: true,
|
||||
idleTimeoutSeconds: 1,
|
||||
lastSeenOnlineStore: {
|
||||
...mockTimestampStore,
|
||||
@@ -93,6 +135,7 @@ describe('useLogoutDisconnectedUserEffect', () => {
|
||||
const props: UseLogoutDisconnectedUserEffectProps = {
|
||||
enableEffect: true,
|
||||
autologoutIsEnabled: true,
|
||||
isLoggedIn: true,
|
||||
idleTimeoutSeconds: 300,
|
||||
lastSeenOnlineStore: mockTimestampStore,
|
||||
identityApi: mockIdentityApi,
|
||||
|
||||
@@ -24,6 +24,7 @@ export const LAST_SEEN_ONLINE_STORAGE_KEY =
|
||||
export type UseLogoutDisconnectedUserEffectProps = {
|
||||
enableEffect: boolean;
|
||||
autologoutIsEnabled: boolean;
|
||||
isLoggedIn: boolean | null;
|
||||
idleTimeoutSeconds: number;
|
||||
lastSeenOnlineStore: TimestampStore;
|
||||
identityApi: IdentityApi;
|
||||
@@ -32,6 +33,7 @@ export type UseLogoutDisconnectedUserEffectProps = {
|
||||
export const useLogoutDisconnectedUserEffect = ({
|
||||
enableEffect,
|
||||
autologoutIsEnabled,
|
||||
isLoggedIn,
|
||||
idleTimeoutSeconds,
|
||||
lastSeenOnlineStore,
|
||||
identityApi,
|
||||
@@ -41,30 +43,34 @@ export const useLogoutDisconnectedUserEffect = ({
|
||||
* Considers disconnected users as inactive users.
|
||||
* If all Backstage tabs are closed and idleTimeoutMinutes are passed then logout the user anyway.
|
||||
*/
|
||||
if (autologoutIsEnabled && enableEffect) {
|
||||
const lastSeenOnline = lastSeenOnlineStore.get();
|
||||
if (lastSeenOnline) {
|
||||
const now = new Date();
|
||||
const nowSeconds = Math.ceil(now.getTime() / 1000);
|
||||
const lastSeenOnlineSeconds = Math.ceil(
|
||||
lastSeenOnline.getTime() / 1000,
|
||||
);
|
||||
if (nowSeconds - lastSeenOnlineSeconds > idleTimeoutSeconds) {
|
||||
identityApi.signOut();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* save for the first time when app is loaded, so that
|
||||
* if user logs in and does nothing we still have a
|
||||
* lastSeenOnline value in store
|
||||
*/
|
||||
lastSeenOnlineStore.save(new Date());
|
||||
} else {
|
||||
lastSeenOnlineStore.delete();
|
||||
const shouldCheckDisconnectedUser = autologoutIsEnabled && enableEffect;
|
||||
|
||||
// Prevent lastSeen getting deleted before logged state is checked
|
||||
if (isLoggedIn === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldCheckDisconnectedUser || !isLoggedIn) {
|
||||
lastSeenOnlineStore.delete();
|
||||
return;
|
||||
}
|
||||
|
||||
const lastSeenOnline = lastSeenOnlineStore.get();
|
||||
if (lastSeenOnline) {
|
||||
const now = new Date();
|
||||
const nowSeconds = Math.ceil(now.getTime() / 1000);
|
||||
const lastSeenOnlineSeconds = Math.ceil(lastSeenOnline.getTime() / 1000);
|
||||
if (nowSeconds - lastSeenOnlineSeconds > idleTimeoutSeconds) {
|
||||
lastSeenOnlineStore.delete();
|
||||
identityApi.signOut();
|
||||
return;
|
||||
}
|
||||
}
|
||||
lastSeenOnlineStore.save(new Date());
|
||||
}, [
|
||||
autologoutIsEnabled,
|
||||
enableEffect,
|
||||
isLoggedIn,
|
||||
identityApi,
|
||||
idleTimeoutSeconds,
|
||||
lastSeenOnlineStore,
|
||||
|
||||
Reference in New Issue
Block a user