mirror of https://github.com/rancher/dashboard.git
353 lines
11 KiB
Vue
353 lines
11 KiB
Vue
<script>
|
|
import ModalWithCard from '@shell/components/ModalWithCard';
|
|
import { Banner } from '@components/Banner';
|
|
import PercentageBar from '@shell/components/PercentageBar.vue';
|
|
import throttle from 'lodash/throttle';
|
|
import Inactivity from '@shell/utils/inactivity';
|
|
import { MANAGEMENT, EXT, NORMAN } from '@shell/config/types';
|
|
import { SETTING } from '@shell/config/settings';
|
|
|
|
let globalId;
|
|
|
|
const MODAL_VISIBILITY_CHECK_DELAY_SECONDS = 10;
|
|
|
|
export default {
|
|
name: 'Inactivity',
|
|
components: {
|
|
ModalWithCard, Banner, PercentageBar
|
|
},
|
|
data() {
|
|
return {
|
|
sessionTokenName: null,
|
|
tokens: [],
|
|
isUserActive: false,
|
|
userActivityIsoDate: '',
|
|
modalVisibilityCheckRan: false,
|
|
isOpen: false,
|
|
showModalAfter: null,
|
|
expiresAt: null,
|
|
inactivityTimeoutId: null,
|
|
courtesyTimer: null,
|
|
courtesyTimerId: null,
|
|
courtesyCountdown: null,
|
|
trackInactivity: throttle(this._trackInactivity, 1000),
|
|
id: null
|
|
};
|
|
},
|
|
beforeUnmount() {
|
|
this.removeEventListeners();
|
|
this.clearAllTimeouts();
|
|
},
|
|
computed: {
|
|
timerPercentageLeft() {
|
|
return Math.floor((this.courtesyCountdown / this.courtesyTimer ) * 100);
|
|
},
|
|
colorStops() {
|
|
return {
|
|
0: '--info', 30: '--info', 70: '--info'
|
|
};
|
|
},
|
|
userSessionTtlIdleSetting() {
|
|
return this.$store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.AUTH_USER_SESSION_IDLE_TTL_MINUTES);
|
|
},
|
|
userSessionTtlSetting() {
|
|
return this.$store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.AUTH_USER_SESSION_TTL_MINUTES);
|
|
},
|
|
userActivityResource() {
|
|
return this.$store.getters['management/byId'](EXT.USER_ACTIVITY, this.sessionTokenName);
|
|
},
|
|
ttlIdleValue() {
|
|
return parseInt(this.userSessionTtlIdleSetting?.value || 0);
|
|
},
|
|
ttlValue() {
|
|
return parseInt(this.userSessionTtlSetting?.value || 0);
|
|
},
|
|
isFeatureEnabled() {
|
|
return this.ttlIdleValue < this.ttlValue;
|
|
},
|
|
userActivityExpiresAt() {
|
|
return this.userActivityResource?.status?.expiresAt || '';
|
|
},
|
|
watcherData() {
|
|
return {
|
|
userActivityExpiresAt: this.userActivityExpiresAt,
|
|
sessionTokenName: this.sessionTokenName,
|
|
isFeatureEnabled: this.isFeatureEnabled
|
|
};
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
// every time the Idle setting changes, we need to fetch the updated userActivity
|
|
async ttlIdleValue() {
|
|
await Inactivity.getUserActivity(this.$store, this.sessionTokenName);
|
|
},
|
|
watcherData: {
|
|
async handler(neu, old) {
|
|
if (!old?.isFeatureEnabled && neu?.isFeatureEnabled) {
|
|
const tokenName = Inactivity.getSessionTokenName();
|
|
|
|
if (tokenName) {
|
|
this.sessionTokenName = tokenName;
|
|
}
|
|
|
|
await this.initializeInactivityData();
|
|
}
|
|
|
|
const currDate = Date.now();
|
|
const endDate = new Date(neu.userActivityExpiresAt || '0001-01-01 00:00:00 +0000 UTC').getTime();
|
|
|
|
if (endDate > currDate && neu?.sessionTokenName && neu?.isFeatureEnabled) {
|
|
// feature is considered as enabled
|
|
// make sure we always clean up first so that we don't get duplicate timers running
|
|
this.stopInactivity();
|
|
|
|
// resets inactivity data and timers, starting the inactivity again with the proper data
|
|
this.resetInactivityDataAndTimers(this.userActivityResource);
|
|
|
|
// add event listeners for UI interaction
|
|
this.addIdleListeners();
|
|
}
|
|
|
|
if (!neu?.isFeatureEnabled) {
|
|
this.stopInactivity();
|
|
}
|
|
},
|
|
immediate: true,
|
|
deep: true
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
async initializeInactivityData() {
|
|
const canListUserAct = this.$store.getters[`management/canList`](EXT.USER_ACTIVITY);
|
|
const canListTokens = this.$store.getters[`rancher/canList`](NORMAN.TOKEN);
|
|
|
|
if (canListUserAct && canListTokens) {
|
|
const tokens = await this.$store.dispatch('rancher/findAll', { type: NORMAN.TOKEN, opt: { watch: false } });
|
|
|
|
this.tokens = tokens;
|
|
|
|
// handle the fetching/storage of session token name
|
|
if (!this.sessionTokenName) {
|
|
const sessionToken = this.tokens.find((token) => {
|
|
return token.description === 'UI session' && token.current;
|
|
});
|
|
|
|
if (sessionToken?.name) {
|
|
this.sessionTokenName = sessionToken.name;
|
|
Inactivity.setSessionTokenName(sessionToken.name);
|
|
}
|
|
}
|
|
|
|
// get the latest userActivity data so that get reactivity on all this logic
|
|
const userActivityData = await Inactivity.getUserActivity(this.$store, this.sessionTokenName, false);
|
|
|
|
const expiresAt = userActivityData?.status?.expiresAt;
|
|
const currDate = Date.now();
|
|
const endDate = new Date(expiresAt).getTime();
|
|
|
|
// If expiresAt isn't initialised yet '0001-01-01 00:00:00 +0000 UTC' || '', or just passed the 'now' date
|
|
// We need to update/initialise the UserActivity resource
|
|
if ((currDate > endDate) || !expiresAt) {
|
|
const updatedData = await Inactivity.updateUserActivity(this.userActivityResource, this.sessionTokenName, new Date().toISOString());
|
|
|
|
this.expiresAt = updatedData?.status?.expiresAt;
|
|
} else if (expiresAt) {
|
|
this.expiresAt = expiresAt;
|
|
}
|
|
}
|
|
},
|
|
_trackInactivity() {
|
|
if (this.isOpen || !this.showModalAfter) {
|
|
return;
|
|
}
|
|
|
|
const endTime = Date.now() + this.showModalAfter * 1000;
|
|
|
|
this.id = endTime;
|
|
globalId = endTime;
|
|
|
|
const checkInactivityTimer = () => {
|
|
const now = Date.now();
|
|
|
|
if (this.id !== globalId) {
|
|
return;
|
|
}
|
|
|
|
if (now >= endTime) {
|
|
this.isOpen = true;
|
|
this.startCountdown();
|
|
} else {
|
|
// When we have X seconds to go until we display the modal, check for activity on the backend flag
|
|
// it may have come from another tab in the same browser
|
|
if (now >= endTime - (MODAL_VISIBILITY_CHECK_DELAY_SECONDS * 1000) && !this.modalVisibilityCheckRan) {
|
|
this.modalVisibilityCheckRan = true;
|
|
|
|
if (this.isUserActive) {
|
|
this.resetUserActivity();
|
|
} else {
|
|
this.checkBackendInactivity(this.expiresAt);
|
|
}
|
|
}
|
|
|
|
this.inactivityTimeoutId = setTimeout(checkInactivityTimer, 1000);
|
|
}
|
|
};
|
|
|
|
checkInactivityTimer();
|
|
},
|
|
startCountdown() {
|
|
const endTime = Date.now() + (this.courtesyCountdown * 1000);
|
|
|
|
const checkCountdown = async() => {
|
|
const now = Date.now();
|
|
|
|
if (now >= endTime) {
|
|
this.clearAllTimeouts();
|
|
const isUserActive = await this.checkBackendInactivity(this.expiresAt);
|
|
|
|
if (!isUserActive) {
|
|
return this.$store.dispatch('auth/logout', { sessionIdle: true });
|
|
}
|
|
} else {
|
|
this.courtesyCountdown = Math.floor((endTime - now) / 1000);
|
|
this.courtesyTimerId = setTimeout(checkCountdown, 1000);
|
|
}
|
|
};
|
|
|
|
checkCountdown();
|
|
},
|
|
async checkBackendInactivity(currExpiresAt) {
|
|
let isUserActive = false;
|
|
const userActivityData = await Inactivity.getUserActivity(this.$store, this.sessionTokenName);
|
|
|
|
// this means that something updated the backend expiresAt, which means we must now reset the timers and adjust for new data
|
|
if (userActivityData?.status?.expiresAt && (userActivityData?.status?.expiresAt !== currExpiresAt)) {
|
|
isUserActive = true;
|
|
this.resetInactivityDataAndTimers(userActivityData);
|
|
}
|
|
|
|
return isUserActive;
|
|
},
|
|
setUserAsActive() {
|
|
this.isUserActive = true;
|
|
this.userActivityIsoDate = new Date().toISOString();
|
|
},
|
|
addIdleListeners() {
|
|
document.addEventListener('mousemove', this.setUserAsActive);
|
|
document.addEventListener('mousedown', this.setUserAsActive);
|
|
document.addEventListener('keypress', this.setUserAsActive);
|
|
document.addEventListener('touchmove', this.setUserAsActive);
|
|
document.addEventListener('visibilitychange', this.setUserAsActive);
|
|
},
|
|
removeEventListeners() {
|
|
document.removeEventListener('mousemove', this.setUserAsActive);
|
|
document.removeEventListener('mousedown', this.setUserAsActive);
|
|
document.removeEventListener('keypress', this.setUserAsActive);
|
|
document.removeEventListener('touchmove', this.setUserAsActive);
|
|
document.removeEventListener('visibilitychange', this.setUserAsActive);
|
|
},
|
|
async resetUserActivity(useCurrDate = false) {
|
|
let seenAt;
|
|
|
|
if (useCurrDate) {
|
|
seenAt = new Date().toISOString();
|
|
} else {
|
|
seenAt = this.userActivityIsoDate;
|
|
}
|
|
|
|
const userActivityData = await Inactivity.updateUserActivity(this.userActivityResource, this.sessionTokenName, seenAt);
|
|
|
|
this.resetInactivityDataAndTimers(userActivityData);
|
|
},
|
|
stopInactivity() {
|
|
this.modalVisibilityCheckRan = false;
|
|
this.isOpen = false;
|
|
this.isUserActive = false;
|
|
this.userActivityIsoDate = '';
|
|
|
|
this.removeEventListeners();
|
|
this.clearAllTimeouts();
|
|
},
|
|
resetInactivityDataAndTimers(userActivityData) {
|
|
const data = Inactivity.parseTTLData(userActivityData);
|
|
|
|
this.modalVisibilityCheckRan = false;
|
|
this.isOpen = false;
|
|
this.isUserActive = false;
|
|
this.userActivityIsoDate = '';
|
|
|
|
this.courtesyTimer = data.courtesyTimer;
|
|
this.courtesyCountdown = data.courtesyCountdown;
|
|
this.showModalAfter = data.showModalAfter;
|
|
this.sessionTokenName = data.sessionTokenName;
|
|
this.expiresAt = data.expiresAt;
|
|
|
|
const shownAfter = Math.round(((data.showModalAfter || 0) / 60) * 100) / 100;
|
|
const shownFor = Math.round(((data.courtesyTimer || 0) / 60) * 100) / 100;
|
|
|
|
console.debug(`UI inactivity modal (backend-based) will show after ${ shownAfter }(m) and be shown for ${ shownFor }(m)`); // eslint-disable-line no-console
|
|
|
|
this.clearAllTimeouts();
|
|
this.trackInactivity();
|
|
},
|
|
clearAllTimeouts() {
|
|
clearTimeout(this.inactivityTimeoutId);
|
|
clearTimeout(this.courtesyTimerId);
|
|
}
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<ModalWithCard
|
|
v-if="isOpen"
|
|
ref="inactivityModal"
|
|
name="inactivityModal"
|
|
save-text="Continue"
|
|
>
|
|
<template #title>
|
|
{{ t('inactivity.title') }}
|
|
</template>
|
|
<span>{{ courtesyCountdown }}</span>
|
|
|
|
<template #content>
|
|
<Banner color="info">
|
|
{{ t('inactivity.banner') }}
|
|
</Banner>
|
|
|
|
<p>
|
|
{{ t('inactivity.content') }}
|
|
</p>
|
|
|
|
<PercentageBar
|
|
class="mt-20"
|
|
:modelValue="timerPercentageLeft"
|
|
:color-stops="colorStops"
|
|
/>
|
|
</template>
|
|
|
|
<template
|
|
#footer
|
|
>
|
|
<div class="card-actions">
|
|
<button
|
|
class="btn role-tertiary bg-primary"
|
|
@click.prevent="resetUserActivity(true)"
|
|
>
|
|
<t k="inactivity.cta" />
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</ModalWithCard>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.card-actions {
|
|
display: flex;
|
|
width: 100%;
|
|
justify-content: flex-end;
|
|
}
|
|
</style>
|