From a95e9ad70cbcc58f665d9525dc533790af50f15e Mon Sep 17 00:00:00 2001 From: David Zwart Date: Sat, 26 Aug 2023 14:13:02 +0200 Subject: [PATCH 01/10] fix: remove VueAxios --- package.json | 1 - pnpm-lock.yaml | 13 ------------- src/main.ts | 5 ++--- 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 622e2b97..ce708160 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c042b5e5..533bea4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,9 +53,6 @@ dependencies: vue: specifier: 2.7.14 version: 2.7.14 - vue-axios: - specifier: 3.5.2 - version: 3.5.2(axios@1.5.0)(vue@2.7.14) vue-router: specifier: 3.6.5 version: 3.6.5(vue@2.7.14) @@ -11017,16 +11014,6 @@ packages: extsprintf: 1.3.0 dev: true - /vue-axios@3.5.2(axios@1.5.0)(vue@2.7.14): - resolution: {integrity: sha512-GP+dct7UlAWkl1qoP3ppw0z6jcSua5/IrMpjB5O8bh089iIiJ+hdxPYH2NPEpajlYgkW5EVMP95ttXWdas1O0g==} - peerDependencies: - axios: '*' - vue: ^3.0.0 || ^2.0.0 - dependencies: - axios: 1.5.0 - vue: 2.7.14 - dev: false - /vue-cli-plugin-vuetify@2.5.8(sass-loader@13.3.2)(vue@2.7.14)(vuetify-loader@1.9.2)(webpack@5.88.2): resolution: {integrity: sha512-uqi0/URJETJBbWlQHD1l0pnY7JN8Ytu+AL1fw50HFlGByPa8/xx+mq19GkFXA9FcwFT01IqEc/TkxMPugchomg==} peerDependencies: diff --git a/src/main.ts b/src/main.ts index fe66ad92..5c2b7ab2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,8 +4,6 @@ import pinia from "./plugins/pinia"; import VueRouter from "vue-router"; import appRouter from "./router"; import vuetify from "./plugins/vuetify"; -import axios from "axios"; -import VueAxios from "vue-axios"; import { configureVeeValidate } from "@/plugins/veevalidate"; import { generateAppConstants } from "@/shared/app.constants"; import { registerFileDropDirective } from "@/directives/file-upload.directive"; @@ -17,7 +15,8 @@ import BaseDialog from "@/components/Generic/Dialogs/BaseDialog.vue"; import { useSnackbar } from "./shared/snackbar.composable"; Vue.config.productionTip = false; -Vue.use(VueAxios, axios); +Vue.config.silent = true; +Vue.config.devtools = false; configureVeeValidate(); registerFileDropDirective(); From 0297aa702df94cc466c770352a498b3a16514d9d Mon Sep 17 00:00:00 2001 From: David Zwart Date: Sat, 26 Aug 2023 14:13:59 +0200 Subject: [PATCH 02/10] chore: cleanup route names --- src/router/index.ts | 29 +++++++++++++---------------- src/router/route-names.ts | 10 ++++++++++ src/router/utils.ts | 2 +- 3 files changed, 24 insertions(+), 17 deletions(-) create mode 100644 src/router/route-names.ts diff --git a/src/router/index.ts b/src/router/index.ts index 1d69ab60..decf816f 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -15,6 +15,8 @@ import DiagnosticsSettings from "../components/Settings/DiagnosticsSettings.vue" import { useAuthStore } from "@/store/auth.store"; import LoginView from "../components/Login/LoginView.vue"; import NotFoundView from "../components/NotFound/NotFoundView.vue"; +import LackingPermission from "@/components/Login/LackingPermission.vue"; +import { RouteNames } from "@/router/route-names"; const NeedsAuth = { requiresAuth: true, @@ -23,17 +25,6 @@ const NoAuth = { requiresAuth: false, }; -export const RouteNames = { - Home: "Home", - Login: "Login", - Printers: "PrintersView", - Settings: "Settings", - PrintStatistics: "Print Statistics", - About: "About", - // This route can be used for routes that are not found or data that cannot be found (by routing) - NotFound: "NotFound", -}; - const routes: Array = [ { path: "/", @@ -49,7 +40,7 @@ const routes: Array = [ }, { path: "/printers", - name: "PrintersView", + name: RouteNames.PrintersView, meta: NeedsAuth, component: PrintersView, }, @@ -107,19 +98,25 @@ const routes: Array = [ }, { path: "/statistics", - name: "Print Statistics", + name: RouteNames.PrintStatistics, meta: NeedsAuth, component: PrintStatisticsView, }, { path: "/about", - name: "About", + name: RouteNames.About, meta: NeedsAuth, component: AboutHelp, }, { path: "*", - name: "NotFound", + name: RouteNames.LackingPermission, + meta: NoAuth, + component: LackingPermission, + }, + { + path: "*", + name: RouteNames.NotFound, meta: NoAuth, component: NotFoundView, }, @@ -143,7 +140,7 @@ appRouter.beforeEach(async (to, from, next) => { } authStore.loadTokens(); - if (!authStore.isLoggedIn) { + if (!authStore.hasAuthToken) { console.debug("Not logged in, redirecting to login page"); if (from.path == "/login") { throw new Error("Already on login page, cannot redirect"); diff --git a/src/router/route-names.ts b/src/router/route-names.ts new file mode 100644 index 00000000..4aa835f4 --- /dev/null +++ b/src/router/route-names.ts @@ -0,0 +1,10 @@ +export const RouteNames = { + Home: "Home", + Login: "Login", + PrintersView: "PrintersView", + Settings: "Settings", + PrintStatistics: "Print Statistics", + About: "About", + LackingPermission: "LackingPermission", + NotFound: "NotFound", +}; diff --git a/src/router/utils.ts b/src/router/utils.ts index ac60f86e..847ac625 100644 --- a/src/router/utils.ts +++ b/src/router/utils.ts @@ -1,5 +1,5 @@ import VueRouter from "vue-router"; -import { RouteNames } from "./index"; +import { RouteNames } from "./route-names"; export async function routeToPath(router: VueRouter, name: string) { return router.push({ name: name }); From 1dfc4f1fd28712642dbd4c67d2aea2598ed5189a Mon Sep 17 00:00:00 2001 From: David Zwart Date: Sat, 26 Aug 2023 14:15:06 +0200 Subject: [PATCH 03/10] fix: make getHttpClient async and apply better axios instance creation --- src/backend/auth.service.ts | 8 ++++---- src/backend/base.service.ts | 12 ++++++------ src/shared/http-client.ts | 12 ++++++++---- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/backend/auth.service.ts b/src/backend/auth.service.ts index 99f48014..110edcb4 100644 --- a/src/backend/auth.service.ts +++ b/src/backend/auth.service.ts @@ -7,12 +7,12 @@ export interface Tokens { export class AuthService { static async getLoginRequired() { - const httpClient = getHttpClient(false); + const httpClient = await getHttpClient(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); return await httpClient.post("api/auth/login", { username, password, @@ -20,12 +20,12 @@ export class AuthService { } static async refreshLogin(refreshToken: string) { - const httpClient = getHttpClient(false); + const httpClient = await getHttpClient(false); return await httpClient.post<{ token: string }>("api/auth/refresh", { refreshToken }); } static async verifyLogin() { - const httpClient = getHttpClient(true); + const httpClient = await getHttpClient(true); return await httpClient.post("api/auth/verify"); } } diff --git a/src/backend/base.service.ts b/src/backend/base.service.ts index 5b6ffc4f..f80751a8 100644 --- a/src/backend/base.service.ts +++ b/src/backend/base.service.ts @@ -3,19 +3,19 @@ import { getHttpClient } from "@/shared/http-client"; export class BaseService { protected static async getApi(path: string) { - const httpClient = getHttpClient(true); + const httpClient = await getHttpClient(true); const response = await httpClient.get(path); return response.data; } protected static async putApi(path: string, body?: any) { - const httpClient = getHttpClient(true); + const httpClient = await getHttpClient(true); const response = await httpClient.put(path, body); return response.data; } protected static async postApi(path: string, body?: any) { - const httpClient = getHttpClient(true); + const httpClient = await getHttpClient(true); const response = await httpClient.post(path, body); return response.data; } @@ -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(path: string, body?: any) { - const httpClient = getHttpClient(true); + const httpClient = await getHttpClient(true); const response = await httpClient.request({ url: path, method: "delete", @@ -40,7 +40,7 @@ export class BaseService { } protected static async patchApi(path: string, body: any) { - const httpClient = getHttpClient(true); + const httpClient = await getHttpClient(true); const response = await httpClient.patch(path, body); return response.data; } diff --git a/src/shared/http-client.ts b/src/shared/http-client.ts index d25045d7..d696aabf 100644 --- a/src/shared/http-client.ts +++ b/src/shared/http-client.ts @@ -1,16 +1,20 @@ -import axios from "axios"; +import axios, { AxiosError, AxiosRequestConfig, HttpStatusCode } from "axios"; import { useAuthStore } from "@/store/auth.store"; import Vue from "vue"; +import { useSnackbar } from "@/shared/snackbar.composable"; +import { sleep } from "@/utils/time.utils"; +import { useRouter } from "vue-router/composables"; +import { RouteNames } from "@/router/route-names"; /** * Made async for future possibility of getting base URI externally or asynchronously */ export async function getBaseUri() { - // return Vue.config.devtools ? "https://demo.fdm-monster.net" : ""; - return Vue.config.devtools ? "http://localhost:4000/" : "/"; // Same-origin policy + // return process.env.NODE_ENV === "development" ? "https://demo.fdm-monster.net" : ""; + return process.env.NODE_ENV === "development" ? "http://localhost:4000/" : "/"; // Same-origin policy } -export function getHttpClient(withAuth: boolean = true) { +export async function getHttpClient(withAuth: boolean = true) { axios.interceptors.request.use(async (config) => { config.baseURL = await getBaseUri(); if (withAuth) { From 8a666051e917f199f707e585dfe38cea6265fa21 Mon Sep 17 00:00:00 2001 From: David Zwart Date: Sat, 26 Aug 2023 14:16:59 +0200 Subject: [PATCH 04/10] feat: add LackingPermission component, adjust authStore --- src/App.vue | 6 +-- src/AppLoader.vue | 62 +++++++++++++--------- src/components/Generic/TopBar.vue | 2 +- src/components/Login/LackingPermission.vue | 25 +++++++++ src/components/Login/LoginView.vue | 2 +- src/components/PrinterGrid/PrinterGrid.vue | 6 +-- src/store/auth.store.ts | 21 ++++++-- 7 files changed, 85 insertions(+), 39 deletions(-) create mode 100644 src/components/Login/LackingPermission.vue diff --git a/src/App.vue b/src/App.vue index 533f3833..be2a752a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -5,8 +5,8 @@ - - + + @@ -79,7 +79,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); diff --git a/src/AppLoader.vue b/src/AppLoader.vue index 1aa003ca..9d4898a6 100644 --- a/src/AppLoader.vue +++ b/src/AppLoader.vue @@ -1,21 +1,21 @@ +v + diff --git a/src/components/Generic/TopBar.vue b/src/components/Generic/TopBar.vue index e2f2bfe9..731a9f2c 100644 --- a/src/components/Generic/TopBar.vue +++ b/src/components/Generic/TopBar.vue @@ -9,7 +9,7 @@ + + + + FDM + Monster + + + + FDM Monster Background + + + You do not have permission to access this page. Page: FloorSettings + + The following permission is required: + Server.FakePermission.Read + + + + diff --git a/src/components/Login/LoginView.vue b/src/components/Login/LoginView.vue index db80d661..d895d62d 100644 --- a/src/components/Login/LoginView.vue +++ b/src/components/Login/LoginView.vue @@ -31,7 +31,7 @@ const authStore = useAuthStore(); onMounted(() => { authStore.loadTokens(); - if (authStore.isLoggedIn || authStore.loginRequired === false) { + if (authStore.hasAuthToken || authStore.loginRequired === false) { const routePath = route.query.redirect; if (!routePath) { diff --git a/src/components/PrinterGrid/PrinterGrid.vue b/src/components/PrinterGrid/PrinterGrid.vue index b305d576..30a7c760 100644 --- a/src/components/PrinterGrid/PrinterGrid.vue +++ b/src/components/PrinterGrid/PrinterGrid.vue @@ -78,11 +78,11 @@ diff --git a/src/components/Generic/Snackbars/AppErrorSnackbar.vue b/src/components/Generic/Snackbars/AppErrorSnackbar.vue index 9405dd3d..ce92e4aa 100644 --- a/src/components/Generic/Snackbars/AppErrorSnackbar.vue +++ b/src/components/Generic/Snackbars/AppErrorSnackbar.vue @@ -22,17 +22,7 @@
{{ snackbarTitle }}
- {{ expandError && fullSubtitle?.length ? fullSubtitle : snackbarSubtitle }} -
- - expand_more - expand_less - + {{ snackbarSubtitle }}
@@ -43,7 +33,6 @@ - diff --git a/src/components/Login/LoginForm.vue b/src/components/Login/LoginForm.vue index 2972c0d1..9c16c9ca 100644 --- a/src/components/Login/LoginForm.vue +++ b/src/components/Login/LoginForm.vue @@ -77,7 +77,7 @@ - diff --git a/src/components/Login/PermissionDenied.vue b/src/components/Login/PermissionDenied.vue new file mode 100644 index 00000000..1590d331 --- /dev/null +++ b/src/components/Login/PermissionDenied.vue @@ -0,0 +1,75 @@ + + diff --git a/src/main.ts b/src/main.ts index 5c2b7ab2..d1ca18a9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,6 +13,7 @@ import * as Sentry from "@sentry/vue"; import { BrowserTracing } from "@sentry/vue"; import BaseDialog from "@/components/Generic/Dialogs/BaseDialog.vue"; import { useSnackbar } from "./shared/snackbar.composable"; +import { AxiosError } from "axios"; Vue.config.productionTip = false; Vue.config.silent = true; @@ -49,11 +50,22 @@ Vue.use(VueRouter); Vue.component(BaseDialog.name, BaseDialog); Vue.config.errorHandler = (err) => { - console.error(`An error was caught [${err.name}]:\n ${err.message}\n ${err.stack}`); + if (err instanceof AxiosError) { + console.error( + `An error was caught [${err.name}]:\n ${err.message}\n ${err.config?.url}\n${err.stack}` + ); + useSnackbar().openErrorMessage({ + title: "An error occurred", + subtitle: err.message, + timeout: 5000, + }); + return; + } else { + console.error(`An error was caught [${err.name}]:\n ${err.message}\n ${err.stack}`); + } useSnackbar().openErrorMessage({ title: "An error occurred", - subtitle: err.message?.length <= 35 ? err.message : err.message.slice(0, 23) + "...", - fullSubtitle: err.message, + subtitle: err.message, timeout: 5000, }); }; diff --git a/src/router/index.ts b/src/router/index.ts index 905decd8..47e82e67 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -15,7 +15,7 @@ import DiagnosticsSettings from "../components/Settings/DiagnosticsSettings.vue" import { useAuthStore } from "@/store/auth.store"; import LoginView from "../components/Login/LoginView.vue"; import NotFoundView from "../components/NotFound/NotFoundView.vue"; -import LackingPermission from "@/components/Login/LackingPermission.vue"; +import PermissionDenied from "@/components/Login/PermissionDenied.vue"; import { RouteNames } from "@/router/route-names"; const NeedsAuth = { @@ -109,10 +109,10 @@ const routes: Array = [ component: AboutHelp, }, { - path: "/lacking-permission", - name: RouteNames.LackingPermission, + path: "/permission-denied", + name: RouteNames.PermissionDenied, meta: NeedsAuth, - component: LackingPermission, + component: PermissionDenied, }, { path: "*", diff --git a/src/router/route-names.ts b/src/router/route-names.ts index 4aa835f4..562b4eaa 100644 --- a/src/router/route-names.ts +++ b/src/router/route-names.ts @@ -5,6 +5,6 @@ export const RouteNames = { Settings: "Settings", PrintStatistics: "Print Statistics", About: "About", - LackingPermission: "LackingPermission", + PermissionDenied: "PermissionDenied", NotFound: "NotFound", }; diff --git a/src/shared/http-client.ts b/src/shared/http-client.ts index fd6e4e45..558381dc 100644 --- a/src/shared/http-client.ts +++ b/src/shared/http-client.ts @@ -1,5 +1,6 @@ import axios, { AxiosError, AxiosRequestConfig, HttpStatusCode } from "axios"; import { useAuthStore } from "@/store/auth.store"; +import { useEventBus } from "@vueuse/core"; /** * Made async for future possibility of getting base URI externally or asynchronously @@ -52,17 +53,34 @@ export async function getHttpClient(withAuth: boolean = true, autoHandle401: boo const { response, config } = error; if (!response) { console.error("No response was returned by axios", error); - return Promise.reject(error); + return Promise.reject({ + message: `No response was returned by axios - URL ${config?.url}`, + stack: error.stack, + }); } - if (![HttpStatusCode.Unauthorized, HttpStatusCode.Unauthorized].includes(response.status)) { + if (![HttpStatusCode.Unauthorized, HttpStatusCode.Forbidden].includes(response.status)) { // Timeout issues etc? console.error("Error in axios response interceptor which is not 401 or 403", error); - return Promise.reject(error); + return Promise.reject({ + message: `${error.message} - URL ${config?.url}`, + stack: error.stack, + }); } if (response.status === HttpStatusCode.Forbidden) { - console.error("403 Forbidden"); + const data = error?.response?.data as { + error?: string; + roles?: string[]; + permissions?: string[]; + }; + console.error("403 Forbidden", data); + useEventBus("auth:permission-denied").emit({ + roles: data?.roles, + permissions: data?.permissions, + error: data?.error, + url: config?.url, + }); return Promise.reject(error); } @@ -72,15 +90,20 @@ export async function getHttpClient(withAuth: boolean = true, autoHandle401: boo // If this is called on AppLoader and failing, poll it if status 0 await authStore.checkLoginRequired(); + // If this fails, the server is just confused const success = await authStore.verifyOrRefreshLoginOnce(); if (success) { - console.debug("Redoing request", config?.url); + console.debug("Redoing request without interceptors", config?.url); return axios(config as AxiosRequestConfig); } - console.error("401 Unauthorized"); + console.error("[HttpClient] 401 Unauthorized - emitting 'auth:failure'"); + useEventBus("auth:failure").emit({ + url: config?.url, + error: error.message, + }); - return error; + return Promise.reject(error); } ); return instance; diff --git a/src/shared/snackbar.composable.ts b/src/shared/snackbar.composable.ts index 5a9b5f3d..3a92bf01 100644 --- a/src/shared/snackbar.composable.ts +++ b/src/shared/snackbar.composable.ts @@ -20,7 +20,6 @@ export interface InfoMessage { export interface ErrorMessage { title: string; subtitle?: string | null; - fullSubtitle?: string | null; timeout?: number; // The idea is that the error can be revisited on a separate page/dialog // url?: string; diff --git a/src/store/auth.store.ts b/src/store/auth.store.ts index 1a77af11..8eb9feab 100644 --- a/src/store/auth.store.ts +++ b/src/store/auth.store.ts @@ -3,15 +3,14 @@ import { useJwt } from "@vueuse/integrations/useJwt"; import type { JwtPayload } from "jwt-decode"; import { AuthService, type Tokens } from "@/backend/auth.service"; import { AxiosError, HttpStatusCode } from "axios"; -import { RemovableRef, useLocalStorage } from "@vueuse/core"; export interface IClaims extends JwtPayload { name: string; } export interface AuthState { - refreshToken: RemovableRef | null; - token: RemovableRef | null; + refreshToken: string | null; + token: string | null; loginRequired: boolean | null; } @@ -46,16 +45,15 @@ export const useAuthStore = defineStore("auth", { }, logout() { console.debug("Logging out"); - this.setIdToken(null); - this.setRefreshToken(null); + this.setIdToken(undefined); + this.setRefreshToken(undefined); }, async verifyOrRefreshLoginOnce() { try { await AuthService.verifyLogin(); return true; } catch (e1) { - const canRefresh = this.hasAuthToken && this.isLoginExpired && this.hasRefreshToken; - if (canRefresh) { + if (this.hasRefreshToken) { try { await this.refreshLoginToken(); await AuthService.verifyLogin(); @@ -69,10 +67,8 @@ export const useAuthStore = defineStore("auth", { } }, loadTokens() { - const tokenRef = useLocalStorage("token", null); - const refreshTokenRef = useLocalStorage("refreshToken", null); - this.token = tokenRef.value; - this.refreshToken = refreshTokenRef.value; + this.token = localStorage.getItem("token"); + this.refreshToken = localStorage.getItem("refreshToken"); }, async refreshLoginToken() { if (!this.refreshToken) { @@ -88,7 +84,7 @@ export const useAuthStore = defineStore("auth", { }) .catch((e: AxiosError) => { if (e.response?.status == HttpStatusCode.Unauthorized) { - this.setTokens(null, null); + this.setTokens(undefined, undefined); console.error( "refreshLoginToken: authentication error, failed to refresh tokens", e.response?.status @@ -102,23 +98,25 @@ export const useAuthStore = defineStore("auth", { throw e; }); }, - setTokens(token: string | null, refreshToken: string | null) { + setTokens(token?: string, refreshToken?: string) { this.setIdToken(token); this.setRefreshToken(refreshToken); }, - setIdToken(token: string | null) { - if (token == null) { + setIdToken(token?: string) { + if (!token?.length) { localStorage.removeItem("token"); + } else { + localStorage.setItem("token", token as string); + this.token = token; } - localStorage.setItem("token", token as string); - this.token = token; }, - setRefreshToken(refreshToken: string | null) { - if (refreshToken == null) { + setRefreshToken(refreshToken?: string) { + if (!refreshToken?.length) { localStorage.removeItem("refreshToken"); + } else { + localStorage.setItem("refreshToken", refreshToken as string); + this.refreshToken = refreshToken; } - localStorage.setItem("refreshToken", refreshToken as string); - this.refreshToken = refreshToken; }, }, getters: { diff --git a/src/store/floor.store.ts b/src/store/floor.store.ts index 356e9020..fe87cb01 100644 --- a/src/store/floor.store.ts +++ b/src/store/floor.store.ts @@ -78,6 +78,7 @@ export const useFloorStore = defineStore("Floors", { return data; }, saveFloors(floors: Floor[]) { + if (!floors?.length) return; this.floors = floors.sort((f, f2) => f.floor - f2.floor); const floorId = this.selectedFloor?._id; const foundFloor = this.floors.find((f) => f._id === floorId); diff --git a/src/store/printer.store.ts b/src/store/printer.store.ts index 22b1bdff..d4636af8 100644 --- a/src/store/printer.store.ts +++ b/src/store/printer.store.ts @@ -97,6 +97,10 @@ export const usePrinterStore = defineStore("Printers", { return data; }, setPrinters(printers: Printer[]) { + if (!printers?.length) { + this.printers = []; + return; + } const viewedPrinterId = this.sideNavPrinter?.id; if (viewedPrinterId) { this.sideNavPrinter = printers.find((p) => p.id === viewedPrinterId);