Skip to content

Commit

Permalink
Merge pull request #514 from fdm-monster/feat/498-detect-and-handle-r…
Browse files Browse the repository at this point in the history
…efresh-expiry-at-app-runtime-by-intercept-if-failing-log-out

Feat/498 detect and handle refresh expiry at app runtime by intercept if failing log out
  • Loading branch information
davidzwa authored Aug 26, 2023
2 parents 1bf070c + 017a728 commit 072343b
Show file tree
Hide file tree
Showing 24 changed files with 487 additions and 224 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
"validator": "13.11.0",
"vee-validate": "3.4.15",
"vue": "2.7.14",
"vue-axios": "3.5.2",
"vue-router": "3.6.5",
"vuedraggable": "2.24.3",
"vuetify": "2.7.1"
Expand Down
13 changes: 0 additions & 13 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@
<AppProgressSnackbar />
<AlertErrorDialog />

<NavigationDrawer v-if="authStore.isLoggedIn || !authStore.loginRequired" />
<TopBar v-if="authStore.isLoggedIn || !authStore.loginRequired" />
<NavigationDrawer
v-if="(authStore.hasAuthToken && !authStore.isLoginExpired) || !authStore.loginRequired"
/>
<TopBar
v-if="(authStore.hasAuthToken && !authStore.isLoginExpired) || !authStore.loginRequired"
/>

