diff --git a/config/constants/development.js b/config/constants/development.js index 597dbb57..2efc33f9 100644 --- a/config/constants/development.js +++ b/config/constants/development.js @@ -39,5 +39,9 @@ module.exports = { FILE_PICKER_API_KEY: process.env.FILE_PICKER_API_KEY, FILE_PICKER_CONTAINER_NAME: 'tc-challenge-v5-dev', FILE_PICKER_REGION: 'us-east-1', - FILE_PICKER_CNAME: 'fs.topcoder.com' + FILE_PICKER_CNAME: 'fs.topcoder.com', + // if idle for this many minutes, show user a prompt saying they'll be logged out + IDLE_TIMEOUT_MINUTES: 10, + // duration to show the prompt saying user will be logged out, before actually logging out the user + IDLE_TIMEOUT_GRACE_MINUTES: 5 } diff --git a/package-lock.json b/package-lock.json index e44e6c3c..65c76e8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15002,6 +15002,11 @@ "react-side-effect": "^1.1.0" } }, + "react-idle-timer": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/react-idle-timer/-/react-idle-timer-4.6.4.tgz", + "integrity": "sha512-iq61dPud8fgj7l1KOJEY5pyiD532fW0KcIe/5XUe/0lB/4Vytoy4tZBlLGSiYodPzKxTL6HyKoOmG6tyzjD7OQ==" + }, "react-input-autosize": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.2.tgz", diff --git a/package.json b/package.json index 726e30b9..02e50216 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "react-dom": "^16.7.0", "react-google-charts": "^3.0.13", "react-helmet": "^5.2.0", + "react-idle-timer": "^4.6.4", "react-js-pagination": "^3.0.3", "react-popper": "^2.2.4", "react-redux": "^6.0.0", diff --git a/src/routes.js b/src/routes.js index 1c3041da..f91e870e 100644 --- a/src/routes.js +++ b/src/routes.js @@ -17,8 +17,15 @@ import { loadChallengeDetails } from './actions/challenges' import { connect } from 'react-redux' import { checkAllowedRoles } from './util/tc' import { setCookie, removeCookie, isBetaMode } from './util/cookie' +import IdleTimer from 'react-idle-timer' +import AlertModal from './components/Modal/AlertModal' +import modalStyles from './styles/modal.module.scss' -const { ACCOUNTS_APP_LOGIN_URL } = process.env +const { ACCOUNTS_APP_LOGIN_URL, IDLE_TIMEOUT_MINUTES, IDLE_TIMEOUT_GRACE_MINUTES, COMMUNITY_APP_URL } = process.env + +const theme = { + container: modalStyles.modalContainer +} class RedirectToChallenge extends React.Component { componentWillMount () { @@ -59,6 +66,19 @@ RedirectToChallenge.propTypes = { const ConnectRedirectToChallenge = connect(mapStateToProps, mapDispatchToProps)(RedirectToChallenge) class Routes extends React.Component { + constructor (props) { + super(props) + this.idleTimer = null + this.handleOnIdle = this.handleOnIdle.bind(this) + + this.logoutIntervalRef = null + this.state = { + showIdleModal: false, + logsoutIn: IDLE_TIMEOUT_GRACE_MINUTES * 60, // convert to seconds + logoutIntervalRef: null + } + } + componentWillMount () { this.checkAuth() } @@ -87,68 +107,97 @@ class Routes extends React.Component { } } + handleOnIdle () { + this.idleTimer.pause() + const intervalId = setInterval(() => { + const remaining = this.state.logsoutIn + if (remaining > 0) { + this.setState(state => ({ ...state, logsoutIn: remaining - 1 })) + } else { + window.location = `${COMMUNITY_APP_URL}/logout` + } + }, 1000) + + this.setState(state => ({ ...state, showIdleModal: true, logoutIntervalRef: intervalId })) + } + render () { if (!this.props.isLoggedIn) { return null } - let isAllowed = checkAllowedRoles(_.get(decodeToken(this.props.token), 'roles')) + const isAllowed = checkAllowedRoles(_.get(decodeToken(this.props.token), 'roles')) + const modal = (= 60 ? Math.ceil(this.state.logsoutIn / 60) + ' minute(s).' : this.state.logsoutIn + ' second(s)'}`} + closeText='Resume Session' + onClose={() => { + clearInterval(this.state.logoutIntervalRef) + if (this.idleTimer.isIdle()) { + this.idleTimer.resume() + this.idleTimer.reset() + this.setState(state => ({ + ...state, showIdleModal: false, logsoutIn: 120 + })) + } + }} + />) - if (!isAllowed) { - let warnMessage = 'You are not authorized to use this application' - return ( + return ( + { this.idleTimer = ref }} timeout={1000 * 60 * IDLE_TIMEOUT_MINUTES} onIdle={this.handleOnIdle} debounce={250}> + {!isAllowed && renderApp( - , + , , )()} - /> + />} + + {isAllowed && <> + renderApp( + , + , + + )()} + /> + renderApp( + , + , + + )()} + /> + renderApp( + , + , + + )()} /> + + renderApp( + , + , + + )()} /> + renderApp( + , + , + + )()} /> + } + + {/* If path is not defined redirect to landing page */} - ) - } - - return ( - - renderApp( - , - , - - )()} - /> - renderApp( - , - , - - )()} - /> - renderApp( - , - , - - )()} /> - - renderApp( - , - , - - )()} /> - renderApp( - , - , - - )()} /> - {/* If path is not defined redirect to landing page */} - - + {this.state.showIdleModal && modal} + ) } } diff --git a/src/styles/modal.module.scss b/src/styles/modal.module.scss new file mode 100644 index 00000000..12a06dbc --- /dev/null +++ b/src/styles/modal.module.scss @@ -0,0 +1,114 @@ +@import "./includes.scss"; + +.modalContainer { + padding: 0; + position: fixed; + overflow: auto; + z-index: 10000; + top: 0; + right: 0; + bottom: 0; + left: 0; + box-sizing: border-box; + width: auto; + max-width: none; + transform: none; + background: transparent; + color: $text-color; + opacity: 1; + display: flex; + justify-content: center; + align-items: center; + + .contentContainer { + box-sizing: border-box; + background: $white; + opacity: 1; + position: relative; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + border-radius: 6px; + margin: 0 auto; + width: 100% !important; + padding: 30px; + + .content { + padding: 30px; + width: 100%; + height: 100%; + } + + .title { + @include roboto-bold(); + + font-size: 30px; + line-height: 36px; + margin-bottom: 30px; + margin-top: 0; + + } + + span { + @include roboto; + + font-size: 22px; + font-weight: 400; + line-height: 26px; + } + + &.confirm { + width: 999px; + + .buttonGroup { + display: flex; + justify-content: space-between; + margin-top: 30px; + + .buttonSizeA { + width: 193px; + height: 40px; + margin-right: 33px; + + span { + font-size: 18px; + font-weight: 500; + } + } + + .buttonSizeB{ + width: 160px; + height: 40px; + + span { + font-size: 18px; + font-weight: 500; + line-height: 22px; + } + } + } + } + + .buttonGroup { + display: flex; + justify-content: space-between; + margin-top: 30px; + + .button { + width: 135px; + height: 40px; + margin-right: 66px; + + span { + font-size: 18px; + font-weight: 500; + } + } + + .button:last-child { + margin-right: 0; + } + } + } +}