From 2b237d3d713ebebfe9ebf6fb11ee5c5f79e66629 Mon Sep 17 00:00:00 2001 From: igor Date: Sat, 14 Feb 2026 12:41:32 +0100 Subject: [PATCH] auto-logout after session expired --- frontend/src/locales/cs.json | 3 ++- frontend/src/locales/de.json | 3 ++- frontend/src/locales/en.json | 3 ++- frontend/src/locales/es.json | 3 ++- frontend/src/locales/sk.json | 3 ++- frontend/src/main.ts | 25 ++++++++++++++++++++ frontend/src/utils/error.ts | 44 +++++++++++++++++++++++++++++++++++- 7 files changed, 78 insertions(+), 6 deletions(-) diff --git a/frontend/src/locales/cs.json b/frontend/src/locales/cs.json index 387716d..4308012 100644 --- a/frontend/src/locales/cs.json +++ b/frontend/src/locales/cs.json @@ -81,7 +81,8 @@ "created": "Položka byla vytvořena.", "saved": "Změny byly uloženy.", "updated": "Změny byly aplikovány.", - "deleted": "Položka byla smazána." + "deleted": "Položka byla smazána.", + "sessionExpired": "Relace vypršela. Přihlas se znovu." }, "errors": { "loadDay": "Nepodařilo se načíst den.", diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index eb440a7..ff96c55 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -81,7 +81,8 @@ "created": "Element wurde erstellt.", "saved": "Änderungen wurden gespeichert.", "updated": "Änderungen wurden übernommen.", - "deleted": "Element wurde gelöscht." + "deleted": "Element wurde gelöscht.", + "sessionExpired": "Sitzung ist abgelaufen. Bitte melde dich erneut an." }, "errors": { "loadDay": "Tag konnte nicht geladen werden.", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 8de4816..cff57d7 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -81,7 +81,8 @@ "created": "Item was created.", "saved": "Changes were saved.", "updated": "Changes were applied.", - "deleted": "Item was deleted." + "deleted": "Item was deleted.", + "sessionExpired": "Session expired. Please sign in again." }, "errors": { "loadDay": "Failed to load the day.", diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json index 9da5395..be5e718 100644 --- a/frontend/src/locales/es.json +++ b/frontend/src/locales/es.json @@ -81,7 +81,8 @@ "created": "Elemento creado.", "saved": "Cambios guardados.", "updated": "Cambios aplicados.", - "deleted": "Elemento eliminado." + "deleted": "Elemento eliminado.", + "sessionExpired": "La sesión ha caducado. Inicia sesión de nuevo." }, "errors": { "loadDay": "No se pudo cargar el día.", diff --git a/frontend/src/locales/sk.json b/frontend/src/locales/sk.json index 8cb5b40..eb836f9 100644 --- a/frontend/src/locales/sk.json +++ b/frontend/src/locales/sk.json @@ -81,7 +81,8 @@ "created": "Položka bola vytvorená.", "saved": "Zmeny boli uložené.", "updated": "Zmeny boli aplikované.", - "deleted": "Položka bola zmazaná." + "deleted": "Položka bola zmazaná.", + "sessionExpired": "Relácia vypršala. Prihlás sa znova." }, "errors": { "loadDay": "Nepodarilo sa načítať deň.", diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 77ecfb3..fd21fe2 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -5,12 +5,37 @@ import App from './App.vue' import i18n from './i18n' import router from './router' import { pinia } from './stores' +import { useAuthStore } from './stores/auth' import { useThemeStore } from './stores/theme' +import { useUIStore } from './stores/ui' +import { SESSION_EXPIRED_EVENT } from './utils/error' const app = createApp(App) const themeStore = useThemeStore(pinia) +const authStore = useAuthStore(pinia) +const uiStore = useUIStore(pinia) themeStore.initialize() +const handleSessionExpired = async () => { + const wasAuthenticated = authStore.isAuthenticated + authStore.clearSession() + + if (wasAuthenticated) { + uiStore.info(String(i18n.global.t('ux.toast.sessionExpired'))) + } + + await router.isReady() + if (router.currentRoute.value.name !== 'auth') { + await router.replace({ name: 'auth' }) + } +} + +if (typeof window !== 'undefined') { + window.addEventListener(SESSION_EXPIRED_EVENT, () => { + void handleSessionExpired() + }) +} + app.component('font-awesome-icon', FontAwesomeIcon) app.use(pinia) app.use(router) diff --git a/frontend/src/utils/error.ts b/frontend/src/utils/error.ts index 21856b6..57a8640 100644 --- a/frontend/src/utils/error.ts +++ b/frontend/src/utils/error.ts @@ -1,4 +1,46 @@ -export const toErrorMessage = (error: unknown, fallback: string): string => { +export const SESSION_EXPIRED_EVENT = 'auth:session-expired' + +const TOKEN_ERROR_MESSAGES = new Set(['Invalid or expired token']) +const SESSION_EXPIRED_EVENT_THROTTLE_MS = 500 + +let lastSessionExpiredEventAt = 0 + +const resolveErrorMessage = (error: unknown): string => { + if (typeof error === 'string') { + return error + } + + if (error instanceof Error && typeof error.message === 'string') { + return error.message + } + + return '' +} + +const emitSessionExpiredEvent = () => { + if (typeof window === 'undefined') { + return + } + + const now = Date.now() + if (now - lastSessionExpiredEventAt < SESSION_EXPIRED_EVENT_THROTTLE_MS) { + return + } + + lastSessionExpiredEventAt = now + window.dispatchEvent(new Event(SESSION_EXPIRED_EVENT)) +} + +export const isSessionExpiredError = (error: unknown): boolean => { + return TOKEN_ERROR_MESSAGES.has(resolveErrorMessage(error)) +} + +export const toErrorMessage = (error: unknown, fallback: string): string => { + if (isSessionExpiredError(error)) { + emitSessionExpiredEvent() + return fallback + } + if (typeof error === 'string' && error.length > 0) { return error }