storj/web/satellite/src/views/DashboardArea.vue
NickolaiYurchenko 70c8ccb435 web/satellite: inactivity timer to dashboard added
added InactivityTimerEnabled flag to enable/disable feature
added InactivityTimerDelay to configure delay time in seconds
default timer set up to 10 minutes
reset dom events: keypress, mouseover, mousedown, touchmove

Change-Id: Idb66067c2902b2cdbe1a972225319c8abff97927
2022-02-16 07:16:00 +00:00

485 lines
15 KiB
Vue

// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="dashboard">
<div v-if="isLoading" class="loading-overlay active">
<LoaderImage class="loading-icon" />
</div>
<div v-if="!isLoading" class="dashboard__wrap">
<template v-if="!isNewNavStructure">
<BetaSatBar v-if="isBetaSatellite" />
<PaidTierBar v-if="!creditCards.length && !isOnboardingTour" :open-add-p-m-modal="togglePMModal" />
<ProjectInfoBar v-if="isProjectListPage" />
<MFARecoveryCodeBar v-if="showMFARecoveryCodeBar" :open-generate-modal="generateNewMFARecoveryCodes" />
</template>
<template v-if="isNewNavStructure">
<div class="dashboard__wrap__new-main-area">
<NewNavigationArea v-if="!isNavigationHidden" />
<div
class="dashboard__wrap__new-main-area__content-wrap"
:class="{
'with-one-bar': amountOfInfoBars === 1,
'with-two-bars': amountOfInfoBars === 2,
'with-three-bars': amountOfInfoBars === 3,
'with-four-bars': amountOfInfoBars === 4,
}"
>
<BetaSatBar v-if="isBetaSatellite" />
<PaidTierBar v-if="!creditCards.length && !isOnboardingTour" :open-add-p-m-modal="togglePMModal" />
<ProjectInfoBar v-if="isProjectListPage" />
<MFARecoveryCodeBar v-if="showMFARecoveryCodeBar" :open-generate-modal="generateNewMFARecoveryCodes" />
<router-view class="dashboard__wrap__new-main-area__content-wrap__content" />
</div>
</div>
</template>
<template v-else>
<DashboardHeader />
<div
class="dashboard__wrap__main-area"
:class="{
'with-one-bar-old': amountOfInfoBars === 1,
'with-two-bars-old': amountOfInfoBars === 2,
'with-three-bars-old': amountOfInfoBars === 3,
'with-four-bars-old': amountOfInfoBars === 4,
}"
>
<NavigationArea class="regular-navigation" />
<router-view class="dashboard__wrap__main-area__content" />
</div>
</template>
</div>
<AddPaymentMethodModal v-if="isAddPMModal" :on-close="togglePMModal" />
<MFARecoveryCodesPopup v-if="isMFACodesPopup" :toggle-modal="toggleMFACodesPopup" />
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import AddPaymentMethodModal from '@/components/account/billing/paidTier/AddPaymentMethodModal.vue';
import PaidTierBar from '@/components/infoBars/PaidTierBar.vue';
import MFARecoveryCodeBar from '@/components/infoBars/MFARecoveryCodeBar.vue';
import BetaSatBar from '@/components/infoBars/BetaSatBar.vue';
import MFARecoveryCodesPopup from '@/components/account/mfa/MFARecoveryCodesPopup.vue';
import DashboardHeader from '@/components/header/HeaderArea.vue';
import NavigationArea from '@/components/navigation/NavigationArea.vue';
import NewNavigationArea from '@/components/navigation/newNavigationStructure/NewNavigationArea.vue';
import ProjectInfoBar from "@/components/infoBars/ProjectInfoBar.vue";
import LoaderImage from '@/../static/images/common/loader.svg';
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
import { RouteConfig } from '@/router';
import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
import { PAYMENTS_ACTIONS, PAYMENTS_MUTATIONS } from '@/store/modules/payments';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { USER_ACTIONS } from '@/store/modules/users';
import { CouponType } from '@/types/coupons';
import { CreditCard } from '@/types/payments';
import { Project } from '@/types/projects';
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
import { AppState } from '@/utils/constants/appStateEnum';
import { LocalData } from '@/utils/localData';
import { User } from "@/types/users";
import { AuthHttpApi } from "@/api/auth";
import { MetaUtils } from "@/utils/meta";
const {
SETUP_ACCOUNT,
GET_CREDIT_CARDS,
} = PAYMENTS_ACTIONS;
// @vue/component
@Component({
components: {
NavigationArea,
NewNavigationArea,
DashboardHeader,
LoaderImage,
PaidTierBar,
MFARecoveryCodeBar,
BetaSatBar,
ProjectInfoBar,
MFARecoveryCodesPopup,
AddPaymentMethodModal,
},
})
export default class DashboardArea extends Vue {
// List of DOM events that resets inactivity timer.
private readonly resetActivityEvents: string[] = ['keypress', 'mousemove', 'mousedown', 'touchmove'];
private readonly auth: AuthHttpApi = new AuthHttpApi();
private inactivityTimerId: ReturnType<typeof setTimeout>;
// Minimum number of recovery codes before the recovery code warning bar is shown.
public recoveryCodeWarningThreshold = 4;
public isMFACodesPopup = false;
/**
* Lifecycle hook after initial render.
* Pre fetches user`s and project information.
*/
public async mounted(): Promise<void> {
this.setupInactivityTimers();
try {
await this.$store.dispatch(USER_ACTIONS.GET);
} catch (error) {
if (!(error instanceof ErrorUnauthorized)) {
await this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.ERROR);
await this.$notify.error(error.message);
}
setTimeout(async () => await this.$router.push(RouteConfig.Login.path), 1000);
return;
}
try {
await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.STOP_ACCESS_GRANTS_WEB_WORKER);
await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.SET_ACCESS_GRANTS_WEB_WORKER);
} catch (error) {
await this.$notify.error(`Unable to set access grants wizard. ${error.message}`);
}
try {
const couponType = await this.$store.dispatch(SETUP_ACCOUNT);
if (couponType === CouponType.NoCoupon) {
await this.$notify.error(`The coupon code was invalid, and could not be applied to your account`);
}
if (couponType === CouponType.SignupCoupon) {
await this.$notify.success(`The coupon code was added successfully`);
}
} catch (error) {
await this.$notify.error(`Unable to setup account. ${error.message}`);
}
try {
await this.$store.dispatch(GET_CREDIT_CARDS);
} catch (error) {
await this.$notify.error(`Unable to get credit cards. ${error.message}`);
}
let projects: Project[] = [];
try {
projects = await this.$store.dispatch(PROJECTS_ACTIONS.FETCH);
} catch (error) {
return;
}
if (!projects.length) {
try {
await this.$store.dispatch(PROJECTS_ACTIONS.CREATE_DEFAULT_PROJECT);
await this.$router.push(RouteConfig.OnboardingTour.with(RouteConfig.OverviewStep).path);
await this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.LOADED);
} catch (error) {
return;
}
return;
}
this.selectProject(projects);
await this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.LOADED);
}
/**
* Generates new MFA recovery codes and toggles popup visibility.
*/
public async generateNewMFARecoveryCodes(): Promise<void> {
try {
await this.$store.dispatch(USER_ACTIONS.GENERATE_USER_MFA_RECOVERY_CODES);
this.toggleMFACodesPopup();
} catch (error) {
await this.$notify.error(error.message);
}
}
/**
* Opens add payment method modal.
*/
public togglePMModal(): void {
this.$store.commit(PAYMENTS_MUTATIONS.TOGGLE_IS_ADD_PM_MODAL_SHOWN);
}
/**
* Toggles MFA recovery codes popup visibility.
*/
public toggleMFACodesPopup(): void {
this.isMFACodesPopup = !this.isMFACodesPopup;
}
/**
* Checks if stored project is in fetched projects array and selects it.
* Selects first fetched project if check is not successful.
* @param fetchedProjects - fetched projects array
*/
private selectProject(fetchedProjects: Project[]): void {
const storedProjectID = LocalData.getSelectedProjectId();
const isProjectInFetchedProjects = fetchedProjects.some(project => project.id === storedProjectID);
if (storedProjectID && isProjectInFetchedProjects) {
this.storeProject(storedProjectID);
return;
}
// Length of fetchedProjects array is checked before selectProject() function call.
this.storeProject(fetchedProjects[0].id);
}
/**
* Stores project to vuex store and browser's local storage.
* @param projectID - project id string
*/
private storeProject(projectID: string): void {
this.$store.dispatch(PROJECTS_ACTIONS.SELECT, projectID);
LocalData.setSelectedProjectId(projectID);
}
/**
* Returns amount of rendered info bars.
* It is used to set height of content's container.
*/
public get amountOfInfoBars(): number {
const conditions: boolean[] = [
this.isBetaSatellite,
!this.creditCards.length && !this.isOnboardingTour,
this.isProjectListPage,
this.showMFARecoveryCodeBar,
]
return conditions.filter(c => c).length;
}
/**
* Indicates if current route is projects list page.
*/
public get isProjectListPage(): boolean {
return this.$route.name === RouteConfig.ProjectsList.name;
}
/**
* Indicates if add payment method modal is shown.
*/
public get isAddPMModal(): boolean {
return this.$store.state.paymentsModule.isAddPMModalShown;
}
/**
* Returns credit cards from store.
*/
public get creditCards(): CreditCard[] {
return this.$store.state.paymentsModule.creditCards;
}
/**
* Indicates if current route is onboarding tour.
*/
public get isOnboardingTour(): boolean {
return this.$route.path.includes(RouteConfig.OnboardingTour.path);
}
/**
* Indicates if satellite is in beta.
*/
public get isBetaSatellite(): boolean {
return this.$store.state.appStateModule.isBetaSatellite;
}
/**
* Indicates if loading screen is active.
*/
public get isLoading(): boolean {
return this.$store.state.appStateModule.appState.fetchState === AppState.LOADING;
}
/**
* Indicates whether the MFA recovery code warning bar should be shown.
*/
public get showMFARecoveryCodeBar(): boolean {
const user : User = this.$store.getters.user;
return user.isMFAEnabled && user.mfaRecoveryCodeCount < this.recoveryCodeWarningThreshold;
}
/**
* Indicates if new navigation structure is used.
*/
public get isNewNavStructure(): boolean {
return this.$store.state.appStateModule.isNewNavStructure;
}
/**
* Indicates if navigation side bar is hidden.
*/
public get isNavigationHidden(): boolean {
return this.isOnboardingTour || this.isCreateProjectPage;
}
/**
* Indicates if current route is create project page.
*/
private get isCreateProjectPage(): boolean {
return this.$route.name === RouteConfig.CreateProject.name;
}
/**
* Sets up timer id with given delay.
*/
private startInactivityTimer(): void {
const inactivityTimerDelayInSeconds = MetaUtils.getMetaContent('inactivity-timer-delay');
this.inactivityTimerId = setTimeout(this.handleInactive, parseInt(inactivityTimerDelayInSeconds) * 1000);
}
/**
* Performs logout and cleans event listeners.
*/
private async handleInactive(): Promise<void> {
try {
await this.auth.logout();
this.resetActivityEvents.forEach((eventName: string) => {
document.removeEventListener(eventName, this.resetInactivityTimer);
});
await this.$router.push(RouteConfig.Login.path);
await this.$notify.notify('Your session was timed out.');
} catch (error) {
await this.$notify.error(error.message);
}
}
/**
* Resets inactivity timer.
*/
private resetInactivityTimer(): void {
clearTimeout(this.inactivityTimerId);
this.startInactivityTimer();
}
/**
* Adds DOM event listeners and starts timer.
*/
private setupInactivityTimers(): void {
const isInactivityTimerEnabled = MetaUtils.getMetaContent('inactivity-timer-enabled');
if (isInactivityTimerEnabled === 'false') return;
this.resetActivityEvents.forEach((eventName: string) => {
document.addEventListener(eventName, this.resetInactivityTimer, false);
});
this.startInactivityTimer();
}
}
</script>
<style scoped lang="scss">
.loading-overlay {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(134, 134, 148, 0.3);
visibility: hidden;
opacity: 0;
-webkit-transition: all 0.5s linear;
-moz-transition: all 0.5s linear;
-o-transition: all 0.5s linear;
transition: all 0.5s linear;
}
.loading-overlay.active {
visibility: visible;
opacity: 1;
}
.loading-icon {
width: 100px;
height: 100px;
}
.dashboard {
height: 100%;
background-color: #f5f6fa;
display: flex;
flex-direction: column;
&__wrap {
display: flex;
flex-direction: column;
height: 100%;
&__main-area {
display: flex;
height: calc(100% - 62px);
&__content {
overflow-y: auto;
width: 100%;
position: relative;
}
}
&__new-main-area {
display: flex;
width: 100%;
height: 100%;
&__content-wrap {
width: 100%;
&__content {
overflow-y: auto;
}
}
}
}
}
.with-one-bar {
height: calc(100% - 26px);
}
.with-two-bars {
height: calc(100% - 52px);
}
.with-three-bars {
height: calc(100% - 78px);
}
.with-four-bars {
height: calc(100% - 104px);
}
.with-one-bar-old {
height: calc(100% - 62px - 26px);
}
.with-two-bars-old {
height: calc(100% - 62px - 52px);
}
.with-three-bars-old {
height: calc(100% - 62px - 78px);
}
.with-four-bars-old {
height: calc(100% - 62px - 104px);
}
@media screen and (max-width: 1280px) {
.regular-navigation {
display: none;
}
}
</style>