+ Ta session a expiré. Reconnecte-toi pour recharger le menu, la recherche et tes données.
+
+
+
+
+
+
diff --git a/apps/ops/src/api/auth.js b/apps/ops/src/api/auth.js
index cf43779..4892bc7 100644
--- a/apps/ops/src/api/auth.js
+++ b/apps/ops/src/api/auth.js
@@ -1,9 +1,46 @@
+import { ref } from 'vue'
import { BASE_URL } from 'src/config/erpnext'
// Token is optional — in production, nginx injects it server-side.
// Only needed for local dev (VITE_ERP_TOKEN in .env).
const SERVICE_TOKEN = import.meta.env.VITE_ERP_TOKEN || ''
+const isLocalHost = () => window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
+
+// État PARTAGÉ « session Authentik expirée » → l'app affiche un overlay clair (App.vue) au lieu d'une coquille vide (menu blanc).
+export const sessionExpired = ref(false)
+const REAUTH_TS = 'ops.reauth.ts'
+
+// Vérifie que la session Authentik est VIVANTE. whoami (servi par nginx) renvoie 200 avec un courriel VIDE quand la session
+// est morte (ce n'est PAS un 401 → ni _reauth ni le keep-alive ne le voyaient → menu/recherche/avis vides en silence).
+// Ce fetch rafraîchit aussi la fenêtre glissante Authentik. Session morte → 1 rechargement auto (Traefik redirige vers le
+// login), puis overlay si ça persiste (garde anti-boucle 30 s). Renvoie false si la session est morte.
+async function readWhoamiEmail () { // '' = 200 sans courriel · null = injoignable/non-200 (transitoire)
+ try {
+ const res = await fetch(BASE_URL + '/auth/whoami')
+ if (!res.ok) return null
+ const data = await res.json().catch(() => ({}))
+ return data.email || ''
+ } catch { return null }
+}
+export async function verifySessionAlive () {
+ if (isLocalHost()) return true // pas d'Authentik en dev local
+ let email = await readWhoamiEmail()
+ if (email === null) return true // réseau/non-200 = blip transitoire → ne PAS déconnecter
+ if (!email) { // courriel vide → 2e lecture pour filtrer un blip momentané (en-tête Authentik absent l'espace d'un instant)
+ await new Promise(r => setTimeout(r, 1200))
+ const retry = await readWhoamiEmail()
+ if (retry === null) return true
+ email = retry
+ }
+ if (email) { sessionExpired.value = false; try { sessionStorage.removeItem(REAUTH_TS) } catch (e) {} return true }
+ // 2× 200 + courriel vide = session Authentik réellement expirée
+ const last = Number(sessionStorage.getItem(REAUTH_TS) || 0); const now = Date.now()
+ if (now - last > 30000) { try { sessionStorage.setItem(REAUTH_TS, String(now)) } catch (e) {} window.location.reload(); return false }
+ sessionExpired.value = true // déjà tenté récemment → overlay clair (pas de boucle de rechargement)
+ return false
+}
+
// Reconnexion auto sur session Authentik expirée, anti-boucle (1 reload / 20 s max).
let _lastReload = 0
function _reauth (status) {
@@ -62,6 +99,9 @@ export async function getLoggedUser () {
if (res.ok) {
const data = await res.json()
if (data.email && data.email !== '') return data.email
+ // whoami joignable mais courriel VIDE en prod = session Authentik morte → renvoyer '' (signal de session expirée),
+ // PAS de repli sur le propriétaire du jeton ERP (« Administrator ») qui masquerait la panne derrière un menu vide.
+ if (!isLocalHost()) return ''
}
} catch {}
// Fallback: ask ERPNext (returns API token owner, usually "Administrator")
diff --git a/apps/ops/src/composables/useSessionKeepAlive.js b/apps/ops/src/composables/useSessionKeepAlive.js
index 2d7f252..2f3315f 100644
--- a/apps/ops/src/composables/useSessionKeepAlive.js
+++ b/apps/ops/src/composables/useSessionKeepAlive.js
@@ -2,15 +2,15 @@
// Cause : la SPA + /api + /hub sont derrière Authentik ; la session (cookie) expire après une durée d'inactivité.
// Les appels /api (authFetch) se rechargent seuls (_reauth), MAIS les appels /hub (fetch nu) échouent en silence → données absentes.
// Solution : un battement régulier qui (a) RAFRAÎCHIT la session (fenêtre glissante) et (b) recharge proprement si elle est morte.
-import { authFetch } from 'src/api/auth'
-import { BASE_URL } from 'src/config/erpnext'
+import { verifySessionAlive } from 'src/api/auth'
let _timer = null
let _started = false
-// Ping léger à travers la porte Authentik. authFetch gère _reauth (recharge 1×/20 s si 401/redirection IdP/HTML).
+// Battement à travers la porte Authentik : verifySessionAlive rafraîchit la fenêtre glissante ET détecte une session MORTE
+// (whoami 200 + courriel vide, que _reauth ne voyait pas car ce n'est pas un 401) → rechargement propre / overlay clair.
async function ping () {
- try { await authFetch(BASE_URL + '/auth/whoami') } catch (e) { /* réseau transitoire → on réessaie au prochain tick */ }
+ try { await verifySessionAlive() } catch (e) { /* réseau transitoire → on réessaie au prochain tick */ }
}
// Démarre le battement (prod uniquement — pas d'Authentik en dev local). Idempotent.
diff --git a/apps/ops/src/stores/auth.js b/apps/ops/src/stores/auth.js
index d394630..e15931c 100644
--- a/apps/ops/src/stores/auth.js
+++ b/apps/ops/src/stores/auth.js
@@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
-import { getLoggedUser, logout } from 'src/api/auth'
+import { getLoggedUser, logout, verifySessionAlive } from 'src/api/auth'
import { usePermissions } from 'src/composables/usePermissions'
export const useAuthStore = defineStore('auth', () => {
@@ -21,6 +21,10 @@ export const useAuthStore = defineStore('auth', () => {
} finally {
loading.value = false
}
+ // getLoggedUser renvoie '' quand whoami est joignable mais sans courriel = session Authentik expirée (prod).
+ // Sans permissions, l'app afficherait une coquille vide (menu blanc). On relance proprement : rechargement 1× →
+ // login Authentik, sinon overlay clair « session expirée » (verifySessionAlive, anti-boucle).
+ if (!user.value) await verifySessionAlive()
}
return { user, loading, checkSession, doLogout: logout }