<AppLoader>
<v-main>
Expand Down Expand Up @@ -79,7 +83,7 @@ export default defineComponent({
},
async mounted() {
console.debug(
`App.vue mounted. Logged in: ${this.authStore.isLoggedIn}, Expired: ${this.authStore.isLoginExpired}`
`App.vue mounted. Logged in: ${this.authStore.hasAuthToken}, Expired: ${this.authStore.isLoginExpired}`
);
uploadProgressTest(false);
Expand Down
123 changes: 90 additions & 33 deletions src/AppLoader.vue
Original file line number Diff line number Diff line change
@@ -1,47 +1,62 @@
<template>
<span>
<v-overlay
v-model="overlay"
class="align-center justify-center"
opacity="0.98"
style="z-index: 7"
>
<GridLoader :size="20" color="#a70015" />
<v-overlay v-model="overlay" opacity="0.98" style="z-index: 7">
<GridLoader :size="20" class="ma-auto" color="#a70015" />
<br />

<!-- Fade-in -->
<!-- Slow scroll fade-out vtexts -->
<div style="animation: fadeIn 0.75s">{{ overlayMessage }}</div>
</v-overlay>
<slot v-if="!overlay" />
</span>
</template>

<script lang="ts" setup>
import { onBeforeMount, onUnmounted, ref } from "vue";
import GridLoader from "./components/Generic/Loaders/GridLoader.vue";
import { useAuthStore } from "./store/auth.store";
import { useRouter } from "vue-router/composables";
import { useSnackbar } from "@/shared/snackbar.composable";
import { useSettingsStore } from "@/store/settings.store";
import { setSentryEnabled } from "@/utils/sentry.util";
import { useFeatureStore } from "@/store/features.store";
import { useProfileStore } from "@/store/profile.store";
import { useEventBus } from "@vueuse/core";
import { SocketIoService } from "@/shared/socketio.service";
import { useRouter } from "vue-router/composables";
import { sleep } from "@/utils/time.utils";
import { RouteNames } from "@/router/route-names";
const authStore = useAuthStore();
const settingsStore = useSettingsStore();
const featureStore = useFeatureStore();
const profileStore = useProfileStore();
const overlay = ref(true);
const snackbar = useSnackbar();
const overlay = ref(false);
const router = useRouter();
const overlayMessage = ref("");
const snackbar = useSnackbar();
const socketIoClient: SocketIoService = new SocketIoService();
function setOverlay(overlayEnabled: boolean) {
function setOverlay(overlayEnabled: boolean, message: string = "") {
if (!overlayEnabled) {
overlayMessage.value = "";
} else {
overlayMessage.value = message;
}
overlay.value = overlayEnabled;
}
async function routeToLoginSafely() {
setOverlay(true, "Login expired, going back to login");
await sleep(500);
if (router.currentRoute.name !== RouteNames.Login) {
await router.push({ name: RouteNames.Login });
}
setOverlay(false);
}
async function loadAppWithAuthentication() {
try {
authStore.loadTokens();
await authStore.verifyLogin();
await settingsStore.loadSettings();
await featureStore.loadFeatures();
await profileStore.getProfile();
Expand All @@ -52,8 +67,7 @@ async function loadAppWithAuthentication() {
console.log("Error when loading settings.", e);
snackbar.openErrorMessage({
title: "Error",
fullSubtitle: "Error when verifying login and loading settings.",
subtitle: "Error when verifying login.",
subtitle: "Error when verifying login and loading settings.",
});
}
Expand All @@ -62,22 +76,49 @@ async function loadAppWithAuthentication() {
setOverlay(false);
}
const key = useEventBus("auth:login");
key.on(() => {
setOverlay(true);
loadAppWithAuthentication();
// In use (shared/http-client.ts)
const authPermissionDeniedKey = useEventBus("auth:permission-denied");
authPermissionDeniedKey.on(async (event) => {
console.log("[AppLoader] Permission denied, going to permission denied page");
setOverlay(true, "Permission denied");
await router.push({
name: RouteNames.PermissionDenied,
query: {
roles: event?.roles,
page: router.currentRoute.name,
permissions: event?.permissions,
error: event?.error,
url: event?.url,
},
});
setOverlay(false);
});
// Currently unused
const authFailKey = useEventBus("auth:failure");
authFailKey.on(async () => {
console.debug("[AppLoader] Event received: 'auth:failure', going back to login");
setOverlay(true, "Authentication failed, going back to login");
await router.push({ name: RouteNames.Login });
setOverlay(false);
});
// In use (components/Login/LoginForm.vue)
const loginEventKey = useEventBus("auth:login");
loginEventKey.on(async () => {
console.debug("[AppLoader] Event received: 'auth:login', loading app");
setOverlay(true, "Loading app");
await loadAppWithAuthentication();
});
onUnmounted(() => {
key.reset();
if (socketIoClient) {
socketIoClient.disconnect();
}
});
onBeforeMount(async () => {
// test slow page loading
// await sleep(2000);
setOverlay(true, "Loading spools");
// If the route is wrong about login requirements, an error will be shown
const loginRequired = await authStore.checkLoginRequired();
Expand All @@ -86,23 +127,39 @@ onBeforeMount(async () => {
}
// Router will have tackled routing already
if (!authStore.isLoggedIn) {
console.debug("[AppLoader] Checking if tokens are present");
if (!authStore.hasAuthToken && !authStore.hasRefreshToken) {
console.debug("[AppLoader] No tokens present, hiding overlay as router will have handled it");
return setOverlay(false);
}
if (authStore.isLoggedIn && authStore.isLoginExpired) {
try {
await authStore.refreshTokens();
} catch (e) {
console.error("Error when refreshing login.");
snackbar.openErrorMessage({
title: "Login error",
fullSubtitle: "Error when refreshing login.",
});
await router.push({ name: "Login" });
// What if refreshToken is not present or not valid?
setOverlay(true, "Refreshing login");
console.debug("[AppLoader] Verifying or refreshing login once");
const refreshSuccess = await authStore.verifyOrRefreshLoginOnce();
if (!refreshSuccess) {
console.debug("[AppLoader] No success refreshing");
setOverlay(true, "Login expired, going back to login");
await sleep(500);
if (router.currentRoute.name !== RouteNames.Login) {
await router.push({ name: RouteNames.Login });
}
setOverlay(false);
// Dont load app as it will be redirected to login
return;
}
console.debug("[AppLoader] Loading app");
await loadAppWithAuthentication();
});
</script>
<style>
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
</style>
8 changes: 4 additions & 4 deletions src/backend/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,25 @@ export interface Tokens {

export class AuthService {
static async getLoginRequired() {
const httpClient = getHttpClient(false);
const httpClient = await getHttpClient(false, false);
return await httpClient.get<{ loginRequired: boolean }>("api/auth/login-required");
}

static async postLogin(username: string, password: string) {
const httpClient = getHttpClient(false);
const httpClient = await getHttpClient(false, false);
return await httpClient.post<Tokens>("api/auth/login", {
username,
password,
});
}

static async refreshLogin(refreshToken: string) {
const httpClient = getHttpClient(false);
const httpClient = await getHttpClient(false, false);
return await httpClient.post<{ token: string }>("api/auth/refresh", { refreshToken });
}

static async verifyLogin() {
const httpClient = getHttpClient(true);
const httpClient = await getHttpClient(true, false);
return await httpClient.post("api/auth/verify");
}
}
12 changes: 6 additions & 6 deletions src/backend/base.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ import { getHttpClient } from "@/shared/http-client";

export class BaseService {
protected static async getApi<R>(path: string) {
const httpClient = getHttpClient(true);
const httpClient = await getHttpClient(true);
const response = await httpClient.get<R>(path);
return response.data;
}

protected static async putApi<T>(path: string, body?: any) {
const httpClient = getHttpClient(true);
const httpClient = await getHttpClient(true);
const response = await httpClient.put<T>(path, body);
return response.data;
}

protected static async postApi<T>(path: string, body?: any) {
const httpClient = getHttpClient(true);
const httpClient = await getHttpClient(true);
const response = await httpClient.post<T>(path, body);
return response.data;
}
Expand All @@ -25,12 +25,12 @@ export class BaseService {
formData: FormData,
config: AxiosRequestConfig
) {
const httpClient = getHttpClient(true);
const httpClient = await getHttpClient(true);
return await httpClient.post(path, formData, config);
}

protected static async deleteApi<T>(path: string, body?: any) {
const httpClient = getHttpClient(true);
const httpClient = await getHttpClient(true);
const response = await httpClient.request<T>({
url: path,
method: "delete",
Expand All @@ -40,7 +40,7 @@ export class BaseService {
}

protected static async patchApi<T>(path: string, body: any) {
const httpClient = getHttpClient(true);
const httpClient = await getHttpClient(true);
const response = await httpClient.patch<T>(path, body);
return response.data;
}
Expand Down
1 change: 0 additions & 1 deletion src/backend/printers.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
PreCreatePrinter,
} from "@/models/printers/crud/create-printer.model";
import { newRandomNamePair } from "@/shared/noun-adjectives.data";
import validator from "validator";

export class PrintersService extends BaseService {
static applyLoginDetailsPatchForm(
Expand Down
4 changes: 2 additions & 2 deletions src/components/Generic/PrintJobsMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@

<script lang="ts">
import { defineComponent } from "vue";
import { usePrinterStore } from "../../store/printer.store";
import { usePrinterStateStore } from "../../store/printer-state.store";
import { usePrinterStore } from "@/store/printer.store";
import { usePrinterStateStore } from "@/store/printer-state.store";
interface Data {
search: string;
Expand Down
16 changes: 1 addition & 15 deletions src/components/Generic/Snackbars/AppErrorSnackbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,7 @@
<div>
<span class="font-weight-bold text-button">{{ snackbarTitle }}</span>
<div v-if="snackbarSubtitle?.length">
{{ expandError && fullSubtitle?.length ? fullSubtitle : snackbarSubtitle }}
<br />
<v-btn
v-if="fullSubtitle?.length && fullSubtitle?.length < 35"
class="float-end"
icon
@click="expandError = !expandError"
>
<v-icon v-if="!expandError">expand_more</v-icon>
<v-icon v-if="expandError">expand_less</v-icon>
</v-btn>
{{ snackbarSubtitle }}
</div>
</div>
</v-col>
Expand All @@ -43,7 +33,6 @@
</v-btn>
</v-col>
</v-row>
<v-row v-if="expandError"></v-row>
</v-snackbar>
</template>
<script lang="ts" setup>
Expand All @@ -53,16 +42,13 @@ import { onMounted, ref } from "vue";
const snackbar = useSnackbar();
const snackbarTimeout = ref(-1);
const snackbarOpened = ref(false);
const expandError = ref(false);
const snackbarTitle = ref("");
const snackbarSubtitle = ref("");
const fullSubtitle = ref("");
onMounted(() => {
snackbar.onErrorMessage((data: ErrorMessage) => {
snackbarTitle.value = data.title;
snackbarSubtitle.value = data.subtitle ?? "";
fullSubtitle.value = data.fullSubtitle ?? "";
snackbarOpened.value = true;
snackbarTimeout.value = data.timeout ?? 10000;
});
Expand Down
Loading

0 comments on commit 072343b

Please sign in to comment.