diff --git a/backend/src/routes/auth/auth.ts b/backend/src/routes/auth/auth.ts index 74a324c..2537d6c 100644 --- a/backend/src/routes/auth/auth.ts +++ b/backend/src/routes/auth/auth.ts @@ -65,11 +65,12 @@ export default async function authRoutes(server: FastifyInstance) { properties: { email: { type: "string", format: "email" }, password: { type: "string" }, + rememberMe: { type: "boolean" }, }, }, }, }, async (req, reply) => { - const body = req.body as { email: string; password: string }; + const body = req.body as { email: string; password: string; rememberMe?: boolean }; let user: any = null; @@ -135,7 +136,7 @@ export default async function authRoutes(server: FastifyInstance) { httpOnly: true, sameSite: process.env.NODE_ENV === "production" ? "none" : "lax", secure: process.env.NODE_ENV === "production", - maxAge: 60 * 60 * 6, + ...(body.rememberMe ? { maxAge: 60 * 60 * 6 } : {}), }); return { token, refreshToken }; diff --git a/frontend/pages/login.vue b/frontend/pages/login.vue index 19f5e9e..eec016f 100644 --- a/frontend/pages/login.vue +++ b/frontend/pages/login.vue @@ -11,7 +11,8 @@ const router = useRouter() const state = reactive({ email: '', - password: '' + password: '', + rememberMe: true }) const loading = ref(false) @@ -19,7 +20,7 @@ const loading = ref(false) const doLogin = async (event: FormSubmitEvent) => { loading.value = true try { - await auth.login(event.data.email, event.data.password) + await auth.login(event.data.email, event.data.password, event.data.rememberMe) toast.add({title:"Einloggen erfolgreich"}) await router.push("/") } catch (err: any) { @@ -79,6 +80,12 @@ const doLogin = async (event: FormSubmitEvent) => { /> + + Weiter diff --git a/frontend/stores/auth.ts b/frontend/stores/auth.ts index d942b7f..538c096 100644 --- a/frontend/stores/auth.ts +++ b/frontend/stores/auth.ts @@ -27,6 +27,7 @@ export const useAuthStore = defineStore("auth", { sessionWarningTimer: null as ReturnType | null, sessionLogoutTimer: null as ReturnType | null, sessionCountdownTimer: null as ReturnType | null, + sessionPersistent: false, }), actions: { @@ -36,18 +37,18 @@ export const useAuthStore = defineStore("auth", { getStoredToken() { const rootToken = this.tokenCookie().value - if (rootToken || !process.client) return rootToken + if (!process.client) return rootToken - const tokenCookie = document.cookie - .split(";") - .map((part) => part.trim()) - .find((part) => part.startsWith("token=")) + return sessionStorage.getItem("token") + || localStorage.getItem("token") + || rootToken + }, - if (tokenCookie) { - return decodeURIComponent(tokenCookie.slice("token=".length)) - } + getStoredRefreshToken() { + if (!process.client) return null - return localStorage.getItem("token") + return sessionStorage.getItem("refreshToken") + || localStorage.getItem("refreshToken") }, clearScopedTokenCookies() { @@ -155,24 +156,34 @@ export const useAuthStore = defineStore("auth", { const msUntilWarning = msUntilLogout - this.sessionWarningLeadMs if (msUntilWarning <= 0) { + if (this.getStoredRefreshToken()) { + void this.refreshSession() + return + } this.openSessionWarning(expiresAtMs) return } this.sessionWarningTimer = setTimeout(() => { + if (this.getStoredRefreshToken()) { + void this.refreshSession() + return + } this.openSessionWarning(expiresAtMs) }, msUntilWarning) }, - setToken(token: string | null) { + setToken(token: string | null, persistent = this.sessionPersistent) { this.clearScopedTokenCookies() this.tokenCookie().value = token if (process.client) { + localStorage.removeItem("token") + sessionStorage.removeItem("token") + if (token) { - localStorage.setItem("token", token) - } else { - localStorage.removeItem("token") + const storage = persistent ? localStorage : sessionStorage + storage.setItem("token", token) } } @@ -186,6 +197,30 @@ export const useAuthStore = defineStore("auth", { this.scheduleSessionTimers(token) }, + setRefreshToken(refreshToken: string | null, persistent = this.sessionPersistent) { + if (!process.client) return + + localStorage.removeItem("refreshToken") + sessionStorage.removeItem("refreshToken") + + if (refreshToken) { + const storage = persistent ? localStorage : sessionStorage + storage.setItem("refreshToken", refreshToken) + } + }, + + setSession(token: string, refreshToken: string, persistent: boolean) { + this.sessionPersistent = persistent + this.setRefreshToken(refreshToken, persistent) + this.setToken(token, persistent) + }, + + clearStoredSession() { + this.setRefreshToken(null) + this.setToken(null) + this.sessionPersistent = false + }, + async persist(token: string | null) { console.log("On Web") @@ -195,6 +230,7 @@ export const useAuthStore = defineStore("auth", { async initStore() { console.log("Auth initStore") + this.sessionPersistent = process.client && Boolean(localStorage.getItem("refreshToken")) // 1. Check: Haben wir überhaupt ein Token? const token = this.getStoredToken() @@ -207,7 +243,7 @@ export const useAuthStore = defineStore("auth", { } // 2. Token existiert -> Versuche User zu laden - await this.fetchMe(token) + await this.fetchMe(token, true) // Wenn fetchMe fertig ist (egal ob Erfolg oder Fehler), ladebalken weg @@ -236,18 +272,18 @@ export const useAuthStore = defineStore("auth", { } }, - async login(email: string, password: string) { + async login(email: string, password: string, rememberMe = false) { try { console.log("Auth login") - const { token } = await useNuxtApp().$api("/auth/login", { + const { token, refreshToken } = await useNuxtApp().$api("/auth/login", { method: "POST", - body: { email, password } + body: { email, password, rememberMe } }) console.log("Token: " + token) - // 1. WICHTIG: Token sofort ins Cookie schreiben, damit es persistiert wird - this.setToken(token) + // Tokens abhängig von „Angemeldet bleiben“ dauerhaft oder nur für diese Sitzung speichern + this.setSession(token, refreshToken, rememberMe) this.sessionExpired = false // 2. User Daten laden @@ -269,14 +305,18 @@ export const useAuthStore = defineStore("auth", { } catch (e) { console.log("login error:" + e) - // Hier könnte man noch eine Fehlermeldung im UI anzeigen + throw e } }, async logout() { console.log("Auth logout") + const refreshToken = this.getStoredRefreshToken() try { - await useNuxtApp().$api("/auth/logout", { method: "POST" }) + await useNuxtApp().$api("/auth/logout", { + method: "POST", + body: { refreshToken } + }) } catch (e) { console.error("Logout API fehlgeschlagen (egal):", e) } @@ -285,7 +325,7 @@ export const useAuthStore = defineStore("auth", { this.resetState() // Token löschen - this.setToken(null) + this.clearStoredSession() // Nur beim expliziten Logout navigieren wir navigateTo("/login") @@ -295,7 +335,7 @@ export const useAuthStore = defineStore("auth", { console.log("Auth session expired") this.resetState() this.sessionExpired = true - this.setToken(null) + this.clearStoredSession() this.loading = false if (process.client) { @@ -317,7 +357,7 @@ export const useAuthStore = defineStore("auth", { this.loading = false }, - async fetchMe(jwt= null) { + async fetchMe(jwt= null, allowRefresh = true) { console.log("Auth fetchMe") const tempStore = useTempStore() @@ -384,6 +424,10 @@ export const useAuthStore = defineStore("auth", { console.log("fetchMe failed (Invalid Token or Network)", err) if (err?.response?.status === 401 || err?.status === 401 || err?.statusCode === 401) { + if (allowRefresh && this.getStoredRefreshToken()) { + await this.refreshSession() + return + } this.expireSession() return } @@ -395,17 +439,31 @@ export const useAuthStore = defineStore("auth", { }, async refreshSession() { + const refreshToken = this.getStoredRefreshToken() + if (!refreshToken) { + this.expireSession() + return + } + try { - const { token } = await useNuxtApp().$api("/api/auth/refresh", { + const session = await useNuxtApp().$api("/auth/refresh", { method: "POST", + body: { refreshToken } }) - this.setToken(token) - await this.fetchMe(token) + this.setSession(session.token, session.refreshToken, this.sessionPersistent) + await this.fetchMe(session.token, false) this.sessionWarningVisible = false } catch (err) { console.error("JWT refresh failed", err) - await this.logout() + const status = err?.response?.status || err?.status || err?.statusCode + if (status === 401) { + this.expireSession() + } else { + const token = this.getStoredToken() + const expiresAtMs = token ? this.decodeTokenExpiryMs(token) : null + if (expiresAtMs) this.openSessionWarning(expiresAtMs) + } } }, @@ -414,14 +472,15 @@ export const useAuthStore = defineStore("auth", { this.loading = true const res = await useNuxtApp().$api("/api/tenant/switch", { method: "POST", - body: { tenant_id } + body: { + tenant_id, + refreshToken: this.getStoredRefreshToken() + } }) console.log(res) - const {token} = res - - - this.setToken(token) + const {token, refreshToken} = res + this.setSession(token, refreshToken, this.sessionPersistent) await this.init(token)