Skip to content

Commit

Permalink
web/satellite/v2: fix unauthenticated navigation
Browse files Browse the repository at this point in the history
This change fixes an issue where navigating directly to a page when
unauthenticated will show the page for a while before navigating to the
login page, by introducing a full screen loader as seen in the V1 app.
It also moves certain page setup calls (user, projects etc.) to App.vue
so as not to replicate the login-redirect logic in more places. This
also helps avoid calling for setup data that have already been fetched.
Finally, navigation directly to the authentication pages is prevented.

Issue: #6699

Change-Id: I3b5f8570c7d430f2afc5b0366f0e32852e4d1ae0
  • Loading branch information
wilfred-asomanii authored and andriikotko committed Jan 18, 2024
1 parent 8dd73e1 commit dd52ef2
Show file tree
Hide file tree
Showing 13 changed files with 208 additions and 106 deletions.
15 changes: 12 additions & 3 deletions web/satellite/index-vuetify.html
Expand Up @@ -19,19 +19,28 @@
border: 4px solid rgb(1,73,255);
border-top: 4px solid transparent;
border-radius: 50%;
width: 50px;
height: 50px;
width: 90px;
height: 90px;
animation: spin .7s linear infinite;
}

#pre-app-loader img {
position: absolute;
inset: 0;
margin: auto;
}

@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div id="pre-app-loader"><div></div></div>
<div id="pre-app-loader">
<div></div>
<img alt="loader" src="@/../static/images/common/loadIcon.svg">
</div>
<div id="app"></div>
<script type="module" src="/vuetify-poc/src/main.ts"></script>
</body>
Expand Down
2 changes: 2 additions & 0 deletions web/satellite/src/types/router.ts
Expand Up @@ -69,4 +69,6 @@ export abstract class RouteConfig {
public static BucketsDetails = new NavigationLink('details', 'Bucket Details');
public static UploadFile = new NavigationLink('upload/', 'Objects Upload');
public static UploadFileChildren = new NavigationLink(':pathMatch*', 'Objects Upload Children');

public static AuthRoutes = ['/login', '/signup', '/forgot-password', '/activate', '/password-recovery', '/signup-confirmation', '/password-reset-confirmation'];
}
14 changes: 12 additions & 2 deletions web/satellite/src/utils/httpClient.ts
Expand Up @@ -3,6 +3,7 @@

import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
import { useConfigStore } from '@/store/modules/configStore';
import { RouteConfig } from '@/types/router';

