+
@@ -315,4 +317,4 @@ const footerLinks = computed(() => [
-
\ No newline at end of file
+
diff --git a/frontend/stores/auth.ts b/frontend/stores/auth.ts
index 308831e..05610e8 100644
--- a/frontend/stores/auth.ts
+++ b/frontend/stores/auth.ts
@@ -12,13 +12,129 @@ export const useAuthStore = defineStore("auth", {
activeTenantData: null as any,
loading: true as boolean,
notLoggedIn: true,
+ sessionWarningVisible: false,
+ sessionWarningRemainingSeconds: 0,
+ sessionWarningLeadMs: 5 * 60 * 1000,
+ sessionWarningTimer: null as ReturnType
| null,
+ sessionLogoutTimer: null as ReturnType | null,
+ sessionCountdownTimer: null as ReturnType | null,
}),
actions: {
- async persist(token) {
+ decodeTokenExpiryMs(token: string) {
+ try {
+ const parts = token.split(".")
+ if (parts.length < 2) return null
+
+ const payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")))
+ if (!payload?.exp) return null
+
+ return Number(payload.exp) * 1000
+ } catch (err) {
+ console.error("Could not decode token expiry", err)
+ return null
+ }
+ },
+
+ clearSessionTimers() {
+ if (!process.client) return
+
+ if (this.sessionWarningTimer) {
+ clearTimeout(this.sessionWarningTimer)
+ this.sessionWarningTimer = null
+ }
+
+ if (this.sessionLogoutTimer) {
+ clearTimeout(this.sessionLogoutTimer)
+ this.sessionLogoutTimer = null
+ }
+
+ if (this.sessionCountdownTimer) {
+ clearInterval(this.sessionCountdownTimer)
+ this.sessionCountdownTimer = null
+ }
+ },
+
+ startSessionCountdown(expiresAtMs: number) {
+ if (!process.client) return
+
+ if (this.sessionCountdownTimer) {
+ clearInterval(this.sessionCountdownTimer)
+ }
+
+ this.sessionCountdownTimer = setInterval(() => {
+ const remaining = Math.max(0, Math.ceil((expiresAtMs - Date.now()) / 1000))
+ this.sessionWarningRemainingSeconds = remaining
+
+ if (remaining <= 0 && this.sessionCountdownTimer) {
+ clearInterval(this.sessionCountdownTimer)
+ this.sessionCountdownTimer = null
+ }
+ }, 1000)
+ },
+
+ openSessionWarning(expiresAtMs: number) {
+ this.sessionWarningVisible = true
+ this.sessionWarningRemainingSeconds = Math.max(0, Math.ceil((expiresAtMs - Date.now()) / 1000))
+ this.startSessionCountdown(expiresAtMs)
+ },
+
+ scheduleSessionTimers(token?: string | null) {
+ if (!process.client) return
+
+ const tokenToUse = token || useCookie("token").value
+
+ this.clearSessionTimers()
+ this.sessionWarningVisible = false
+ this.sessionWarningRemainingSeconds = 0
+
+ if (!tokenToUse) return
+
+ const expiresAtMs = this.decodeTokenExpiryMs(tokenToUse)
+ if (!expiresAtMs) return
+
+ const now = Date.now()
+ const msUntilLogout = expiresAtMs - now
+
+ if (msUntilLogout <= 0) {
+ void this.logout()
+ return
+ }
+
+ this.sessionLogoutTimer = setTimeout(() => {
+ this.sessionWarningVisible = false
+ void this.logout()
+ }, msUntilLogout)
+
+ const msUntilWarning = msUntilLogout - this.sessionWarningLeadMs
+
+ if (msUntilWarning <= 0) {
+ this.openSessionWarning(expiresAtMs)
+ return
+ }
+
+ this.sessionWarningTimer = setTimeout(() => {
+ this.openSessionWarning(expiresAtMs)
+ }, msUntilWarning)
+ },
+
+ setToken(token: string | null) {
+ useCookie("token").value = token
+
+ if (!token) {
+ this.clearSessionTimers()
+ this.sessionWarningVisible = false
+ this.sessionWarningRemainingSeconds = 0
+ return
+ }
+
+ this.scheduleSessionTimers(token)
+ },
+
+ async persist(token: string | null) {
console.log("On Web")
- useCookie("token").value = token // persistieren
+ this.setToken(token)
},
@@ -74,8 +190,7 @@ export const useAuthStore = defineStore("auth", {
console.log("Token: " + token)
// 1. WICHTIG: Token sofort ins Cookie schreiben, damit es persistiert wird
- const tokenCookie = useCookie("token")
- tokenCookie.value = token
+ this.setToken(token)
// 2. User Daten laden
await this.fetchMe(token)
@@ -112,19 +227,22 @@ export const useAuthStore = defineStore("auth", {
this.resetState()
// Token löschen
- useCookie("token").value = null
+ this.setToken(null)
// Nur beim expliziten Logout navigieren wir
navigateTo("/login")
},
resetState() {
+ this.clearSessionTimers()
this.user = null
this.permissions = []
this.profile = null
this.activeTenant = null
this.tenants = []
this.activeTenantData = null
+ this.sessionWarningVisible = false
+ this.sessionWarningRemainingSeconds = 0
},
async fetchMe(jwt= null) {
@@ -162,6 +280,8 @@ export const useAuthStore = defineStore("auth", {
this.activeTenantData = me.tenants.find(i => i.id === me.activeTenant)
}
+ this.scheduleSessionTimers(tokenToUse)
+
console.log(this)
} catch (err: any) {
@@ -170,12 +290,27 @@ export const useAuthStore = defineStore("auth", {
// Stattdessen nur den State sauber machen und Token löschen
this.resetState()
- useCookie("token").value = null
+ this.setToken(null)
// Wir werfen den Fehler nicht weiter, damit initStore normal durchläuft
}
},
+ async refreshSession() {
+ try {
+ const { token } = await useNuxtApp().$api("/api/auth/refresh", {
+ method: "POST",
+ })
+
+ this.setToken(token)
+ await this.fetchMe(token)
+ this.sessionWarningVisible = false
+ } catch (err) {
+ console.error("JWT refresh failed", err)
+ await this.logout()
+ }
+ },
+
async switchTenant(tenant_id: string) {
console.log("Auth switchTenant")
this.loading = true
@@ -188,7 +323,7 @@ export const useAuthStore = defineStore("auth", {
const {token} = res
- useCookie("token").value = token // persistieren
+ this.setToken(token)
await this.init(token)
@@ -198,4 +333,4 @@ export const useAuthStore = defineStore("auth", {
return this.permissions.includes(key)
}
}
-})
\ No newline at end of file
+})