Files
FEDEO/frontend/stores/auth.ts
florianfederspiel 47a9af26fe Tenantdaten vollständig laden
Lädt den aktiven Tenant über die Tenant-Route nach und gibt calendarConfig in der Me-Antwort mit zurück.
2026-06-03 09:27:08 +02:00

435 lines
14 KiB
TypeScript

import { defineStore } from "pinia"
import router from "#app/plugins/router";
import {Preferences} from "@capacitor/preferences";
export const useAuthStore = defineStore("auth", {
state: () => ({
user: null as null | {
id?: string;
user_id?: string;
email: string;
tenant_id?: string;
role?: string;
must_change_password?: boolean;
is_admin?: boolean;
},
profile: null as null | any,
tenants: [] as { tenant_id: string; role: string; tenants: { id: string; name: string }; id?: string; name?: string; hasActiveLicense?: boolean; locked?: string | null }[],
permissions: [] as string[],
activeTenant: null as any,
activeTenantData: null as any,
loading: true as boolean,
notLoggedIn: true,
sessionExpired: false,
sessionWarningVisible: false,
sessionWarningRemainingSeconds: 0,
sessionWarningLeadMs: 5 * 60 * 1000,
sessionWarningTimer: null as ReturnType<typeof setTimeout> | null,
sessionLogoutTimer: null as ReturnType<typeof setTimeout> | null,
sessionCountdownTimer: null as ReturnType<typeof setInterval> | null,
}),
actions: {
tokenCookie() {
return useCookie<string | null>("token", { path: "/" })
},
getStoredToken() {
const rootToken = this.tokenCookie().value
if (rootToken || !process.client) return rootToken
const tokenCookie = document.cookie
.split(";")
.map((part) => part.trim())
.find((part) => part.startsWith("token="))
if (tokenCookie) {
return decodeURIComponent(tokenCookie.slice("token=".length))
}
return localStorage.getItem("token")
},
clearScopedTokenCookies() {
if (!process.client) return
const pathname = window.location.pathname || "/"
const pathParts = pathname.split("/").filter(Boolean)
const paths = new Set(["/"])
pathParts.reduce((path, part) => {
const nextPath = `${path === "/" ? "" : path}/${part}`
paths.add(nextPath)
return nextPath
}, "/")
paths.forEach((path) => {
document.cookie = `token=; Max-Age=0; path=${path}`
})
},
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 || this.getStoredToken()
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) {
this.expireSession()
return
}
this.sessionLogoutTimer = setTimeout(() => {
this.expireSession()
}, msUntilLogout)
const msUntilWarning = msUntilLogout - this.sessionWarningLeadMs
if (msUntilWarning <= 0) {
this.openSessionWarning(expiresAtMs)
return
}
this.sessionWarningTimer = setTimeout(() => {
this.openSessionWarning(expiresAtMs)
}, msUntilWarning)
},
setToken(token: string | null) {
this.clearScopedTokenCookies()
this.tokenCookie().value = token
if (process.client) {
if (token) {
localStorage.setItem("token", token)
} else {
localStorage.removeItem("token")
}
}
if (!token) {
this.clearSessionTimers()
this.sessionWarningVisible = false
this.sessionWarningRemainingSeconds = 0
return
}
this.scheduleSessionTimers(token)
},
async persist(token: string | null) {
console.log("On Web")
this.setToken(token)
},
async initStore() {
console.log("Auth initStore")
// 1. Check: Haben wir überhaupt ein Token?
const token = this.getStoredToken()
if (!token) {
// Kein Token -> Wir sind fertig, User ist Gast.
this.user = null
this.loading = false
return
}
// 2. Token existiert -> Versuche User zu laden
await this.fetchMe(token)
// Wenn fetchMe fertig ist (egal ob Erfolg oder Fehler), ladebalken weg
// Optional: Wenn eingeloggt, leite zur Home, falls gewünscht
if(this.activeTenant > 0) {
this.loading = false
// Hier vorsichtig sein: Nicht navigieren, wenn der User auf eine Deep-Link URL will!
// navigateTo("/") <-- Das würde ich hier evtl. sogar weglassen und der Middleware überlassen
}
this.loading = false
},
async init(token=null) {
console.log("Auth init")
await this.fetchMe(token)
const tempStore = useTempStore()
if(this.profile?.temp_config) tempStore.setStoredTempConfig(this.profile.temp_config)
if(this.activeTenant > 0) {
this.loading = false
navigateTo("/")
}
},
async login(email: string, password: string) {
try {
console.log("Auth login")
const { token } = await useNuxtApp().$api("/auth/login", {
method: "POST",
body: { email, password }
})
console.log("Token: " + token)
// 1. WICHTIG: Token sofort ins Cookie schreiben, damit es persistiert wird
this.setToken(token)
this.sessionExpired = false
// 2. User Daten laden
await this.fetchMe(token)
console.log(this.user)
// 3. WICHTIG: Jetzt explizit weiterleiten!
// Prüfen, ob der User geladen wurde
if (this.user) {
// Falls Passwort-Änderung erzwungen wird (passend zu deiner Middleware)
if (this.user.must_change_password) {
return navigateTo("/password-change")
}
// Normaler Login -> Dashboard
return navigateTo("/")
}
} catch (e) {
console.log("login error:" + e)
// Hier könnte man noch eine Fehlermeldung im UI anzeigen
}
},
async logout() {
console.log("Auth logout")
try {
await useNuxtApp().$api("/auth/logout", { method: "POST" })
} catch (e) {
console.error("Logout API fehlgeschlagen (egal):", e)
}
// State resetten
this.resetState()
// Token löschen
this.setToken(null)
// Nur beim expliziten Logout navigieren wir
navigateTo("/login")
},
expireSession() {
console.log("Auth session expired")
this.resetState()
this.sessionExpired = true
this.setToken(null)
this.loading = false
if (process.client) {
navigateTo("/login")
}
},
resetState() {
this.clearSessionTimers()
this.user = null
this.permissions = []
this.profile = null
this.activeTenant = null
this.tenants = []
this.activeTenantData = null
this.sessionExpired = false
this.sessionWarningVisible = false
this.sessionWarningRemainingSeconds = 0
this.loading = false
},
async fetchMe(jwt= null) {
console.log("Auth fetchMe")
const tempStore = useTempStore()
// Token aus Argument oder Cookie holen
const tokenToUse = jwt || this.getStoredToken()
if (!tokenToUse) {
const wasSessionExpired = this.sessionExpired
this.resetState()
this.sessionExpired = wasSessionExpired
return
}
try {
const me = await useNuxtApp().$api("/api/me", {
headers: {
Authorization: `Bearer ${tokenToUse}`,
context: { jwt: tokenToUse }
}
})
// ... (Deine Logik für tenants, sorting etc. bleibt gleich) ...
console.log(me)
this.user = me.user
this.permissions = me.permissions
this.tenants = me.tenants
this.tenants.sort(function (a, b) {
if (a.id < b.id) return -1
if (a.id > b.id) return 1
})
this.profile = me.profile
if(this.profile?.temp_config) tempStore.setStoredTempConfig(this.profile.temp_config)
if(me.activeTenant > 0) {
const normalizedActiveTenant = String(me.activeTenant)
this.activeTenant = normalizedActiveTenant
this.activeTenantData = me.tenants.find(i => String(i.id) === normalizedActiveTenant) || null
try {
const tenant = await useNuxtApp().$api("/api/tenant", {
headers: {
Authorization: `Bearer ${tokenToUse}`,
context: { jwt: tokenToUse }
}
})
if (tenant?.id) {
this.activeTenantData = tenant
}
} catch (tenantError) {
console.error("fetch active tenant failed", tenantError)
}
}
this.scheduleSessionTimers(tokenToUse)
console.log(this)
} catch (err: any) {
// WICHTIG: Hier NICHT this.logout() aufrufen, weil das navigiert!
console.log("fetchMe failed (Invalid Token or Network)", err)
if (err?.response?.status === 401 || err?.status === 401 || err?.statusCode === 401) {
this.expireSession()
return
}
this.loading = false
// 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
const res = await useNuxtApp().$api("/api/tenant/switch", {
method: "POST",
body: { tenant_id }
})
console.log(res)
const {token} = res
this.setToken(token)
await this.init(token)
},
hasPermission(key: string) {
return this.permissions.includes(key)
}
}
})