KI-AGENT: Refresh-Token für Web-Sitzungen ergänzen
This commit is contained in:
@@ -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 };
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user