16848: Synchronizes auto-logout between different windows/tabs.
[arvados-workbench2.git] / src / views-components / auto-logout / auto-logout.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import { connect } from "react-redux";
6 import { useIdleTimer } from "react-idle-timer";
7 import { Dispatch } from "redux";
8
9 import { RootState } from "~/store/store";
10 import { SnackbarKind, snackbarActions } from "~/store/snackbar/snackbar-actions";
11 import { logout } from "~/store/auth/auth-action";
12 import parse from "parse-duration";
13 import * as React from "react";
14 import { min } from "lodash";
15
16 interface AutoLogoutDataProps {
17     sessionIdleTimeout: number;
18     lastWarningDuration: number;
19 }
20
21 interface AutoLogoutActionProps {
22     doLogout: () => void;
23     doWarn: (message: string, duration: number) => void;
24     doCloseWarn: () => void;
25 }
26
27 const mapStateToProps = (state: RootState, ownProps: any): AutoLogoutDataProps => {
28     return {
29         sessionIdleTimeout: parse(state.auth.config.clusterConfig.Workbench.IdleTimeout, 's') || 0,
30         lastWarningDuration: ownProps.lastWarningDuration || 60,
31     };
32 };
33
34 const mapDispatchToProps = (dispatch: Dispatch): AutoLogoutActionProps => ({
35     doLogout: () => dispatch<any>(logout(true)),
36     doWarn: (message: string, duration: number) =>
37         dispatch(snackbarActions.OPEN_SNACKBAR({
38             message, hideDuration: duration, kind: SnackbarKind.WARNING })),
39     doCloseWarn: () => dispatch(snackbarActions.CLOSE_SNACKBAR()),
40 });
41
42 export type AutoLogoutProps = AutoLogoutDataProps & AutoLogoutActionProps;
43
44 const debounce = (delay: number | undefined, fn: Function) => {
45     let timerId: number | null;
46     return (...args: any[]) => {
47         if (timerId) { clearTimeout(timerId); }
48         timerId = setTimeout(() => {
49             fn(...args);
50             timerId = null;
51         }, delay);
52     };
53 };
54
55 const LAST_ACTIVE_TIMESTAMP = 'lastActiveTimestamp';
56
57 export const AutoLogoutComponent = (props: AutoLogoutProps) => {
58     let logoutTimer: NodeJS.Timer;
59     const lastWarningDuration = min([props.lastWarningDuration, props.sessionIdleTimeout])! * 1000;
60     const handleOtherTabActivity = debounce(500, () => {
61         handleOnActive();
62         reset();
63     });
64
65     window.addEventListener('storage', (e: StorageEvent) => {
66         // Other tab activity detected by a localStorage change event.
67         if (e.key === LAST_ACTIVE_TIMESTAMP) { handleOtherTabActivity(); }
68     });
69
70     const handleOnIdle = () => {
71         logoutTimer = setTimeout(
72             () => props.doLogout(), lastWarningDuration);
73         props.doWarn(
74             "Your session is about to be closed due to inactivity",
75             lastWarningDuration);
76     };
77
78     const handleOnActive = () => {
79         if (logoutTimer) { clearTimeout(logoutTimer); }
80         props.doCloseWarn();
81     };
82
83     const handleOnAction = () => {
84         // Notify the other tabs there was some activity.
85         const now = (new Date).getTime();
86         localStorage.setItem(LAST_ACTIVE_TIMESTAMP, now.toString());
87     };
88
89     const { reset } = useIdleTimer({
90         timeout: (props.lastWarningDuration < props.sessionIdleTimeout)
91             ? 1000 * (props.sessionIdleTimeout - props.lastWarningDuration)
92             : 1,
93         onIdle: handleOnIdle,
94         onActive: handleOnActive,
95         onAction: handleOnAction,
96         debounce: 500
97     });
98
99     return <span />;
100 };
101
102 export const AutoLogout = connect(mapStateToProps, mapDispatchToProps)(AutoLogoutComponent);