web/satellite: modularize session timeout code
This change adds a Vue composable and wrapper component for reusing session timeout code. In the future, the composable will be used to implement session timeout in the Vuetify project. References #6147 Change-Id: Ibd049a8a8041007319798ac4187a6ed6487b591f
This commit is contained in:
parent
6c035a70af
commit
75f2152ae3
40
web/satellite/src/components/utils/SessionWrapper.vue
Normal file
40
web/satellite/src/components/utils/SessionWrapper.vue
Normal file
@ -0,0 +1,40 @@
|
||||
// Copyright (C) 2023 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<slot v-bind="sessionTimeout" />
|
||||
<InactivityModal
|
||||
v-if="sessionTimeout.inactivityModalShown.value"
|
||||
:on-continue="() => sessionTimeout.refreshSession(true)"
|
||||
:on-logout="sessionTimeout.handleInactive"
|
||||
:on-close="() => sessionTimeout.inactivityModalShown.value = false"
|
||||
:initial-seconds="INACTIVITY_MODAL_DURATION / 1000"
|
||||
/>
|
||||
<SessionExpiredModal v-if="sessionTimeout.sessionExpiredModalShown.value" :on-redirect="redirectToLogin" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
|
||||
import { useSessionTimeout, INACTIVITY_MODAL_DURATION } from '@/composables/useSessionTimeout';
|
||||
import { RouteConfig } from '@/types/router';
|
||||
|
||||
import InactivityModal from '@/components/modals/InactivityModal.vue';
|
||||
import SessionExpiredModal from '@/components/modals/SessionExpiredModal.vue';
|
||||
|
||||
const analyticsStore = useAnalyticsStore();
|
||||
|
||||
const sessionTimeout = useSessionTimeout();
|
||||
const router = useRouter();
|
||||
|
||||
/**
|
||||
* Redirects to log in screen.
|
||||
*/
|
||||
function redirectToLogin(): void {
|
||||
analyticsStore.pageVisit(RouteConfig.Login.path);
|
||||
router.push(RouteConfig.Login.path);
|
||||
|
||||
sessionTimeout.sessionExpiredModalShown.value = false;
|
||||
}
|
||||
</script>
|
280
web/satellite/src/composables/useSessionTimeout.ts
Normal file
280
web/satellite/src/composables/useSessionTimeout.ts
Normal file
@ -0,0 +1,280 @@
|
||||
// Copyright (C) 2023 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { WatchStopHandle, computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||
|
||||
import { AuthHttpApi } from '@/api/auth';
|
||||
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
|
||||
import { useConfigStore } from '@/store/modules/configStore';
|
||||
import { useObjectBrowserStore } from '@/store/modules/objectBrowserStore';
|
||||
import { DEFAULT_USER_SETTINGS, useUsersStore } from '@/store/modules/usersStore';
|
||||
import { useBucketsStore } from '@/store/modules/bucketsStore';
|
||||
import { useProjectMembersStore } from '@/store/modules/projectMembersStore';
|
||||
import { useABTestingStore } from '@/store/modules/abTestingStore';
|
||||
import { useBillingStore } from '@/store/modules/billingStore';
|
||||
import { useAccessGrantsStore } from '@/store/modules/accessGrantsStore';
|
||||
import { useAppStore } from '@/store/modules/appStore';
|
||||
import { useProjectsStore } from '@/store/modules/projectsStore';
|
||||
import { useNotificationsStore } from '@/store/modules/notificationsStore';
|
||||
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
|
||||
import { useNotify } from '@/utils/hooks';
|
||||
import { LocalData } from '@/utils/localData';
|
||||
import { MODALS } from '@/utils/constants/appStatePopUps';
|
||||
|
||||
const RESET_ACTIVITY_EVENTS: readonly string[] = ['keypress', 'mousemove', 'mousedown', 'touchmove'];
|
||||
export const INACTIVITY_MODAL_DURATION = 60000;
|
||||
|
||||
export function useSessionTimeout() {
|
||||
const initialized = ref<boolean>(false);
|
||||
|
||||
const inactivityTimerId = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||
const sessionRefreshTimerId = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||
const debugTimerId = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||
const debugTimerText = ref<string>('');
|
||||
const isSessionActive = ref<boolean>(false);
|
||||
const isSessionRefreshing = ref<boolean>(false);
|
||||
const inactivityModalShown = ref<boolean>(false);
|
||||
const sessionExpiredModalShown = ref<boolean>(false);
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const bucketsStore = useBucketsStore();
|
||||
const pmStore = useProjectMembersStore();
|
||||
const usersStore = useUsersStore();
|
||||
const abTestingStore = useABTestingStore();
|
||||
const billingStore = useBillingStore();
|
||||
const agStore = useAccessGrantsStore();
|
||||
const appStore = useAppStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
const notificationsStore = useNotificationsStore();
|
||||
const obStore = useObjectBrowserStore();
|
||||
|
||||
const notify = useNotify();
|
||||
|
||||
const auth: AuthHttpApi = new AuthHttpApi();
|
||||
|
||||
/**
|
||||
* Returns the session duration from the store.
|
||||
*/
|
||||
const sessionDuration = computed((): number => {
|
||||
const duration = (usersStore.state.settings.sessionDuration?.fullSeconds || configStore.state.config.inactivityTimerDuration) * 1000;
|
||||
const maxTimeout = 2.1427e+9; // 24.8 days https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value
|
||||
if (duration > maxTimeout) {
|
||||
return maxTimeout;
|
||||
}
|
||||
return duration;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the session refresh interval from the store.
|
||||
*/
|
||||
const sessionRefreshInterval = computed((): number => {
|
||||
return sessionDuration.value / 2;
|
||||
});
|
||||
|
||||
/**
|
||||
* Indicates whether to display the session timer for debugging.
|
||||
*/
|
||||
const debugTimerShown = computed((): boolean => {
|
||||
return configStore.state.config.inactivityTimerViewerEnabled && initialized.value;
|
||||
});
|
||||
|
||||
/**
|
||||
* Clears pinia stores and session timers, removes event listeners,
|
||||
* and displays the session expired modal.
|
||||
*/
|
||||
async function clearStoresAndTimers(): Promise<void> {
|
||||
await Promise.all([
|
||||
pmStore.clear(),
|
||||
projectsStore.clear(),
|
||||
usersStore.clear(),
|
||||
agStore.stopWorker(),
|
||||
agStore.clear(),
|
||||
notificationsStore.clear(),
|
||||
bucketsStore.clear(),
|
||||
appStore.clear(),
|
||||
billingStore.clear(),
|
||||
abTestingStore.reset(),
|
||||
obStore.clear(),
|
||||
]);
|
||||
|
||||
RESET_ACTIVITY_EVENTS.forEach((eventName: string) => {
|
||||
document.removeEventListener(eventName, onSessionActivity);
|
||||
});
|
||||
clearSessionTimers();
|
||||
inactivityModalShown.value = false;
|
||||
sessionExpiredModalShown.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs logout and cleans event listeners and session timers.
|
||||
*/
|
||||
async function handleInactive(): Promise<void> {
|
||||
await clearStoresAndTimers();
|
||||
|
||||
try {
|
||||
await auth.logout();
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorUnauthorized) return;
|
||||
|
||||
notify.notifyError(error, AnalyticsErrorEventSource.OVERALL_SESSION_EXPIRED_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears timers associated with session refreshing and inactivity.
|
||||
*/
|
||||
function clearSessionTimers(): void {
|
||||
[inactivityTimerId.value, sessionRefreshTimerId.value, debugTimerId.value].forEach(id => {
|
||||
if (id !== null) clearTimeout(id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds DOM event listeners and starts session timers.
|
||||
*/
|
||||
function setupSessionTimers(): void {
|
||||
if (initialized.value || !configStore.state.config.inactivityTimerEnabled) return;
|
||||
|
||||
const expiresAt = LocalData.getSessionExpirationDate();
|
||||
|
||||
if (expiresAt) {
|
||||
RESET_ACTIVITY_EVENTS.forEach((eventName: string) => {
|
||||
document.addEventListener(eventName, onSessionActivity, false);
|
||||
});
|
||||
|
||||
if (expiresAt.getTime() - sessionDuration.value + sessionRefreshInterval.value < Date.now()) {
|
||||
refreshSession();
|
||||
}
|
||||
|
||||
restartSessionTimers();
|
||||
}
|
||||
|
||||
initialized.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts timers associated with session refreshing and inactivity.
|
||||
*/
|
||||
function restartSessionTimers(): void {
|
||||
sessionRefreshTimerId.value = setTimeout(async () => {
|
||||
sessionRefreshTimerId.value = null;
|
||||
if (isSessionActive.value) {
|
||||
await refreshSession();
|
||||
}
|
||||
}, sessionRefreshInterval.value);
|
||||
|
||||
inactivityTimerId.value = setTimeout(async () => {
|
||||
if (obStore.uploadingLength) {
|
||||
await refreshSession();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSessionActive.value) return;
|
||||
inactivityModalShown.value = true;
|
||||
inactivityTimerId.value = setTimeout(async () => {
|
||||
await clearStoresAndTimers();
|
||||
notify.notify('Your session was timed out.');
|
||||
}, INACTIVITY_MODAL_DURATION);
|
||||
}, sessionDuration.value - INACTIVITY_MODAL_DURATION);
|
||||
|
||||
if (!debugTimerShown.value) return;
|
||||
|
||||
const debugTimer = () => {
|
||||
const expiresAt = LocalData.getSessionExpirationDate();
|
||||
|
||||
if (expiresAt) {
|
||||
const ms = Math.max(0, expiresAt.getTime() - Date.now());
|
||||
const secs = Math.floor(ms / 1000) % 60;
|
||||
|
||||
debugTimerText.value = `${Math.floor(ms / 60000)}:${(secs < 10 ? '0' : '') + secs}`;
|
||||
|
||||
if (ms > 1000) {
|
||||
debugTimerId.value = setTimeout(debugTimer, 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
debugTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes session and resets session timers.
|
||||
* @param manual - whether the user manually refreshed session. i.e.: clicked "Stay Logged In".
|
||||
*/
|
||||
async function refreshSession(manual = false): Promise<void> {
|
||||
isSessionRefreshing.value = true;
|
||||
|
||||
try {
|
||||
LocalData.setSessionExpirationDate(await auth.refreshSession());
|
||||
} catch (error) {
|
||||
error.message = (error instanceof ErrorUnauthorized) ? 'Your session was timed out.' : error.message;
|
||||
notify.notifyError(error, AnalyticsErrorEventSource.ALL_PROJECT_DASHBOARD);
|
||||
await handleInactive();
|
||||
isSessionRefreshing.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
clearSessionTimers();
|
||||
restartSessionTimers();
|
||||
inactivityModalShown.value = false;
|
||||
isSessionActive.value = false;
|
||||
isSessionRefreshing.value = false;
|
||||
|
||||
if (manual && !usersStore.state.settings.sessionDuration) {
|
||||
appStore.updateActiveModal(MODALS.editSessionTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets inactivity timer and refreshes session if necessary.
|
||||
*/
|
||||
async function onSessionActivity(): Promise<void> {
|
||||
if (inactivityModalShown.value || isSessionActive.value) return;
|
||||
|
||||
if (sessionRefreshTimerId.value === null && !isSessionRefreshing.value) {
|
||||
await refreshSession();
|
||||
}
|
||||
|
||||
isSessionActive.value = true;
|
||||
}
|
||||
|
||||
let unwatch: WatchStopHandle | null = null;
|
||||
let unwatchImmediately = false;
|
||||
unwatch = watch(() => usersStore.state.settings, newSettings => {
|
||||
if (newSettings !== DEFAULT_USER_SETTINGS) {
|
||||
setupSessionTimers();
|
||||
if (unwatch) {
|
||||
unwatch();
|
||||
return;
|
||||
}
|
||||
unwatchImmediately = true;
|
||||
}
|
||||
}, { immediate: true });
|
||||
if (unwatchImmediately) unwatch();
|
||||
|
||||
usersStore.$onAction(({ name, after, args }) => {
|
||||
if (name === 'clear') clearSessionTimers();
|
||||
else if (name === 'updateSettings') {
|
||||
if (args[0].sessionDuration && args[0].sessionDuration !== usersStore.state.settings.sessionDuration?.nanoseconds) {
|
||||
after((_) => refreshSession());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearSessionTimers();
|
||||
RESET_ACTIVITY_EVENTS.forEach((eventName: string) => {
|
||||
document.removeEventListener(eventName, onSessionActivity);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
inactivityModalShown,
|
||||
sessionExpiredModalShown,
|
||||
debugTimerShown,
|
||||
debugTimerText,
|
||||
refreshSession,
|
||||
handleInactive,
|
||||
clearStoresAndTimers,
|
||||
};
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, reactive } from 'vue';
|
||||
import { computed, reactive, readonly } from 'vue';
|
||||
|
||||
import {
|
||||
DisableMFARequest,
|
||||
@ -15,9 +15,11 @@ import {
|
||||
import { AuthHttpApi } from '@/api/auth';
|
||||
import { useConfigStore } from '@/store/modules/configStore';
|
||||
|
||||
export const DEFAULT_USER_SETTINGS = readonly(new UserSettings());
|
||||
|
||||
export class UsersState {
|
||||
public user: User = new User();
|
||||
public settings: UserSettings = new UserSettings();
|
||||
public settings: Readonly<UserSettings> = DEFAULT_USER_SETTINGS;
|
||||
public userMFASecret = '';
|
||||
public userMFARecoveryCodes: string[] = [];
|
||||
}
|
||||
@ -92,7 +94,7 @@ export const useUsersStore = defineStore('users', () => {
|
||||
|
||||
function clear() {
|
||||
state.user = new User();
|
||||
state.settings = new UserSettings();
|
||||
state.settings = DEFAULT_USER_SETTINGS;
|
||||
state.userMFASecret = '';
|
||||
state.userMFARecoveryCodes = [];
|
||||
}
|
||||
|
@ -4,120 +4,115 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<BrandedLoader v-if="isLoading" />
|
||||
<div v-else class="dashboard__wrap">
|
||||
<div class="dashboard__wrap__main-area">
|
||||
<NavigationArea v-if="!isNavigationHidden" class="dashboard__wrap__main-area__navigation" />
|
||||
<MobileNavigation v-if="!isNavigationHidden" class="dashboard__wrap__main-area__mobile-navigation" />
|
||||
<div
|
||||
class="dashboard__wrap__main-area__content-wrap"
|
||||
:class="{ 'no-nav': isNavigationHidden }"
|
||||
>
|
||||
<div ref="dashboardContent" class="dashboard__wrap__main-area__content-wrap__container">
|
||||
<BetaSatBar v-if="isBetaSatellite" />
|
||||
<MFARecoveryCodeBar v-if="showMFARecoveryCodeBar" :open-generate-modal="generateNewMFARecoveryCodes" />
|
||||
<div class="dashboard__wrap__main-area__content-wrap__container__content banners">
|
||||
<ProjectInvitationBanner v-if="isProjectInvitationBannerShown" />
|
||||
<SessionWrapper v-else>
|
||||
<template #default="session">
|
||||
<div class="dashboard__wrap">
|
||||
<div class="dashboard__wrap__main-area">
|
||||
<NavigationArea v-if="!isNavigationHidden" class="dashboard__wrap__main-area__navigation" />
|
||||
<MobileNavigation v-if="!isNavigationHidden" class="dashboard__wrap__main-area__mobile-navigation" />
|
||||
<div
|
||||
class="dashboard__wrap__main-area__content-wrap"
|
||||
:class="{ 'no-nav': isNavigationHidden }"
|
||||
>
|
||||
<div ref="dashboardContent" class="dashboard__wrap__main-area__content-wrap__container">
|
||||
<BetaSatBar v-if="isBetaSatellite" />
|
||||
<MFARecoveryCodeBar v-if="showMFARecoveryCodeBar" :open-generate-modal="generateNewMFARecoveryCodes" />
|
||||
<div class="dashboard__wrap__main-area__content-wrap__container__content banners">
|
||||
<ProjectInvitationBanner v-if="isProjectInvitationBannerShown" />
|
||||
|
||||
<UpgradeNotification
|
||||
v-if="isPaidTierBannerShown"
|
||||
:open-add-p-m-modal="togglePMModal"
|
||||
/>
|
||||
<UpgradeNotification
|
||||
v-if="isPaidTierBannerShown"
|
||||
:open-add-p-m-modal="togglePMModal"
|
||||
/>
|
||||
|
||||
<v-banner
|
||||
v-if="isAccountFrozen && !isLoading && dashboardContent"
|
||||
severity="critical"
|
||||
:dashboard-ref="dashboardContent"
|
||||
>
|
||||
<template #text>
|
||||
<p class="medium">Your account was frozen due to billing issues. Please update your payment information.</p>
|
||||
<p class="link" @click.stop.self="redirectToBillingPage">To Billing Page</p>
|
||||
</template>
|
||||
</v-banner>
|
||||
<v-banner
|
||||
v-if="isAccountFrozen && !isLoading && dashboardContent"
|
||||
severity="critical"
|
||||
:dashboard-ref="dashboardContent"
|
||||
>
|
||||
<template #text>
|
||||
<p class="medium">Your account was frozen due to billing issues. Please update your payment information.</p>
|
||||
<p class="link" @click.stop.self="redirectToBillingPage">To Billing Page</p>
|
||||
</template>
|
||||
</v-banner>
|
||||
|
||||
<v-banner
|
||||
v-if="isAccountWarned && !isLoading && dashboardContent"
|
||||
severity="warning"
|
||||
:dashboard-ref="dashboardContent"
|
||||
>
|
||||
<template #text>
|
||||
<p class="medium">Your account will be frozen soon due to billing issues. Please update your payment information.</p>
|
||||
<p class="link" @click.stop.self="redirectToBillingPage">To Billing Page</p>
|
||||
</template>
|
||||
</v-banner>
|
||||
<v-banner
|
||||
v-if="isAccountWarned && !isLoading && dashboardContent"
|
||||
severity="warning"
|
||||
:dashboard-ref="dashboardContent"
|
||||
>
|
||||
<template #text>
|
||||
<p class="medium">Your account will be frozen soon due to billing issues. Please update your payment information.</p>
|
||||
<p class="link" @click.stop.self="redirectToBillingPage">To Billing Page</p>
|
||||
</template>
|
||||
</v-banner>
|
||||
|
||||
<v-banner
|
||||
v-if="limitState.hundredIsShown && !isLoading && dashboardContent"
|
||||
severity="critical"
|
||||
:on-click="() => setIsHundredLimitModalShown(true)"
|
||||
:dashboard-ref="dashboardContent"
|
||||
>
|
||||
<template #text>
|
||||
<p class="medium">{{ limitState.hundredLabel }}</p>
|
||||
<p class="link" @click.stop.self="togglePMModal">Upgrade now</p>
|
||||
</template>
|
||||
</v-banner>
|
||||
<v-banner
|
||||
v-if="limitState.eightyIsShown && !isLoading && dashboardContent"
|
||||
severity="warning"
|
||||
:on-click="() => setIsEightyLimitModalShown(true)"
|
||||
:dashboard-ref="dashboardContent"
|
||||
>
|
||||
<template #text>
|
||||
<p class="medium">{{ limitState.eightyLabel }}</p>
|
||||
<p class="link" @click.stop.self="togglePMModal">Upgrade now</p>
|
||||
</template>
|
||||
</v-banner>
|
||||
</div>
|
||||
<router-view class="dashboard__wrap__main-area__content-wrap__container__content" />
|
||||
<div class="dashboard__wrap__main-area__content-wrap__container__content banners-bottom">
|
||||
<UploadNotification
|
||||
v-if="isLargeUploadWarningNotificationShown"
|
||||
wording-bold="Trying to upload a large file?"
|
||||
wording="Check the recommendations for your use case"
|
||||
:notification-icon="WarningIcon"
|
||||
info-notification
|
||||
:on-close-click="onWarningNotificationCloseClick"
|
||||
/>
|
||||
<v-banner
|
||||
v-if="limitState.hundredIsShown && !isLoading && dashboardContent"
|
||||
severity="critical"
|
||||
:on-click="() => setIsHundredLimitModalShown(true)"
|
||||
:dashboard-ref="dashboardContent"
|
||||
>
|
||||
<template #text>
|
||||
<p class="medium">{{ limitState.hundredLabel }}</p>
|
||||
<p class="link" @click.stop.self="togglePMModal">Upgrade now</p>
|
||||
</template>
|
||||
</v-banner>
|
||||
<v-banner
|
||||
v-if="limitState.eightyIsShown && !isLoading && dashboardContent"
|
||||
severity="warning"
|
||||
:on-click="() => setIsEightyLimitModalShown(true)"
|
||||
:dashboard-ref="dashboardContent"
|
||||
>
|
||||
<template #text>
|
||||
<p class="medium">{{ limitState.eightyLabel }}</p>
|
||||
<p class="link" @click.stop.self="togglePMModal">Upgrade now</p>
|
||||
</template>
|
||||
</v-banner>
|
||||
</div>
|
||||
<router-view class="dashboard__wrap__main-area__content-wrap__container__content" />
|
||||
<div class="dashboard__wrap__main-area__content-wrap__container__content banners-bottom">
|
||||
<UploadNotification
|
||||
v-if="isLargeUploadWarningNotificationShown"
|
||||
wording-bold="Trying to upload a large file?"
|
||||
wording="Check the recommendations for your use case"
|
||||
:notification-icon="WarningIcon"
|
||||
info-notification
|
||||
:on-close-click="onWarningNotificationCloseClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="debugTimerShown && !isLoading" class="dashboard__debug-timer">
|
||||
<p>Remaining session time: <b class="dashboard__debug-timer__bold">{{ debugTimerText }}</b></p>
|
||||
</div>
|
||||
<limit-warning-modal
|
||||
v-if="isHundredLimitModalShown && !isLoading"
|
||||
severity="critical"
|
||||
:on-close="() => setIsHundredLimitModalShown(false)"
|
||||
:title="limitState.hundredModalTitle"
|
||||
:limit-type="limitState.hundredModalLimitType"
|
||||
:on-upgrade="togglePMModal"
|
||||
/>
|
||||
<limit-warning-modal
|
||||
v-if="isEightyLimitModalShown && !isLoading"
|
||||
severity="warning"
|
||||
:on-close="() => setIsEightyLimitModalShown(false)"
|
||||
:title="limitState.eightyModalTitle"
|
||||
:limit-type="limitState.eightyModalLimitType"
|
||||
:on-upgrade="togglePMModal"
|
||||
/>
|
||||
<AllModals />
|
||||
<ObjectsUploadingModal v-if="isObjectsUploadModal" />
|
||||
<!-- IMPORTANT! Make sure these 2 modals are positioned as the last elements here so that they are shown on top of everything else -->
|
||||
<InactivityModal
|
||||
v-if="inactivityModalShown"
|
||||
:on-continue="() => refreshSession(true)"
|
||||
:on-logout="handleInactive"
|
||||
:on-close="closeInactivityModal"
|
||||
:initial-seconds="inactivityModalTime / 1000"
|
||||
/>
|
||||
<SessionExpiredModal v-if="sessionExpiredModalShown" :on-redirect="redirectToLogin" />
|
||||
<div v-if="session.debugTimerShown.value && !isLoading" class="dashboard__debug-timer">
|
||||
<p>Remaining session time: <b class="dashboard__debug-timer__bold">{{ session.debugTimerText.value }}</b></p>
|
||||
</div>
|
||||
<limit-warning-modal
|
||||
v-if="isHundredLimitModalShown && !isLoading"
|
||||
severity="critical"
|
||||
:on-close="() => setIsHundredLimitModalShown(false)"
|
||||
:title="limitState.hundredModalTitle"
|
||||
:limit-type="limitState.hundredModalLimitType"
|
||||
:on-upgrade="togglePMModal"
|
||||
/>
|
||||
<limit-warning-modal
|
||||
v-if="isEightyLimitModalShown && !isLoading"
|
||||
severity="warning"
|
||||
:on-close="() => setIsEightyLimitModalShown(false)"
|
||||
:title="limitState.eightyModalTitle"
|
||||
:limit-type="limitState.eightyModalLimitType"
|
||||
:on-upgrade="togglePMModal"
|
||||
/>
|
||||
<AllModals />
|
||||
<ObjectsUploadingModal v-if="isObjectsUploadModal" />
|
||||
</template>
|
||||
</SessionWrapper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
|
||||
@ -127,27 +122,21 @@ import { Project } from '@/types/projects';
|
||||
import { FetchState } from '@/utils/constants/fetchStateEnum';
|
||||
import { LocalData } from '@/utils/localData';
|
||||
import { User } from '@/types/users';
|
||||
import { AuthHttpApi } from '@/api/auth';
|
||||
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
|
||||
import { useNotify } from '@/utils/hooks';
|
||||
import { MODALS } from '@/utils/constants/appStatePopUps';
|
||||
import { useABTestingStore } from '@/store/modules/abTestingStore';
|
||||
import { useUsersStore } from '@/store/modules/usersStore';
|
||||
import { useProjectMembersStore } from '@/store/modules/projectMembersStore';
|
||||
import { useBillingStore } from '@/store/modules/billingStore';
|
||||
import { useAppStore } from '@/store/modules/appStore';
|
||||
import { useAccessGrantsStore } from '@/store/modules/accessGrantsStore';
|
||||
import { useBucketsStore } from '@/store/modules/bucketsStore';
|
||||
import { useProjectsStore } from '@/store/modules/projectsStore';
|
||||
import { useNotificationsStore } from '@/store/modules/notificationsStore';
|
||||
import { useObjectBrowserStore } from '@/store/modules/objectBrowserStore';
|
||||
import { useConfigStore } from '@/store/modules/configStore';
|
||||
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
|
||||
|
||||
import UploadNotification from '@/components/notifications/UploadNotification.vue';
|
||||
import NavigationArea from '@/components/navigation/NavigationArea.vue';
|
||||
import InactivityModal from '@/components/modals/InactivityModal.vue';
|
||||
import SessionExpiredModal from '@/components/modals/SessionExpiredModal.vue';
|
||||
import SessionWrapper from '@/components/utils/SessionWrapper.vue';
|
||||
import BetaSatBar from '@/components/infoBars/BetaSatBar.vue';
|
||||
import MFARecoveryCodeBar from '@/components/infoBars/MFARecoveryCodeBar.vue';
|
||||
import AllModals from '@/components/modals/AllModals.vue';
|
||||
@ -162,60 +151,26 @@ import ObjectsUploadingModal from '@/components/modals/objectUpload/ObjectsUploa
|
||||
import WarningIcon from '@/../static/images/notifications/circleWarning.svg';
|
||||
|
||||
const analyticsStore = useAnalyticsStore();
|
||||
const bucketsStore = useBucketsStore();
|
||||
const configStore = useConfigStore();
|
||||
const appStore = useAppStore();
|
||||
const agStore = useAccessGrantsStore();
|
||||
const billingStore = useBillingStore();
|
||||
const pmStore = useProjectMembersStore();
|
||||
const usersStore = useUsersStore();
|
||||
const abTestingStore = useABTestingStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
const notificationsStore = useNotificationsStore();
|
||||
const obStore = useObjectBrowserStore();
|
||||
|
||||
const notify = useNotify();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const auth: AuthHttpApi = new AuthHttpApi();
|
||||
const resetActivityEvents: string[] = ['keypress', 'mousemove', 'mousedown', 'touchmove'];
|
||||
const inactivityModalTime = 60000;
|
||||
// Minimum number of recovery codes before the recovery code warning bar is shown.
|
||||
const recoveryCodeWarningThreshold = 4;
|
||||
|
||||
const inactivityTimerId = ref<ReturnType<typeof setTimeout> | null>();
|
||||
const sessionRefreshTimerId = ref<ReturnType<typeof setTimeout> | null>();
|
||||
const debugTimerId = ref<ReturnType<typeof setTimeout> | null>();
|
||||
const inactivityModalShown = ref<boolean>(false);
|
||||
const sessionExpiredModalShown = ref<boolean>(false);
|
||||
const isSessionActive = ref<boolean>(false);
|
||||
const isSessionRefreshing = ref<boolean>(false);
|
||||
const isHundredLimitModalShown = ref<boolean>(false);
|
||||
const isEightyLimitModalShown = ref<boolean>(false);
|
||||
const debugTimerText = ref<string>('');
|
||||
|
||||
const dashboardContent = ref<HTMLElement | null>(null);
|
||||
|
||||
/**
|
||||
* Returns the session duration from the store.
|
||||
*/
|
||||
const sessionDuration = computed((): number => {
|
||||
const duration = (usersStore.state.settings.sessionDuration?.fullSeconds || configStore.state.config.inactivityTimerDuration) * 1000;
|
||||
const maxTimeout = 2.1427e+9; // 24.8 days https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value
|
||||
if (duration > maxTimeout) {
|
||||
return maxTimeout;
|
||||
}
|
||||
return duration;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the session refresh interval from the store.
|
||||
*/
|
||||
const sessionRefreshInterval = computed((): number => {
|
||||
return sessionDuration.value / 2;
|
||||
});
|
||||
|
||||
/**
|
||||
* Indicates whether objects upload modal should be shown.
|
||||
*/
|
||||
@ -223,13 +178,6 @@ const isObjectsUploadModal = computed((): boolean => {
|
||||
return configStore.state.config.newUploadModalEnabled && appStore.state.isUploadingModal;
|
||||
});
|
||||
|
||||
/**
|
||||
* Indicates whether to display the session timer for debugging.
|
||||
*/
|
||||
const debugTimerShown = computed((): boolean => {
|
||||
return configStore.state.config.inactivityTimerViewerEnabled;
|
||||
});
|
||||
|
||||
/**
|
||||
* Indicates if account was frozen due to billing issues.
|
||||
*/
|
||||
@ -414,81 +362,6 @@ function storeProject(projectID: string): void {
|
||||
LocalData.setSelectedProjectId(projectID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears timers associated with session refreshing and inactivity.
|
||||
*/
|
||||
function clearSessionTimers(): void {
|
||||
[inactivityTimerId.value, sessionRefreshTimerId.value, debugTimerId.value].forEach(id => {
|
||||
if (id !== null) clearTimeout(id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds DOM event listeners and starts session timers.
|
||||
*/
|
||||
function setupSessionTimers(): void {
|
||||
if (!configStore.state.config.inactivityTimerEnabled) return;
|
||||
|
||||
const expiresAt = LocalData.getSessionExpirationDate();
|
||||
|
||||
if (expiresAt) {
|
||||
resetActivityEvents.forEach((eventName: string) => {
|
||||
document.addEventListener(eventName, onSessionActivity, false);
|
||||
});
|
||||
|
||||
if (expiresAt.getTime() - sessionDuration.value + sessionRefreshInterval.value < Date.now()) {
|
||||
refreshSession();
|
||||
}
|
||||
|
||||
restartSessionTimers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts timers associated with session refreshing and inactivity.
|
||||
*/
|
||||
function restartSessionTimers(): void {
|
||||
sessionRefreshTimerId.value = setTimeout(async () => {
|
||||
sessionRefreshTimerId.value = null;
|
||||
if (isSessionActive.value) {
|
||||
await refreshSession();
|
||||
}
|
||||
}, sessionRefreshInterval.value);
|
||||
|
||||
inactivityTimerId.value = setTimeout(async () => {
|
||||
if (obStore.uploadingLength) {
|
||||
await refreshSession();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSessionActive.value) return;
|
||||
inactivityModalShown.value = true;
|
||||
inactivityTimerId.value = setTimeout(async () => {
|
||||
await clearStoreAndTimers();
|
||||
notify.notify('Your session was timed out.');
|
||||
}, inactivityModalTime);
|
||||
}, sessionDuration.value - inactivityModalTime);
|
||||
|
||||
if (!debugTimerShown.value) return;
|
||||
|
||||
const debugTimer = () => {
|
||||
const expiresAt = LocalData.getSessionExpirationDate();
|
||||
|
||||
if (expiresAt) {
|
||||
const ms = Math.max(0, expiresAt.getTime() - Date.now());
|
||||
const secs = Math.floor(ms/1000)%60;
|
||||
|
||||
debugTimerText.value = `${Math.floor(ms/60000)}:${(secs<10 ? '0' : '')+secs}`;
|
||||
|
||||
if (ms > 1000) {
|
||||
debugTimerId.value = setTimeout(debugTimer, 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
debugTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if stored project is in fetched projects array and selects it.
|
||||
* Selects first fetched project if check is not successful.
|
||||
@ -507,85 +380,6 @@ function selectProject(fetchedProjects: Project[]): void {
|
||||
storeProject(fetchedProjects[0].id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes session and resets session timers.
|
||||
* @param manual - whether the user manually refreshed session. i.e.: clicked "Stay Logged In".
|
||||
*/
|
||||
async function refreshSession(manual = false): Promise<void> {
|
||||
isSessionRefreshing.value = true;
|
||||
|
||||
try {
|
||||
LocalData.setSessionExpirationDate(await auth.refreshSession());
|
||||
} catch (error) {
|
||||
error.message = (error instanceof ErrorUnauthorized) ? 'Your session was timed out.' : error.message;
|
||||
notify.notifyError(error, AnalyticsErrorEventSource.OVERALL_SESSION_EXPIRED_ERROR);
|
||||
await handleInactive();
|
||||
isSessionRefreshing.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
clearSessionTimers();
|
||||
restartSessionTimers();
|
||||
inactivityModalShown.value = false;
|
||||
isSessionActive.value = false;
|
||||
isSessionRefreshing.value = false;
|
||||
|
||||
if (manual && !usersStore.state.settings.sessionDuration) {
|
||||
appStore.updateActiveModal(MODALS.editSessionTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects to log in screen.
|
||||
*/
|
||||
function redirectToLogin(): void {
|
||||
analyticsStore.pageVisit(RouteConfig.Login.path);
|
||||
router.push(RouteConfig.Login.path);
|
||||
|
||||
sessionExpiredModalShown.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears pinia stores and timers.
|
||||
*/
|
||||
async function clearStoreAndTimers(): Promise<void> {
|
||||
await Promise.all([
|
||||
pmStore.clear(),
|
||||
projectsStore.clear(),
|
||||
usersStore.clear(),
|
||||
agStore.stopWorker(),
|
||||
agStore.clear(),
|
||||
notificationsStore.clear(),
|
||||
bucketsStore.clear(),
|
||||
appStore.clear(),
|
||||
billingStore.clear(),
|
||||
abTestingStore.reset(),
|
||||
obStore.clear(),
|
||||
]);
|
||||
|
||||
resetActivityEvents.forEach((eventName: string) => {
|
||||
document.removeEventListener(eventName, onSessionActivity);
|
||||
});
|
||||
clearSessionTimers();
|
||||
inactivityModalShown.value = false;
|
||||
sessionExpiredModalShown.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs logout and cleans event listeners and session timers.
|
||||
*/
|
||||
async function handleInactive(): Promise<void> {
|
||||
await clearStoreAndTimers();
|
||||
|
||||
try {
|
||||
await auth.logout();
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorUnauthorized) return;
|
||||
|
||||
notify.notifyError(error, AnalyticsErrorEventSource.OVERALL_SESSION_EXPIRED_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
function setIsEightyLimitModalShown(value: boolean): void {
|
||||
isEightyLimitModalShown.value = value;
|
||||
}
|
||||
@ -625,13 +419,6 @@ function togglePMModal(): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables session inactivity modal visibility.
|
||||
*/
|
||||
function closeInactivityModal(): void {
|
||||
inactivityModalShown.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects to Billing Page.
|
||||
*/
|
||||
@ -639,41 +426,17 @@ async function redirectToBillingPage(): Promise<void> {
|
||||
await router.push(RouteConfig.Account.with(RouteConfig.Billing.with(RouteConfig.BillingPaymentMethods)).path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets inactivity timer and refreshes session if necessary.
|
||||
*/
|
||||
async function onSessionActivity(): Promise<void> {
|
||||
if (inactivityModalShown.value || isSessionActive.value) return;
|
||||
|
||||
if (sessionRefreshTimerId.value === null && !isSessionRefreshing.value) {
|
||||
await refreshSession();
|
||||
}
|
||||
|
||||
isSessionActive.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook after initial render.
|
||||
* Pre-fetches user`s and project information.
|
||||
*/
|
||||
onMounted(async () => {
|
||||
usersStore.$onAction(({ name, after, args }) => {
|
||||
if (name === 'clear') clearSessionTimers();
|
||||
else if (name === 'updateSettings') {
|
||||
if (args[0].sessionDuration && args[0].sessionDuration !== usersStore.state.settings.sessionDuration?.nanoseconds) {
|
||||
after((_) => refreshSession());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
usersStore.getUser(),
|
||||
abTestingStore.fetchValues(),
|
||||
usersStore.getSettings(),
|
||||
]);
|
||||
|
||||
setupSessionTimers();
|
||||
} catch (error) {
|
||||
if (!(error instanceof ErrorUnauthorized)) {
|
||||
appStore.changeState(FetchState.ERROR);
|
||||
@ -751,13 +514,6 @@ onMounted(async () => {
|
||||
|
||||
appStore.changeState(FetchState.LOADED);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearSessionTimers();
|
||||
resetActivityEvents.forEach((eventName: string) => {
|
||||
document.removeEventListener(eventName, onSessionActivity);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
@ -7,49 +7,43 @@
|
||||
<LoaderImage class="loading-icon" />
|
||||
</div>
|
||||
<div v-else class="all-dashboard">
|
||||
<div class="all-dashboard__bars">
|
||||
<BetaSatBar v-if="isBetaSatellite" />
|
||||
<MFARecoveryCodeBar v-if="showMFARecoveryCodeBar" :open-generate-modal="generateNewMFARecoveryCodes" />
|
||||
</div>
|
||||
<SessionWrapper>
|
||||
<div class="all-dashboard__bars">
|
||||
<BetaSatBar v-if="isBetaSatellite" />
|
||||
<MFARecoveryCodeBar v-if="showMFARecoveryCodeBar" :open-generate-modal="generateNewMFARecoveryCodes" />
|
||||
</div>
|
||||
|
||||
<heading class="all-dashboard__heading" />
|
||||
<heading class="all-dashboard__heading" />
|
||||
|
||||
<div class="all-dashboard__content" :class="{ 'no-x-padding': isMyProjectsPage }">
|
||||
<div class="all-dashboard__content__divider" />
|
||||
<div class="all-dashboard__content" :class="{ 'no-x-padding': isMyProjectsPage }">
|
||||
<div class="all-dashboard__content__divider" />
|
||||
|
||||
<router-view />
|
||||
<router-view />
|
||||
|
||||
<limit-warning-modal
|
||||
v-if="isHundredLimitModalShown && !isLoading"
|
||||
severity="critical"
|
||||
:on-close="() => setIsHundredLimitModalShown(false)"
|
||||
:title="limitState.hundredModalTitle"
|
||||
:limit-type="limitState.hundredModalLimitType"
|
||||
:on-upgrade="togglePMModal"
|
||||
/>
|
||||
<limit-warning-modal
|
||||
v-if="isEightyLimitModalShown && !isLoading"
|
||||
severity="warning"
|
||||
:on-close="() => setIsEightyLimitModalShown(false)"
|
||||
:title="limitState.eightyModalTitle"
|
||||
:limit-type="limitState.eightyModalLimitType"
|
||||
:on-upgrade="togglePMModal"
|
||||
/>
|
||||
<AllModals />
|
||||
</div>
|
||||
<InactivityModal
|
||||
v-if="inactivityModalShown"
|
||||
:on-continue="() => refreshSession(true)"
|
||||
:on-logout="handleInactive"
|
||||
:on-close="closeInactivityModal"
|
||||
:initial-seconds="inactivityModalTime / 1000"
|
||||
/>
|
||||
<SessionExpiredModal v-if="sessionExpiredModalShown" :on-redirect="redirectToLogin" />
|
||||
<limit-warning-modal
|
||||
v-if="isHundredLimitModalShown && !isLoading"
|
||||
severity="critical"
|
||||
:on-close="() => setIsHundredLimitModalShown(false)"
|
||||
:title="limitState.hundredModalTitle"
|
||||
:limit-type="limitState.hundredModalLimitType"
|
||||
:on-upgrade="togglePMModal"
|
||||
/>
|
||||
<limit-warning-modal
|
||||
v-if="isEightyLimitModalShown && !isLoading"
|
||||
severity="warning"
|
||||
:on-close="() => setIsEightyLimitModalShown(false)"
|
||||
:title="limitState.eightyModalTitle"
|
||||
:limit-type="limitState.eightyModalLimitType"
|
||||
:on-upgrade="togglePMModal"
|
||||
/>
|
||||
<AllModals />
|
||||
</div>
|
||||
</SessionWrapper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { MODALS } from '@/utils/constants/appStatePopUps';
|
||||
@ -61,25 +55,18 @@ import { useNotify } from '@/utils/hooks';
|
||||
import { RouteConfig } from '@/types/router';
|
||||
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
|
||||
import { FetchState } from '@/utils/constants/fetchStateEnum';
|
||||
import { LocalData } from '@/utils/localData';
|
||||
import { CouponType } from '@/types/coupons';
|
||||
import { AuthHttpApi } from '@/api/auth';
|
||||
import Heading from '@/views/all-dashboard/components/Heading.vue';
|
||||
import { useABTestingStore } from '@/store/modules/abTestingStore';
|
||||
import { useUsersStore } from '@/store/modules/usersStore';
|
||||
import { useProjectMembersStore } from '@/store/modules/projectMembersStore';
|
||||
import { useBillingStore } from '@/store/modules/billingStore';
|
||||
import { useAppStore } from '@/store/modules/appStore';
|
||||
import { useAccessGrantsStore } from '@/store/modules/accessGrantsStore';
|
||||
import { useBucketsStore } from '@/store/modules/bucketsStore';
|
||||
import { useProjectsStore } from '@/store/modules/projectsStore';
|
||||
import { useNotificationsStore } from '@/store/modules/notificationsStore';
|
||||
import { useObjectBrowserStore } from '@/store/modules/objectBrowserStore';
|
||||
import { useConfigStore } from '@/store/modules/configStore';
|
||||
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
|
||||
|
||||
import InactivityModal from '@/components/modals/InactivityModal.vue';
|
||||
import SessionExpiredModal from '@/components/modals/SessionExpiredModal.vue';
|
||||
import SessionWrapper from '@/components/utils/SessionWrapper.vue';
|
||||
import BetaSatBar from '@/components/infoBars/BetaSatBar.vue';
|
||||
import MFARecoveryCodeBar from '@/components/infoBars/MFARecoveryCodeBar.vue';
|
||||
import AllModals from '@/components/modals/AllModals.vue';
|
||||
@ -93,65 +80,23 @@ const notify = useNotify();
|
||||
|
||||
const analyticsStore = useAnalyticsStore();
|
||||
const configStore = useConfigStore();
|
||||
const bucketsStore = useBucketsStore();
|
||||
const pmStore = useProjectMembersStore();
|
||||
const usersStore = useUsersStore();
|
||||
const abTestingStore = useABTestingStore();
|
||||
const billingStore = useBillingStore();
|
||||
const agStore = useAccessGrantsStore();
|
||||
const appStore = useAppStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
const notificationsStore = useNotificationsStore();
|
||||
const obStore = useObjectBrowserStore();
|
||||
|
||||
const auth: AuthHttpApi = new AuthHttpApi();
|
||||
|
||||
const inactivityModalTime = 60000;
|
||||
// Minimum number of recovery codes before the recovery code warning bar is shown.
|
||||
const recoveryCodeWarningThreshold = 4;
|
||||
|
||||
const inactivityTimerId = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||
const sessionRefreshTimerId = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||
const debugTimerId = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||
const debugTimerText = ref<string>('');
|
||||
const resetActivityEvents: string[] = ['keypress', 'mousemove', 'mousedown', 'touchmove'];
|
||||
const inactivityModalShown = ref<boolean>(false);
|
||||
const sessionExpiredModalShown = ref<boolean>(false);
|
||||
const isSessionActive = ref<boolean>(false);
|
||||
const isSessionRefreshing = ref<boolean>(false);
|
||||
const isHundredLimitModalShown = ref<boolean>(false);
|
||||
const isEightyLimitModalShown = ref<boolean>(false);
|
||||
|
||||
/**
|
||||
* Returns the session duration from the store.
|
||||
*/
|
||||
const sessionDuration = computed((): number => {
|
||||
const duration = (usersStore.state.settings.sessionDuration?.fullSeconds || configStore.state.config.inactivityTimerDuration) * 1000;
|
||||
const maxTimeout = 2.1427e+9; // 24.8 days https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value
|
||||
if (duration > maxTimeout) {
|
||||
return maxTimeout;
|
||||
}
|
||||
return duration;
|
||||
});
|
||||
|
||||
const isMyProjectsPage = computed((): boolean => {
|
||||
return route.path === RouteConfig.AllProjectsDashboard.path;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the session refresh interval from the store.
|
||||
*/
|
||||
const sessionRefreshInterval = computed((): number => {
|
||||
return sessionDuration.value / 2;
|
||||
});
|
||||
|
||||
/**
|
||||
* Indicates whether to display the session timer for debugging.
|
||||
*/
|
||||
const debugTimerShown = computed((): boolean => {
|
||||
return configStore.state.config.inactivityTimerViewerEnabled;
|
||||
});
|
||||
|
||||
/**
|
||||
* Indicates if account was frozen due to billing issues.
|
||||
*/
|
||||
@ -263,57 +208,6 @@ function setIsHundredLimitModalShown(value: boolean): void {
|
||||
isHundredLimitModalShown.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects to log in screen.
|
||||
*/
|
||||
function redirectToLogin(): void {
|
||||
analyticsStore.pageVisit(RouteConfig.Login.path);
|
||||
router.push(RouteConfig.Login.path);
|
||||
|
||||
sessionExpiredModalShown.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears pinia stores and timers.
|
||||
*/
|
||||
async function clearStoreAndTimers(): Promise<void> {
|
||||
await Promise.all([
|
||||
pmStore.clear(),
|
||||
projectsStore.clear(),
|
||||
usersStore.clear(),
|
||||
agStore.stopWorker(),
|
||||
agStore.clear(),
|
||||
notificationsStore.clear(),
|
||||
bucketsStore.clear(),
|
||||
appStore.clear(),
|
||||
billingStore.clear(),
|
||||
abTestingStore.reset(),
|
||||
obStore.clear(),
|
||||
]);
|
||||
|
||||
resetActivityEvents.forEach((eventName: string) => {
|
||||
document.removeEventListener(eventName, onSessionActivity);
|
||||
});
|
||||
clearSessionTimers();
|
||||
inactivityModalShown.value = false;
|
||||
sessionExpiredModalShown.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs logout and cleans event listeners and session timers.
|
||||
*/
|
||||
async function handleInactive(): Promise<void> {
|
||||
await clearStoreAndTimers();
|
||||
|
||||
try {
|
||||
await auth.logout();
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorUnauthorized) return;
|
||||
|
||||
notify.notifyError(error, AnalyticsErrorEventSource.OVERALL_SESSION_EXPIRED_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates new MFA recovery codes and toggles popup visibility.
|
||||
*/
|
||||
@ -333,13 +227,6 @@ function toggleMFARecoveryModal(): void {
|
||||
appStore.updateActiveModal(MODALS.mfaRecovery);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables session inactivity modal visibility.
|
||||
*/
|
||||
function closeInactivityModal(): void {
|
||||
inactivityModalShown.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens add payment method modal.
|
||||
*/
|
||||
@ -352,144 +239,17 @@ function togglePMModal(): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears timers associated with session refreshing and inactivity.
|
||||
*/
|
||||
function clearSessionTimers(): void {
|
||||
[inactivityTimerId.value, sessionRefreshTimerId.value, debugTimerId.value].forEach(id => {
|
||||
if (id !== null) clearTimeout(id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds DOM event listeners and starts session timers.
|
||||
*/
|
||||
function setupSessionTimers(): void {
|
||||
if (!configStore.state.config.inactivityTimerEnabled) return;
|
||||
|
||||
const expiresAt = LocalData.getSessionExpirationDate();
|
||||
|
||||
if (expiresAt) {
|
||||
resetActivityEvents.forEach((eventName: string) => {
|
||||
document.addEventListener(eventName, onSessionActivity, false);
|
||||
});
|
||||
|
||||
if (expiresAt.getTime() - sessionDuration.value + sessionRefreshInterval.value < Date.now()) {
|
||||
refreshSession();
|
||||
}
|
||||
|
||||
restartSessionTimers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts timers associated with session refreshing and inactivity.
|
||||
*/
|
||||
function restartSessionTimers(): void {
|
||||
sessionRefreshTimerId.value = setTimeout(async () => {
|
||||
sessionRefreshTimerId.value = null;
|
||||
if (isSessionActive.value) {
|
||||
await refreshSession();
|
||||
}
|
||||
}, sessionRefreshInterval.value);
|
||||
|
||||
inactivityTimerId.value = setTimeout(async () => {
|
||||
if (obStore.uploadingLength) {
|
||||
await refreshSession();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSessionActive.value) return;
|
||||
inactivityModalShown.value = true;
|
||||
inactivityTimerId.value = setTimeout(async () => {
|
||||
await clearStoreAndTimers();
|
||||
notify.notify('Your session was timed out.');
|
||||
}, inactivityModalTime);
|
||||
}, sessionDuration.value - inactivityModalTime);
|
||||
|
||||
if (!debugTimerShown.value) return;
|
||||
|
||||
const debugTimer = () => {
|
||||
const expiresAt = LocalData.getSessionExpirationDate();
|
||||
|
||||
if (expiresAt) {
|
||||
const ms = Math.max(0, expiresAt.getTime() - Date.now());
|
||||
const secs = Math.floor(ms / 1000) % 60;
|
||||
|
||||
debugTimerText.value = `${Math.floor(ms / 60000)}:${(secs < 10 ? '0' : '') + secs}`;
|
||||
|
||||
if (ms > 1000) {
|
||||
debugTimerId.value = setTimeout(debugTimer, 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
debugTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes session and resets session timers.
|
||||
* @param manual - whether the user manually refreshed session. i.e.: clicked "Stay Logged In".
|
||||
*/
|
||||
async function refreshSession(manual = false): Promise<void> {
|
||||
isSessionRefreshing.value = true;
|
||||
|
||||
try {
|
||||
LocalData.setSessionExpirationDate(await auth.refreshSession());
|
||||
} catch (error) {
|
||||
error.message = (error instanceof ErrorUnauthorized) ? 'Your session was timed out.' : error.message;
|
||||
notify.notifyError(error, AnalyticsErrorEventSource.ALL_PROJECT_DASHBOARD);
|
||||
await handleInactive();
|
||||
isSessionRefreshing.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
clearSessionTimers();
|
||||
restartSessionTimers();
|
||||
inactivityModalShown.value = false;
|
||||
isSessionActive.value = false;
|
||||
isSessionRefreshing.value = false;
|
||||
|
||||
if (manual && !usersStore.state.settings.sessionDuration) {
|
||||
appStore.updateActiveModal(MODALS.editSessionTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets inactivity timer and refreshes session if necessary.
|
||||
*/
|
||||
async function onSessionActivity(): Promise<void> {
|
||||
if (inactivityModalShown.value || isSessionActive.value) return;
|
||||
|
||||
if (sessionRefreshTimerId.value === null && !isSessionRefreshing.value) {
|
||||
await refreshSession();
|
||||
}
|
||||
|
||||
isSessionActive.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle hook after initial render.
|
||||
* Pre-fetches user`s and project information.
|
||||
*/
|
||||
onMounted(async () => {
|
||||
usersStore.$onAction(({ name, after, args }) => {
|
||||
if (name === 'clear') clearSessionTimers();
|
||||
else if (name === 'updateSettings') {
|
||||
if (args[0].sessionDuration && args[0].sessionDuration !== usersStore.state.settings.sessionDuration?.nanoseconds) {
|
||||
after((_) => refreshSession());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
usersStore.getUser(),
|
||||
abTestingStore.fetchValues(),
|
||||
usersStore.getSettings(),
|
||||
]);
|
||||
|
||||
setupSessionTimers();
|
||||
} catch (error) {
|
||||
if (!(error instanceof ErrorUnauthorized)) {
|
||||
appStore.changeState(FetchState.ERROR);
|
||||
@ -552,13 +312,6 @@ onMounted(async () => {
|
||||
await router.push(RouteConfig.OnboardingTour.with(RouteConfig.PricingPlanStep).path);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearSessionTimers();
|
||||
resetActivityEvents.forEach((eventName: string) => {
|
||||
document.removeEventListener(eventName, onSessionActivity);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
Loading…
Reference in New Issue
Block a user