auto-logout after session expired

This commit is contained in:
2026-02-14 12:41:32 +01:00
parent 1d5b730e11
commit 2b237d3d71
7 changed files with 78 additions and 6 deletions

View File

@ -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.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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.",

View File

@ -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ň.",

View File

@ -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)

View File

@ -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
}