Skip to content

feat: logout user if idle for configured duration #1331

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

Merged
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
6 changes: 5 additions & 1 deletion config/constants/development.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
147 changes: 98 additions & 49 deletions src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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 = (<AlertModal
theme={theme}
title='Session Timeout'
message={`You've been idle for quite sometime. You'll be automatically logged out in ${this.state.logsoutIn >= 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 (
<IdleTimer ref={ref => { this.idleTimer = ref }} timeout={1000 * 60 * IDLE_TIMEOUT_MINUTES} onIdle={this.handleOnIdle} debounce={250}>
<Switch>
{!isAllowed &&
<Route exact path='/'
render={() => renderApp(
<Challenges menu='NULL' warnMessage={warnMessage} />,
<Challenges menu='NULL' warnMessage={'You are not authorized to use this application'} />,
<TopBarContainer />,
<Sidebar />
)()}
/>
/>}

{isAllowed && <>
<Route exact path='/'
render={() => renderApp(
<Challenges menu='NULL' />,
<TopBarContainer />,
<Sidebar />
)()}
/>
<Route exact path='/self-service'
render={() => renderApp(
<Challenges selfService />,
<TopBarContainer />,
<Sidebar selfService />
)()}
/>
<Route exact path='/projects/:projectId/challenges/new'
render={({ match }) => renderApp(
<ChallengeEditor />,
<TopBarContainer />,
<Sidebar projectId={match.params.projectId} menu={'New Challenge'} />
)()} />
<Route exact path='/challenges/:challengeId' component={ConnectRedirectToChallenge} />
<Route
path='/projects/:projectId/challenges/:challengeId'
render={({ match }) => renderApp(
<ChallengeEditor />,
<TopBarContainer />,
<Sidebar projectId={match.params.projectId} menu={'New Challenge'} />
)()} />
<Route exact path='/projects/:projectId/challenges'
render={({ match }) => renderApp(
<Challenges projectId={match.params.projectId} />,
<TopBarContainer projectId={match.params.projectId} />,
<Sidebar projectId={match.params.projectId} />
)()} />
</>}

{/* If path is not defined redirect to landing page */}
<Redirect to='/' />
</Switch>
)
}

return (
<Switch>
<Route exact path='/'
render={() => renderApp(
<Challenges menu='NULL' />,
<TopBarContainer />,
<Sidebar />
)()}
/>
<Route exact path='/self-service'
render={() => renderApp(
<Challenges selfService />,
<TopBarContainer />,
<Sidebar selfService />
)()}
/>
<Route exact path='/projects/:projectId/challenges/new'
render={({ match }) => renderApp(
<ChallengeEditor />,
<TopBarContainer />,
<Sidebar projectId={match.params.projectId} menu={'New Challenge'} />
)()} />
<Route exact path='/challenges/:challengeId' component={ConnectRedirectToChallenge} />
<Route
path='/projects/:projectId/challenges/:challengeId'
render={({ match }) => renderApp(
<ChallengeEditor />,
<TopBarContainer />,
<Sidebar projectId={match.params.projectId} menu={'New Challenge'} />
)()} />
<Route exact path='/projects/:projectId/challenges'
render={({ match }) => renderApp(
<Challenges projectId={match.params.projectId} />,
<TopBarContainer projectId={match.params.projectId} />,
<Sidebar projectId={match.params.projectId} />
)()} />
{/* If path is not defined redirect to landing page */}
<Redirect to='/' />
</Switch>
{this.state.showIdleModal && modal}
</IdleTimer>
)
}
}
Expand Down
114 changes: 114 additions & 0 deletions src/styles/modal.module.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}