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);
+ },
},
});