KI-AGENT: Refresh-Token für Web-Sitzungen ergänzen

This commit is contained in:
2026-07-20 22:12:09 +02:00
parent 657a4f9e85
commit 5585bd7012
3 changed files with 104 additions and 37 deletions

View File

@@ -65,11 +65,12 @@ export default async function authRoutes(server: FastifyInstance) {
properties: { properties: {
email: { type: "string", format: "email" }, email: { type: "string", format: "email" },
password: { type: "string" }, password: { type: "string" },
rememberMe: { type: "boolean" },
}, },
}, },
}, },
}, async (req, reply) => { }, 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; let user: any = null;
@@ -135,7 +136,7 @@ export default async function authRoutes(server: FastifyInstance) {
httpOnly: true, httpOnly: true,
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax", sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
secure: process.env.NODE_ENV === "production", secure: process.env.NODE_ENV === "production",
maxAge: 60 * 60 * 6, ...(body.rememberMe ? { maxAge: 60 * 60 * 6 } : {}),
}); });
return { token, refreshToken }; return { token, refreshToken };

View File

@@ -11,7 +11,8 @@ const router = useRouter()
const state = reactive({ const state = reactive({
email: '', email: '',
password: '' password: '',
rememberMe: true
}) })
const loading = ref(false) const loading = ref(false)
@@ -19,7 +20,7 @@ const loading = ref(false)
const doLogin = async (event: FormSubmitEvent<typeof state>) => { const doLogin = async (event: FormSubmitEvent<typeof state>) => {
loading.value = true loading.value = true
try { 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"}) toast.add({title:"Einloggen erfolgreich"})
await router.push("/") await router.push("/")
} catch (err: any) { } catch (err: any) {
@@ -79,6 +80,12 @@ const doLogin = async (event: FormSubmitEvent<typeof state>) => {
/> />
</UFormField> </UFormField>
<UCheckbox
v-model="state.rememberMe"
name="rememberMe"
label="Angemeldet bleiben"
/>
<UButton type="submit" block class="w-full" :loading="loading"> <UButton type="submit" block class="w-full" :loading="loading">
Weiter Weiter
</UButton> </UButton>

View File

@@ -27,6 +27,7 @@ export const useAuthStore = defineStore("auth", {
sessionWarningTimer: null as ReturnType<typeof setTimeout> | null, sessionWarningTimer: null as ReturnType<typeof setTimeout> | null,
sessionLogoutTimer: null as ReturnType<typeof setTimeout> | null, sessionLogoutTimer: null as ReturnType<typeof setTimeout> | null,
sessionCountdownTimer: null as ReturnType<typeof setInterval> | null, sessionCountdownTimer: null as ReturnType<typeof setInterval> | null,
sessionPersistent: false,
}), }),
actions: { actions: {
@@ -36,18 +37,18 @@ export const useAuthStore = defineStore("auth", {
getStoredToken() { getStoredToken() {
const rootToken = this.tokenCookie().value const rootToken = this.tokenCookie().value
if (rootToken || !process.client) return rootToken if (!process.client) return rootToken
const tokenCookie = document.cookie return sessionStorage.getItem("token")
.split(";") || localStorage.getItem("token")
.map((part) => part.trim()) || rootToken
.find((part) => part.startsWith("token=")) },
if (tokenCookie) { getStoredRefreshToken() {
return decodeURIComponent(tokenCookie.slice("token=".length)) if (!process.client) return null
}
return localStorage.getItem("token") return sessionStorage.getItem("refreshToken")
|| localStorage.getItem("refreshToken")
}, },
clearScopedTokenCookies() { clearScopedTokenCookies() {
@@ -155,24 +156,34 @@ export const useAuthStore = defineStore("auth", {
const msUntilWarning = msUntilLogout - this.sessionWarningLeadMs const msUntilWarning = msUntilLogout - this.sessionWarningLeadMs
if (msUntilWarning <= 0) { if (msUntilWarning <= 0) {
if (this.getStoredRefreshToken()) {
void this.refreshSession()
return
}
this.openSessionWarning(expiresAtMs) this.openSessionWarning(expiresAtMs)
return return
} }
this.sessionWarningTimer = setTimeout(() => { this.sessionWarningTimer = setTimeout(() => {
if (this.getStoredRefreshToken()) {
void this.refreshSession()
return
}
this.openSessionWarning(expiresAtMs) this.openSessionWarning(expiresAtMs)
}, msUntilWarning) }, msUntilWarning)
}, },
setToken(token: string | null) { setToken(token: string | null, persistent = this.sessionPersistent) {
this.clearScopedTokenCookies() this.clearScopedTokenCookies()
this.tokenCookie().value = token this.tokenCookie().value = token
if (process.client) { if (process.client) {
localStorage.removeItem("token")
sessionStorage.removeItem("token")
if (token) { if (token) {
localStorage.setItem("token", token) const storage = persistent ? localStorage : sessionStorage
} else { storage.setItem("token", token)
localStorage.removeItem("token")
} }
} }
@@ -186,6 +197,30 @@ export const useAuthStore = defineStore("auth", {
this.scheduleSessionTimers(token) 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) { async persist(token: string | null) {
console.log("On Web") console.log("On Web")
@@ -195,6 +230,7 @@ export const useAuthStore = defineStore("auth", {
async initStore() { async initStore() {
console.log("Auth initStore") console.log("Auth initStore")
this.sessionPersistent = process.client && Boolean(localStorage.getItem("refreshToken"))
// 1. Check: Haben wir überhaupt ein Token? // 1. Check: Haben wir überhaupt ein Token?
const token = this.getStoredToken() const token = this.getStoredToken()
@@ -207,7 +243,7 @@ export const useAuthStore = defineStore("auth", {
} }
// 2. Token existiert -> Versuche User zu laden // 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 // 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 { try {
console.log("Auth login") console.log("Auth login")
const { token } = await useNuxtApp().$api("/auth/login", { const { token, refreshToken } = await useNuxtApp().$api("/auth/login", {
method: "POST", method: "POST",
body: { email, password } body: { email, password, rememberMe }
}) })
console.log("Token: " + token) console.log("Token: " + token)
// 1. WICHTIG: Token sofort ins Cookie schreiben, damit es persistiert wird // Tokens abhängig von „Angemeldet bleiben dauerhaft oder nur für diese Sitzung speichern
this.setToken(token) this.setSession(token, refreshToken, rememberMe)
this.sessionExpired = false this.sessionExpired = false
// 2. User Daten laden // 2. User Daten laden
@@ -269,14 +305,18 @@ export const useAuthStore = defineStore("auth", {
} catch (e) { } catch (e) {
console.log("login error:" + e) console.log("login error:" + e)
// Hier könnte man noch eine Fehlermeldung im UI anzeigen throw e
} }
}, },
async logout() { async logout() {
console.log("Auth logout") console.log("Auth logout")
const refreshToken = this.getStoredRefreshToken()
try { try {
await useNuxtApp().$api("/auth/logout", { method: "POST" }) await useNuxtApp().$api("/auth/logout", {
method: "POST",
body: { refreshToken }
})
} catch (e) { } catch (e) {
console.error("Logout API fehlgeschlagen (egal):", e) console.error("Logout API fehlgeschlagen (egal):", e)
} }
@@ -285,7 +325,7 @@ export const useAuthStore = defineStore("auth", {
this.resetState() this.resetState()
// Token löschen // Token löschen
this.setToken(null) this.clearStoredSession()
// Nur beim expliziten Logout navigieren wir // Nur beim expliziten Logout navigieren wir
navigateTo("/login") navigateTo("/login")
@@ -295,7 +335,7 @@ export const useAuthStore = defineStore("auth", {
console.log("Auth session expired") console.log("Auth session expired")
this.resetState() this.resetState()
this.sessionExpired = true this.sessionExpired = true
this.setToken(null) this.clearStoredSession()
this.loading = false this.loading = false
if (process.client) { if (process.client) {
@@ -317,7 +357,7 @@ export const useAuthStore = defineStore("auth", {
this.loading = false this.loading = false
}, },
async fetchMe(jwt= null) { async fetchMe(jwt= null, allowRefresh = true) {
console.log("Auth fetchMe") console.log("Auth fetchMe")
const tempStore = useTempStore() const tempStore = useTempStore()
@@ -384,6 +424,10 @@ export const useAuthStore = defineStore("auth", {
console.log("fetchMe failed (Invalid Token or Network)", err) console.log("fetchMe failed (Invalid Token or Network)", err)
if (err?.response?.status === 401 || err?.status === 401 || err?.statusCode === 401) { if (err?.response?.status === 401 || err?.status === 401 || err?.statusCode === 401) {
if (allowRefresh && this.getStoredRefreshToken()) {
await this.refreshSession()
return
}
this.expireSession() this.expireSession()
return return
} }
@@ -395,17 +439,31 @@ export const useAuthStore = defineStore("auth", {
}, },
async refreshSession() { async refreshSession() {
const refreshToken = this.getStoredRefreshToken()
if (!refreshToken) {
this.expireSession()
return
}
try { try {
const { token } = await useNuxtApp().$api("/api/auth/refresh", { const session = await useNuxtApp().$api("/auth/refresh", {
method: "POST", method: "POST",
body: { refreshToken }
}) })
this.setToken(token) this.setSession(session.token, session.refreshToken, this.sessionPersistent)
await this.fetchMe(token) await this.fetchMe(session.token, false)
this.sessionWarningVisible = false this.sessionWarningVisible = false
} catch (err) { } catch (err) {
console.error("JWT refresh failed", 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 this.loading = true
const res = await useNuxtApp().$api("/api/tenant/switch", { const res = await useNuxtApp().$api("/api/tenant/switch", {
method: "POST", method: "POST",
body: { tenant_id } body: {
tenant_id,
refreshToken: this.getStoredRefreshToken()
}
}) })
console.log(res) console.log(res)
const {token} = res const {token, refreshToken} = res
this.setSession(token, refreshToken, this.sessionPersistent)
this.setToken(token)
await this.init(token) await this.init(token)