/**
* HttpClient is a custom wrapper around fetch api.
Expand Down Expand Up @@ -83,8 +84,8 @@ export class HttpClient {
* Call logout and redirect to login.
*/
private async handleUnauthorized(): Promise<void> {
const path = window.location.href;
if (!path.includes('/login') && !path.includes('/signup')) {
let path = window.location.href;
if (!this.isAuthRoute(path)) {
try {
const logoutPath = '/api/v0/auth/logout';
const request: RequestInit = {
Expand All @@ -102,6 +103,11 @@ export class HttpClient {
}

setTimeout(() => {
// path may have changed after timeout.
path = window.location.href;
if (this.isAuthRoute(path)) {
return;
}
const origin = window.location.origin;
if (document.querySelector('.v-overlay-container')) {
window.location.href = origin + useConfigStore().optionalV2Path + '/login';
Expand All @@ -111,4 +117,8 @@ export class HttpClient {
}, 2000);
}
}

private isAuthRoute(path: string): boolean {
return RouteConfig.AuthRoutes.some((route) => path.includes(route));
}
}
99 changes: 95 additions & 4 deletions web/satellite/vuetify-poc/src/App.vue
Expand Up @@ -2,23 +2,48 @@
// See LICENSE for copying information.

<template>
<ErrorPage v-if="isErrorPageShown" />
<branded-loader v-if="isLoading" />
<ErrorPage v-else-if="isErrorPageShown" />
<router-view v-else />
<Notifications />
</template>

<script setup lang="ts">
import { computed, onMounted } from 'vue';
import { computed, onBeforeMount, ref } from 'vue';
import { useTheme } from 'vuetify';
import { useRoute, useRouter } from 'vue-router';
import { useConfigStore } from '@/store/modules/configStore';
import { useAppStore } from '@poc/store/appStore';
import { APIError } from '@/utils/error';
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
import { useABTestingStore } from '@/store/modules/abTestingStore';
import { useUsersStore } from '@/store/modules/usersStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useNotify } from '@/utils/hooks';
import { useBillingStore } from '@/store/modules/billingStore';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { RouteConfig } from '@/types/router';
import Notifications from '@poc/layouts/default/Notifications.vue';
import ErrorPage from '@poc/components/ErrorPage.vue';
import BrandedLoader from '@poc/components/utils/BrandedLoader.vue';
const appStore = useAppStore();
const abTestingStore = useABTestingStore();
const billingStore = useBillingStore();
const configStore = useConfigStore();
const usersStore = useUsersStore();
const projectsStore = useProjectsStore();
const analyticsStore = useAnalyticsStore();
const notify = useNotify();
const router = useRouter();
const theme = useTheme();
const route = useRoute();
const isLoading = ref<boolean>(true);
/**
* Indicates whether an error page should be shown in place of the router view.
Expand All @@ -28,14 +53,80 @@ const isErrorPageShown = computed<boolean>((): boolean => {
});
/**
* Lifecycle hook after initial render.
* Indicates if billing features are enabled.
*/
const billingEnabled = computed<boolean>(() => configStore.state.config.billingFeaturesEnabled);
/**
* Sets up the app by fetching all necessary data.
*/
async function setup() {
isLoading.value = true;
try {
await usersStore.getUser();
const promises: Promise<void | object | string>[] = [
usersStore.getSettings(),
projectsStore.getProjects(),
projectsStore.getUserInvitations(),
abTestingStore.fetchValues(),
];
if (billingEnabled.value) {
promises.push(billingStore.setupAccount());
}
await Promise.all(promises);
const invites = projectsStore.state.invitations;
const projects = projectsStore.state.projects;
if (appStore.state.hasJustLoggedIn && !invites.length && projects.length <= 1) {
if (!projects.length) {
await projectsStore.createDefaultProject(usersStore.state.user.id);
} else {
projectsStore.selectProject(projects[0].id);
await router.push(`/projects/${projectsStore.state.selectedProject.urlId}/dashboard`);
analyticsStore.pageVisit('/projects/dashboard');
analyticsStore.eventTriggered(AnalyticsEvent.NAVIGATE_PROJECTS);
}
}
} catch (error) {
if (!(error instanceof ErrorUnauthorized)) {
notify.notifyError(error, AnalyticsErrorEventSource.OVERALL_APP_WRAPPER_ERROR);
appStore.setErrorPage((error as APIError).status ?? 500, true);
} else {
await new Promise(resolve => setTimeout(resolve, 1000));
if (!RouteConfig.AuthRoutes.includes(route.path)) await router.push('/login');
}
}
isLoading.value = false;
}
/**
* Lifecycle hook before initial render.
* Sets up variables from meta tags from config such satellite name, etc.
*/
onMounted(async (): Promise<void> => {
onBeforeMount(async (): Promise<void> => {
const savedTheme = localStorage.getItem('theme') || 'light';
if ((savedTheme === 'dark' && !theme.global.current.value.dark) || (savedTheme === 'light' && theme.global.current.value.dark)) {
theme.global.name.value = savedTheme;
}
try {
await configStore.getConfig();
} catch (error) {
isLoading.value = false;
notify.notifyError(error, AnalyticsErrorEventSource.OVERALL_APP_WRAPPER_ERROR);
appStore.setErrorPage((error as APIError).status ?? 500, true);
return;
}
await setup();
isLoading.value = false;
});
usersStore.$onAction(({ name, after }) => {
if (name === 'login') {
after((_) => setup());
}
});
</script>
49 changes: 49 additions & 0 deletions web/satellite/vuetify-poc/src/components/utils/BrandedLoader.vue
@@ -0,0 +1,49 @@
// Copyright (C) 2024 Storj Labs, Inc.
// See LICENSE for copying information.

<template>
<div class="loader d-flex align-center justify-center">
<div class="loader__spinner" />
<img alt="loader" src="@/../static/images/common/loadIcon.svg" class="loader__icon">
</div>
</template>

<style scoped lang="scss">
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.loader {
position: absolute;
inset: 0;
&__spinner {
width: 90px;
height: 90px;
margin: auto 0;
border: solid 3px var(--c-blue-3);
border-radius: 50%;
border-right-color: transparent;
border-bottom-color: transparent;
border-left-color: transparent;
transition: all 0.5s ease-in;
animation-name: rotate;
animation-duration: 1.2s;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
&__icon {
position: absolute;
inset: 0;
margin: auto;
}
}
</style>
42 changes: 3 additions & 39 deletions web/satellite/vuetify-poc/src/layouts/default/AllProjects.vue
Expand Up @@ -3,10 +3,7 @@

<template>
<v-app>
<div v-if="isLoading" class="d-flex align-center justify-center w-100 h-100">
<v-progress-circular color="primary" indeterminate size="64" />
</div>
<session-wrapper v-else>
<session-wrapper>
<default-bar />
<default-view />

Expand All @@ -17,16 +14,15 @@
</template>

<script setup lang="ts">
import { VApp, VProgressCircular } from 'vuetify/components';
import { onBeforeMount, onBeforeUnmount, ref } from 'vue';
import { VApp } from 'vuetify/components';
import { onBeforeUnmount } from 'vue';
import { useRouter } from 'vue-router';
import DefaultBar from './AppBar.vue';
import DefaultView from './View.vue';
import { useAppStore } from '@poc/store/appStore';
import { useUsersStore } from '@/store/modules/usersStore';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
Expand All @@ -43,38 +39,6 @@ const analyticsStore = useAnalyticsStore();
const notify = useNotify();
const router = useRouter();
const isLoading = ref<boolean>(true);
/**
* Lifecycle hook after initial render.
* Pre-fetches user`s and project information.
*/
onBeforeMount(async () => {
try {
await usersStore.getSettings();
await usersStore.getUser();
await projectsStore.getProjects();
const invites = await projectsStore.getUserInvitations();
const projects = projectsStore.state.projects;
if (appStore.state.hasJustLoggedIn && !invites.length && projects.length <= 1) {
if (!projects.length) {
await projectsStore.createDefaultProject(usersStore.state.user.id);
} else {
projectsStore.selectProject(projects[0].id);
await router.push(`/projects/${projectsStore.state.selectedProject.urlId}/dashboard`);
analyticsStore.pageVisit('/projects/dashboard');
analyticsStore.eventTriggered(AnalyticsEvent.NAVIGATE_PROJECTS);
}
}
} catch (error) {
notify.notifyError(error, AnalyticsErrorEventSource.ALL_PROJECT_DASHBOARD);
}
isLoading.value = false;
});
onBeforeUnmount(() => {
appStore.toggleHasJustLoggedIn(false);
});
Expand Down
6 changes: 1 addition & 5 deletions web/satellite/vuetify-poc/src/layouts/default/AppBar.vue
Expand Up @@ -264,11 +264,7 @@ function toggleTheme(newTheme: string): void {
watch(() => theme.global.current.value.dark, (newVal: boolean) => {
activeTheme.value = newVal ? 1 : 0;
});
// Check for stored theme in localStorage. If none, default to 'light'
toggleTheme(localStorage.getItem('theme') || 'light');
activeTheme.value = theme.global.current.value.dark ? 1 : 0;
}, { immediate: true });
function closeSideNav(): void {
if (mdAndDown.value) appStore.toggleNavigationDrawer(false);
Expand Down
17 changes: 17 additions & 0 deletions web/satellite/vuetify-poc/src/layouts/default/Auth.vue
Expand Up @@ -10,7 +10,24 @@

<script setup lang="ts">
import { VApp } from 'vuetify/components';
import { onBeforeMount } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import AuthBar from './AuthBar.vue';
import DefaultView from './View.vue';
import { useUsersStore } from '@/store/modules/usersStore';
const usersStore = useUsersStore();
const route = useRoute();
const router = useRouter();
onBeforeMount(() => {
if (usersStore.state.user.id) {
// user is already logged in
router.replace('/projects');
return;
}
});
</script>

0 comments on commit dd52ef2

Please sign in to comment.