added AutoLogoutProvider

Signed-off-by: Manuel Scurti <manuel.scurti@agilelab.it>
This commit is contained in:
Manuel Scurti
2023-06-10 20:07:57 +02:00
parent 54b411d9ae
commit 9b74166d11
17 changed files with 946 additions and 2 deletions
+67
View File
@@ -0,0 +1,67 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed 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.
*/
export interface Config {
auth?: {
/**
* Autologout feature configuration
*/
autologout?: {
/**
* Enable or disable the autologout feature
* @visibility frontend
*/
enabled?: boolean;
/**
* Number of minutes after which the inactive user is logged out automatically.
* Default is 60 minutes (1 hour)
* @visibility frontend
*/
idleTimeoutMinutes?: number;
/**
* Number of seconds before the idle timeout where the user will be asked if it's still active.
* A dialog will be shown.
* Default is 10 seconds.
* Set to 0 seconds to disable the prompt.
* @visibility frontend
*/
promptBeforeIdleSeconds?: number;
/**
* Enable/disable the usage of worker thread timers instead of main thread timers.
* Default is true.
* If you experience some browser incompatibility, you may try to set this to false.
* @visibility frontend
*/
useWorkerTimers?: boolean;
/**
* List of events that the autologout event listener will catch to detect if the user is active
* These events are standard DOM events.
* @visibility frontend
*/
events?: string[];
/**
* Enable/disable the automatic logout also on users that are logged in but with no Backstage tabs open.
* Default is true.
* @visibility frontend
*/
logoutIfDisconnected?: boolean;
};
};
}
+13 -2
View File
@@ -64,6 +64,7 @@
"rc-progress": "3.5.1",
"react-helmet": "6.1.0",
"react-hook-form": "^7.12.2",
"react-idle-timer": "5.6.2",
"react-markdown": "^8.0.0",
"react-sparklines": "^1.7.0",
"react-syntax-highlighter": "^15.4.5",
@@ -101,9 +102,19 @@
"@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/react-window": "^1.8.5",
"@types/zen-observable": "^0.8.0",
"cross-fetch": "^3.1.5",
"jest-localstorage-mock": "^2.4.26",
"msw": "^1.0.0"
},
"files": [
"dist"
]
"dist",
"config.d.ts"
],
"configSchema": "config.d.ts",
"jest": {
"resetMocks": false,
"setupFiles": [
"jest-localstorage-mock"
]
}
}
@@ -0,0 +1,207 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed 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 { identityApiRef, useApi } from '@backstage/core-plugin-api';
import React, { PropsWithChildren, useEffect, useMemo, useState } from 'react';
import {
EventsType,
IdleTimerProvider,
IIdleTimer,
workerTimers,
} from 'react-idle-timer';
import {
LAST_SEEN_ONLINE_STORAGE_KEY,
useLogoutDisconnectedUserEffect,
} from './disconnectedUsers';
import { StillTherePrompt } from './StillTherePrompt';
import { DefaultTimestampStore, TimestampStore } from './timestampStore';
export type AutoLogoutTrackableEvent = EventsType;
export type AutoLogoutProviderProps = {
/**
* Enable/disable the AutoLogoutMechanism.
* @default true
*/
enabled?: boolean;
/**
* The amount of time (in minutes) of inactivity
* after which the user is automatically logged out.
* @default 60 minutes.
*/
idleTimeoutMinutes?: number;
/**
* The number of seconds before the idleTimeout expires,
* at which the user will be alerted by a Dialog that
* they are about to be logged out.
* @default 10 seconds
*/
promptBeforeIdleSeconds?: number;
/**
* Enable/disable the usage of Node's worker thread timers instead of main thread timers.
* This is helpful if you notice that the your browser is killing inactive tab's timers, like the one used by AutoLogoutProvider.
* If you experience some browser incompatibility, you may try to set this to false.
* @default true.
*/
useWorkerTimers?: boolean;
/**
* List of DOM events that the AutoLogoutProvider will track to determine if the user is active or not.
* @default the default list includes all the needed events for keyboard/pointers/mouse devices.
*/
events?: AutoLogoutTrackableEvent[];
/**
* Enable/disable the autologout for disconnected users.
* disconnected users are the ones that are logged in but have no Backstage tab open in their browsers.
* If enabled, disconnected users will be automatically logged out after `idleTimeoutMinutes`
* @default true
*/
logoutIfDisconnected?: boolean;
};
/**
* The Autologout feature enables platform engineers to add a mechanism to log out users after a configurable amount of time of inactivity.
* When enabled, the mechanism will track user actions (mouse movement, mouse click, key pressing, taps, etc.) in order to determine if they are active or not.
* After a certain amount of inactivity/idle time, the user session is invalidated and they are required to sign in again.
*/
export const AutoLogoutProvider = ({
children,
enabled = true,
idleTimeoutMinutes = 60,
promptBeforeIdleSeconds = 10,
useWorkerTimers = true,
events = [
'mousemove',
'keydown',
'wheel',
'DOMMouseScroll',
'mousewheel',
'mousedown',
'touchstart',
'touchmove',
'MSPointerDown',
'MSPointerMove',
'visibilitychange',
],
logoutIfDisconnected = true,
}: PropsWithChildren<AutoLogoutProviderProps>): JSX.Element => {
const identityApi = useApi(identityApiRef);
useEffect(() => {
if (idleTimeoutMinutes < 0.5) {
throw new Error(
'❌ idleTimeoutMinutes property should be >= 0.5 minutes (30 seconds).',
);
}
if (promptBeforeIdleSeconds < 0) {
throw new Error(
'❌ promptBeforeIdleSeconds property should be >= 0 seconds. Set to 0 to disable the prompt.',
);
}
if (idleTimeoutMinutes * 60 <= promptBeforeIdleSeconds) {
throw new Error(
`❌ promptBeforeIdleSeconds should be smaller than idleTimeoutMinutes`,
);
}
}, [idleTimeoutMinutes, promptBeforeIdleSeconds]);
const lastSeenOnlineStore: TimestampStore = useMemo(
() => new DefaultTimestampStore(LAST_SEEN_ONLINE_STORAGE_KEY),
[],
);
const [promptOpen, setPromptOpen] = useState<boolean>(false);
const [remainingTimeCountdown, setRemainingTimeCountdown] =
useState<number>(0);
useLogoutDisconnectedUserEffect({
enableEffect: logoutIfDisconnected,
autologoutIsEnabled: enabled,
idleTimeoutSeconds: idleTimeoutMinutes * 60,
lastSeenOnlineStore,
identityApi,
});
if (!enabled) {
return <>{children}</>;
}
const promptBeforeIdleMillis = promptBeforeIdleSeconds * 1000;
const promptBeforeIdle = promptBeforeIdleMillis > 0 ? true : false;
const onPrompt = () => {
// onPrompt will be called `promptBeforeIdle` milliseconds before `timeout`.
// All events are disabled while the prompt is active.
// If the user wishes to stay active, call the `activate()` method.
// You can get the remaining prompt time with the `getRemainingTime()` method,
setPromptOpen(true);
setRemainingTimeCountdown(promptBeforeIdleMillis);
};
const onIdle = () => {
// onIdle will be called after the timeout is reached.
// Events will be rebound as long as `stopOnMount` is not set.
setPromptOpen(false);
setRemainingTimeCountdown(0);
identityApi.signOut();
};
const onActive = () => {
// onActive will only be called if `activate()` is called while `isPrompted()`
// is true. Here you will also want to close your modal and perform
// any active actions.
setPromptOpen(false);
setRemainingTimeCountdown(0);
};
const onAction = (
_event?: Event | undefined,
_idleTimer?: IIdleTimer | null,
) => {
// onAction will be called if any user event is detected. The list of events that triggers a user event detection is the list of configured events
// If any user event is detected we update the Last seen online in storage
lastSeenOnlineStore.save(new Date());
};
return (
<IdleTimerProvider
timeout={idleTimeoutMinutes * 60 * 1000}
events={events}
crossTab
name="autologout-timer"
timers={useWorkerTimers ? workerTimers : undefined}
onIdle={onIdle}
onActive={promptBeforeIdle ? onActive : undefined}
onAction={logoutIfDisconnected ? onAction : undefined}
onPrompt={promptBeforeIdle ? onPrompt : undefined}
promptBeforeIdle={promptBeforeIdle ? promptBeforeIdleMillis : undefined}
syncTimers={1000}
>
{promptBeforeIdle && (
<StillTherePrompt
open={promptOpen}
setOpen={setPromptOpen}
remainingTime={remainingTimeCountdown}
setRemainingTime={setRemainingTimeCountdown}
promptTimeoutMillis={promptBeforeIdleMillis}
/>
)}
{children}
</IdleTimerProvider>
);
};
@@ -0,0 +1,85 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed 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 { createMocks } from 'react-idle-timer';
// eslint-disable-next-line no-restricted-imports
import { MessageChannel } from 'worker_threads';
import { ApiProvider } from '@backstage/core-app-api';
import { identityApiRef } from '@backstage/core-plugin-api';
import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils';
import React from 'react';
import { AutoLogoutProvider } from './AutoLogoutProvider';
import { cleanup, render } from '@testing-library/react';
// Mock the signOut function of identityApiRef
const mockSignOut = jest.fn();
const mockIdentityApi = { signOut: mockSignOut };
const apis = TestApiRegistry.from([identityApiRef, mockIdentityApi]);
describe('AutoLogoutProvider', () => {
beforeAll(() => {
createMocks();
// @ts-ignore
global.MessageChannel = MessageChannel;
});
afterAll(cleanup);
afterEach(() => {
jest.clearAllMocks();
});
it('renders children even when enabled is false', async () => {
const { queryByText } = await renderWithEffects(
<ApiProvider apis={apis}>
<AutoLogoutProvider enabled={false}>
<div>Test Child</div>
</AutoLogoutProvider>
</ApiProvider>,
);
expect(queryByText('Test Child')).not.toBeNull();
expect(mockSignOut).not.toHaveBeenCalled();
});
it('should throw error if idleTimeoutMinutes is smaller than promptBeforeSeconds', async () => {
expect(() =>
render(
<ApiProvider apis={apis}>
<AutoLogoutProvider
idleTimeoutMinutes={0.5}
promptBeforeIdleSeconds={120}
>
<div>Test Child</div>
</AutoLogoutProvider>
</ApiProvider>,
),
).toThrow();
});
it('should throw error if idleTimeoutMinutes is smaller than 30 seconds', async () => {
expect(() =>
render(
<ApiProvider apis={apis}>
<AutoLogoutProvider idleTimeoutMinutes={0.49}>
<div>Test Child</div>
</AutoLogoutProvider>
</ApiProvider>,
),
).toThrow();
});
});
@@ -0,0 +1,63 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed 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 { configApiRef, useApi } from '@backstage/core-plugin-api';
import { PropsWithChildren, useMemo } from 'react';
import {
AutoLogoutProvider,
AutoLogoutProviderProps,
AutoLogoutTrackableEvent,
} from './AutoLogoutProvider';
import React from 'react';
export type ConfigBasedAutoLogoutProviderProps = {};
/**
* A config based flavour of the AutoLogoutProvider.
* It allows to configure Autologout settings through `app-config`
*/
export const ConfigBasedAutoLogoutProvider = ({
children,
}: PropsWithChildren<ConfigBasedAutoLogoutProviderProps>): JSX.Element => {
const configApi = useApi(configApiRef);
const props: AutoLogoutProviderProps = useMemo(() => {
return {
enabled: configApi.getOptionalBoolean('auth.autologout.enabled'),
idleTimeoutMinutes: configApi.getOptionalNumber(
'auth.autologout.idleTimeoutMinutes',
),
promptBeforeIdleSeconds: configApi.getOptionalNumber(
'auth.autologout.promptBeforeIdleSeconds',
),
useWorkerTimers: configApi.getOptionalBoolean(
'auth.autologout.useWorkerTimers',
),
events: configApi
.getOptionalStringArray('auth.autologout.events')
?.map(event => event as AutoLogoutTrackableEvent),
logoutIfDisconnected: configApi.getOptionalBoolean(
'auth.autologout.logoutIfDisconnected',
),
};
}, [configApi]);
return (
<AutoLogoutProvider {...{ ...props, enabled: props.enabled ?? false }}>
{children}
</AutoLogoutProvider>
);
};
@@ -0,0 +1,89 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed 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 Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
import React, { useEffect } from 'react';
import { useIdleTimerContext } from 'react-idle-timer';
export interface StillTherePromptProps {
promptTimeoutMillis: number;
remainingTime: number;
setRemainingTime: (amount: number) => void;
open: boolean;
setOpen: (value: boolean) => void;
}
export const StillTherePrompt = (props: StillTherePromptProps) => {
const {
setOpen,
open,
promptTimeoutMillis,
remainingTime,
setRemainingTime,
} = props;
const idleTimer = useIdleTimerContext();
useEffect(() => {
const interval = setInterval(() => {
setRemainingTime(Math.ceil(idleTimer.getRemainingTime()));
}, 500);
return () => {
clearInterval(interval);
};
}, [idleTimer, setRemainingTime]);
const handleStillHere = () => {
setOpen(false);
idleTimer.activate();
};
const timeTillPrompt = Math.max(
remainingTime - promptTimeoutMillis / 1000,
0,
);
const seconds = timeTillPrompt > 1 ? 'seconds' : 'second';
return (
<Dialog open={open}>
<DialogTitle>Logging out due to inactivity</DialogTitle>
<DialogContent>
<DialogContentText>
You are about to be disconnected in{' '}
<b>
{Math.ceil(remainingTime / 1000)} {seconds}
</b>
. Are you still there?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={handleStillHere}
color="secondary"
variant="contained"
size="small"
>
Yes! Don't log me out
</Button>
</DialogActions>
</Dialog>
);
};
@@ -0,0 +1,110 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed 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 { renderHook } from '@testing-library/react-hooks';
import { act } from 'react-dom/test-utils';
import {
useLogoutDisconnectedUserEffect,
UseLogoutDisconnectedUserEffectProps,
} from './disconnectedUsers';
const mockIdentityApi = {
signOut: jest.fn(),
getProfileInfo: jest.fn(),
getBackstageIdentity: jest.fn(),
getCredentials: jest.fn(),
};
const mockTimestampStore = {
get: jest.fn(),
save: jest.fn(),
delete: jest.fn(),
};
describe('useLogoutDisconnectedUserEffect', () => {
it('should not do anything if effect is not enabled', () => {
const props: UseLogoutDisconnectedUserEffectProps = {
enableEffect: false,
autologoutIsEnabled: true,
idleTimeoutSeconds: 300,
lastSeenOnlineStore: mockTimestampStore,
identityApi: mockIdentityApi,
};
renderHook(() => useLogoutDisconnectedUserEffect(props));
expect(mockTimestampStore.get).not.toHaveBeenCalled();
expect(mockIdentityApi.signOut).not.toHaveBeenCalled();
});
it('should delete the store if autologout is not enabled', () => {
const props: UseLogoutDisconnectedUserEffectProps = {
enableEffect: true,
autologoutIsEnabled: false,
idleTimeoutSeconds: 300,
lastSeenOnlineStore: mockTimestampStore,
identityApi: mockIdentityApi,
};
renderHook(() => useLogoutDisconnectedUserEffect(props));
expect(mockTimestampStore.delete).toHaveBeenCalled();
});
it('should call signOut if idle timeout passed', () => {
jest.useFakeTimers();
const props: UseLogoutDisconnectedUserEffectProps = {
enableEffect: true,
autologoutIsEnabled: true,
idleTimeoutSeconds: 1,
lastSeenOnlineStore: {
...mockTimestampStore,
get: jest.fn().mockReturnValue(new Date(Date.now() - 2000)), // 2 seconds before now
},
identityApi: mockIdentityApi,
};
renderHook(() => useLogoutDisconnectedUserEffect(props));
act(() => {
jest.advanceTimersByTime(2000);
});
expect(mockIdentityApi.signOut).toHaveBeenCalled();
jest.useRealTimers();
});
it('should save the current time to the store when app is loaded', () => {
const props: UseLogoutDisconnectedUserEffectProps = {
enableEffect: true,
autologoutIsEnabled: true,
idleTimeoutSeconds: 300,
lastSeenOnlineStore: mockTimestampStore,
identityApi: mockIdentityApi,
};
renderHook(() => useLogoutDisconnectedUserEffect(props));
expect(mockTimestampStore.get).toHaveBeenCalled();
expect(mockTimestampStore.save).toHaveBeenCalled();
});
afterEach(() => {
jest.clearAllMocks();
});
});
@@ -0,0 +1,72 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed 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 { IdentityApi } from '@backstage/core-plugin-api';
import { useEffect } from 'react';
import { TimestampStore } from './timestampStore';
export const LAST_SEEN_ONLINE_STORAGE_KEY =
'@backstage/autologout:lastSeenOnline';
export type UseLogoutDisconnectedUserEffectProps = {
enableEffect: boolean;
autologoutIsEnabled: boolean;
idleTimeoutSeconds: number;
lastSeenOnlineStore: TimestampStore;
identityApi: IdentityApi;
};
export const useLogoutDisconnectedUserEffect = ({
enableEffect,
autologoutIsEnabled,
idleTimeoutSeconds,
lastSeenOnlineStore,
identityApi,
}: UseLogoutDisconnectedUserEffectProps) => {
useEffect(() => {
/**
* 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();
}
}, [
autologoutIsEnabled,
enableEffect,
identityApi,
idleTimeoutSeconds,
lastSeenOnlineStore,
]);
};
@@ -0,0 +1,22 @@
import { LAST_SEEN_ONLINE_STORAGE_KEY } from './disconnectedUsers';
/*
* Copyright 2023 The Backstage Authors
*
* Licensed 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.
*/
export * from './AutoLogoutProvider';
export * from './ConfigBasedAutoLogoutProvider';
export const AUTOLOGOUT_LAST_SEEN_ONLINE_STORAGE_KEY =
LAST_SEEN_ONLINE_STORAGE_KEY;
@@ -0,0 +1,55 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed 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 { DefaultTimestampStore, TimestampStore } from './timestampStore';
describe('DefaultTimestampStore', () => {
let timestampStore: TimestampStore;
const key = 'test-key';
const date = new Date();
beforeEach(() => {
timestampStore = new DefaultTimestampStore(key);
// Clear the mock storage before each test
localStorage.clear();
});
it('should save the date into the localStorage', () => {
timestampStore.save(date);
expect(localStorage.setItem).toHaveBeenCalledWith(key, date.toJSON());
expect(localStorage.__STORE__[key]).toBe(date.toJSON());
});
it('should get the date from the localStorage', () => {
localStorage.setItem(key, date.toJSON());
const retrievedDate = timestampStore.get();
expect(localStorage.getItem).toHaveBeenCalledWith(key);
expect(retrievedDate).toEqual(date);
});
it('should return null if no date is set in the localStorage', () => {
const retrievedDate = timestampStore.get();
expect(localStorage.getItem).toHaveBeenCalledWith(key);
expect(retrievedDate).toBeNull();
});
it('should delete the date from the localStorage', () => {
localStorage.setItem(key, date.toJSON());
timestampStore.delete();
expect(localStorage.removeItem).toHaveBeenCalledWith(key);
expect(localStorage.__STORE__[key]).toBeUndefined();
});
});
@@ -0,0 +1,39 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed 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.
*/
export interface TimestampStore {
save(date: Date): void;
delete(): void;
get(): Date | null;
}
export class DefaultTimestampStore implements TimestampStore {
constructor(private readonly key: string) {}
save(date: Date): void {
localStorage.setItem(this.key, date.toJSON());
}
delete(): void {
localStorage.removeItem(this.key);
}
get(): Date | null {
const timestamp = localStorage.getItem(this.key);
return timestamp !== null ? new Date(Date.parse(timestamp)) : null;
}
}
@@ -15,6 +15,7 @@
*/
export * from './AlertDisplay';
export * from './AutoLogoutProvider';
export * from './Avatar';
export * from './LinkButton';
export * from './CodeSnippet';
@@ -31,6 +31,7 @@ import { commonProvider } from './commonProvider';
import { guestProvider } from './guestProvider';
import { customProvider } from './customProvider';
import { IdentityApiSignOutProxy } from './IdentityApiSignOutProxy';
import { AUTOLOGOUT_LAST_SEEN_ONLINE_STORAGE_KEY } from '../../components';
const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider';
@@ -95,6 +96,7 @@ export const useSignInProviders = (
IdentityApiSignOutProxy.from({
identityApi,
signOut: async () => {
localStorage.removeItem(AUTOLOGOUT_LAST_SEEN_ONLINE_STORAGE_KEY);
localStorage.removeItem(PROVIDER_STORAGE_KEY);
await identityApi.signOut?.();
},