From d572cae89245f2db3816b613e502a03bbc9a4621 Mon Sep 17 00:00:00 2001 From: Samuel Weirich <4281791+SamuelWei@users.noreply.github.com> Date: Wed, 14 Jan 2026 20:15:10 +0100 Subject: [PATCH] feat: add frontend version checking for outdated clients Introduce `FrontendVersion` middleware to backend to attach the current frontend version hash in the HTTP response headers. This allows the client to detect if the frontend version is outdated. Create a new `FrontendOutdatedDialog` Vue component that watches for changes in the frontend version and prompts users to reload if they are running an outdated version. Integrate this component into the main application layout. Update the `Api` service to set up an Axios interceptor to read the 'X-Frontend-Version' header from responses and store it in the settings store, triggering the version check logic. Enhance user experience by ensuring they always use the latest client features and bug fixes. --- app/Http/Kernel.php | 2 + app/Http/Middleware/FrontendVersion.php | 32 +++++++++++++++ lang/en/app.php | 1 + resources/js/components/App.vue | 3 +- .../js/components/FrontendOutdatedDialog.vue | 41 +++++++++++++++++++ resources/js/services/Api.js | 11 +++++ resources/js/stores/loading.js | 4 ++ resources/js/stores/settings.js | 10 +++++ 8 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 app/Http/Middleware/FrontendVersion.php create mode 100644 resources/js/components/FrontendOutdatedDialog.vue diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index e8936955d..db94b0829 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -3,6 +3,7 @@ namespace App\Http; use App\Http\Middleware\EnsureModelNotStale; +use App\Http\Middleware\FrontendVersion; use App\Http\Middleware\LogContext; use App\Http\Middleware\RequestMetricsMiddleware; use App\Http\Middleware\RoomAuthenticate; @@ -59,6 +60,7 @@ class Kernel extends HttpKernel \Illuminate\Routing\Middleware\SubstituteBindings::class, \App\Http\Middleware\SetApplicationLocale::class, LogContext::class, + FrontendVersion::class, ], ]; diff --git a/app/Http/Middleware/FrontendVersion.php b/app/Http/Middleware/FrontendVersion.php new file mode 100644 index 000000000..4cf7ec093 --- /dev/null +++ b/app/Http/Middleware/FrontendVersion.php @@ -0,0 +1,32 @@ +headers->set('X-Frontend-Version', $manifestHash); + } + + return $response; + } +} diff --git a/lang/en/app.php b/lang/en/app.php index f50528ac5..1e6b23e1b 100644 --- a/lang/en/app.php +++ b/lang/en/app.php @@ -34,6 +34,7 @@ 'error' => 'An error occurred', 'errors' => [ 'attendance_agreement_missing' => 'Consent to record attendance is required.', + 'frontend_outdated' => 'Your version of the application is outdated. Please reload to continue.', 'join_failed' => 'Joining the room has failed because a connection error has occurred.', 'meeting_attendance_disabled' => 'Attendance logging was not active at this meeting.', 'meeting_attendance_not_ended' => 'The attendance logs are not yet available for this meeting as it has not yet ended.', diff --git a/resources/js/components/App.vue b/resources/js/components/App.vue index 273ba86d9..90ff12b01 100644 --- a/resources/js/components/App.vue +++ b/resources/js/components/App.vue @@ -26,6 +26,7 @@
+
@@ -37,7 +38,7 @@ import { useLoadingStore } from "../stores/loading"; import { useSettingsStore } from "../stores/settings"; import Toast from "primevue/toast"; import { useRoute } from "vue-router"; -import { computed } from "vue"; +import { computed, watch } from "vue"; const loadingStore = useLoadingStore(); const settingsStore = useSettingsStore(); diff --git a/resources/js/components/FrontendOutdatedDialog.vue b/resources/js/components/FrontendOutdatedDialog.vue new file mode 100644 index 000000000..4bccc6acf --- /dev/null +++ b/resources/js/components/FrontendOutdatedDialog.vue @@ -0,0 +1,41 @@ + + + diff --git a/resources/js/services/Api.js b/resources/js/services/Api.js index d70623a9e..78547c135 100644 --- a/resources/js/services/Api.js +++ b/resources/js/services/Api.js @@ -18,6 +18,17 @@ export class Api { this.t = i18n.global.t; } + setupAxiosInterceptors(settingsStore) { + // Add a response interceptor + axios.interceptors.response.use(function onFulfilled(response) { + const frontendHash = response.headers["x-frontend-version"]; + if (frontendHash !== undefined) + settingsStore.setFrontendVersion(frontendHash); + + return response; + }); + } + /** * Makes a request with the passed params. * diff --git a/resources/js/stores/loading.js b/resources/js/stores/loading.js index dedfbaf3a..29ed5134c 100644 --- a/resources/js/stores/loading.js +++ b/resources/js/stores/loading.js @@ -1,6 +1,7 @@ import { defineStore } from "pinia"; import { useAuthStore } from "./auth"; import { useSettingsStore } from "./settings"; +import { useApi } from "../composables/useApi.js"; export const useLoadingStore = defineStore("loading", { state: () => { @@ -23,12 +24,15 @@ export const useLoadingStore = defineStore("loading", { }, actions: { async initialize() { + const api = useApi(); const auth = useAuthStore(); const settings = useSettingsStore(); this.setLoading(); await settings.getSettings(); + settings.setupAxiosInterceptors(); + await auth.getCurrentUser(); this.initialized = true; diff --git a/resources/js/stores/settings.js b/resources/js/stores/settings.js index 4507eef5a..0bc797f62 100644 --- a/resources/js/stores/settings.js +++ b/resources/js/stores/settings.js @@ -8,6 +8,7 @@ export const useSettingsStore = defineStore("settings", { state: () => { return { settings: null, + frontendVersion: null, }; }, getters: { @@ -30,5 +31,14 @@ export const useSettingsStore = defineStore("settings", { this.settings.theme.rounded, ); }, + + setFrontendVersion(version) { + this.frontendVersion = version; + }, + + setupAxiosInterceptors() { + const api = useApi(); + api.setupAxiosInterceptors(this); + }, }, });