Skip to content

Commit

Permalink
Rewrite TimeoutDialog
Browse files Browse the repository at this point in the history
- Use the last time an API request was made to determine if the session
token is about to expire.
- Use a setTimeout loop to keep the countdown up to date, not to
actually trigger the logout.
- Use IdleJS to determine if the user has been inactive for 15 minutes.
- Separate messages for client side inactivity and an expiring session
token.
  • Loading branch information
dchiquito committed Mar 4, 2022
1 parent d918065 commit 93e212e
Showing 1 changed file with 99 additions and 106 deletions.
205 changes: 99 additions & 106 deletions client/src/components/TimeoutDialog.vue
Original file line number Diff line number Diff line change
@@ -1,104 +1,105 @@
<script>
import {
mapState, mapActions, mapMutations,
} from 'vuex';
<script lang="ts">
import IdleJS from 'idle-js';
import djangoRest from '@/django';
import store from '@/store';
import {
computed, defineComponent, ref,
} from '@vue/composition-api';
const initMinutes = 1;
const initSeconds = 59;
const warningDuration = 2 * 60 * 1000; // the warning box will pop up for 2 minutes
// The server-side session token lasts 30 minutes
const sessionTimeout = 30 * 60 * 1000;
// Log out after 15 minutes if the user is away from keyboard
const idleTimeout = 15 * 60 * 1000;
export default {
export default defineComponent({
name: 'TimeoutDialog',
data: () => ({
show: false,
minutes: initMinutes,
seconds: initSeconds,
}),
computed: {
...mapState(['actionTimeout']),
minutesStr() {
switch (this.minutes) {
case 0:
return '';
case 1:
return '1 minute';
default:
return `${this.minutes} minutes`;
}
},
secondsStr() {
switch (this.seconds) {
case 0:
return '';
case 1:
return '1 second';
default:
return `${this.seconds} seconds`;
}
},
done() {
return this.minutes <= 0 && this.seconds <= 0;
},
},
watch: {
// vue-idle: Adds a computed value 'isAppIdle' to all Vue objects
isAppIdle(idle) {
if (idle && !this.show) {
this.show = true;
this.decrement();
}
},
actionTimeout(timeout) {
if (timeout && !this.show) {
this.show = true;
this.decrement();
}
},
},
created() {
this.startActionTimer();
},
methods: {
...mapActions(['startActionTimer', 'resetActionTimer']),
...mapMutations(['setActionTimeout']),
reset() {
// reset dialog
this.show = false;
this.minutes = initMinutes;
this.seconds = initSeconds;
setup() {
const show = ref(false);
const idleWarningTriggered = ref(false);
const idleStartTime = ref(0);
const timeRemaining = ref(0);
const timeRemainingStr = computed(() => {
const secondsRemaining = Math.floor(timeRemaining.value / 1000);
const minutes = Math.floor(secondsRemaining / 60);
const seconds = String(Math.floor(secondsRemaining % 60)).padStart(2, '0');
return `${minutes}:${seconds}`;
});
// reset no-action timer
this.setActionTimeout(false);
this.resetActionTimer();
},
logout() {
this.minutes = 0;
this.seconds = 0;
},
reload() {
this.$router.go();
},
decrement() {
if (this.show) {
setTimeout(() => {
this.seconds -= 1;
const lastApiRequestTime = computed(() => store.state.lastApiRequestTime);
if (this.minutes <= 0 && this.seconds <= 0) {
djangoRest.logout();
return;
}
const reset = () => {
// Send a request to refresh the server token
// Not awaited since we don't actually care about the result
djangoRest.projects();
if (this.seconds === 0) {
this.minutes -= 1;
this.seconds = initSeconds;
}
// reset dialog
show.value = false;
idleWarningTriggered.value = false;
};
const logout = async () => {
await djangoRest.logout();
// This will redirect to the login page
await djangoRest.login();
};
this.decrement();
}, 1000);
// Watch for an absence of user interaction
const idle = new IdleJS({
idle: idleTimeout - warningDuration,
onIdle() {
if (!show.value) {
idleWarningTriggered.value = true;
idleStartTime.value = Date.now();
}
},
});
idle.start();
// This function calls itself after 1 second to check if the session has expired and to keep
// the countdown timer up to date.
const updateCountdown = () => {
const now = Date.now();
console.log('updating countdown', now);
const sessionTimeRemaining = lastApiRequestTime.value + sessionTimeout - now;
if (idleWarningTriggered.value) {
// If the user is idle, we also need to consider the idle warning
const idleTimeRemaining = idleStartTime.value + warningDuration - now;
timeRemaining.value = Math.min(sessionTimeRemaining, idleTimeRemaining);
} else {
timeRemaining.value = sessionTimeRemaining;
}
// The timer has expired, log out
if (timeRemaining.value <= 0) {
logout();
}
},
// Show the warning if the time remaining is getting close to 0
show.value = timeRemaining.value < warningDuration;
setTimeout(updateCountdown, 1000);
};
updateCountdown();
// TODO when the webpack dev server reloads, it doesn't stop this setTimeout loop.
// The component that the old loop was servicing no longer exists so it doesn't actually matter,
// but it would be nice to garbage collect it.
return {
show,
idleWarningTriggered,
timeRemaining,
timeRemainingStr,
reset,
logout,
sessionTimeout,
idleTimeout,
};
},
};
onIdle() {
// TODO why this does not trigger
console.log('Triggered onIdle!');
this.show = true;
this.idleWarningTriggered = true;
this.idleStartTime = Date.now();
},
});
</script>

<template>
Expand All @@ -113,13 +114,15 @@ export default {
</v-card-title>

<v-card-text class="py-4 px-6">
<p v-if="done">
You have been logged out due to inactivity. Refresh the page to log
back in
<p v-if="idleWarningTriggered">
You have been idle for almost {{ Math.floor(idleTimeout / (60 * 1000)) }} minutes.
</p>
<p v-else>
You have been inactive for almost 30 minutes. Your session will
automatically terminate in {{ minutesStr }} {{ secondsStr }}
You have not made any network requests in almost
{{ Math.floor(sessionTimeout / (60 * 1000)) }} minutes.
</p>
<p>
Your session will automatically terminate in {{ timeRemainingStr }}
</p>
</v-card-text>

Expand All @@ -128,29 +131,19 @@ export default {
<v-card-actions>
<v-spacer />
<v-btn
v-if="!done"
color="primary"
text
@click="reset"
>
Continue Session
</v-btn>
<v-btn
v-if="!done"
color="secondary"
text
@click="logout"
>
Logout
</v-btn>
<v-btn
v-if="done"
color="primary"
text
@click="reload"
>
Reload
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
Expand Down

0 comments on commit 93e212e

Please sign in to comment.