fix: auto-refresh access token on 401 in API client
- Neuer refreshAccessToken()-Helper: POST /auth/refresh → neuer access_token - Bei 401-Response: Token refreshen, Request automatisch wiederholen - Parallele Requests: nur ein Refresh gleichzeitig (_refreshing-Promise) - Refresh fehlgeschlagen → localStorage löschen + Redirect zu /login - Gilt für alle API-Aufrufe (Desktop + Mobile) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,37 @@
|
|||||||
const BASE_URL = '/api/v1'
|
const BASE_URL = '/api/v1'
|
||||||
|
|
||||||
|
// Läuft ein Refresh bereits? Damit parallele Requests nicht mehrfach refreshen
|
||||||
|
let _refreshing: Promise<string | null> | null = null
|
||||||
|
|
||||||
|
async function refreshAccessToken(): Promise<string | null> {
|
||||||
|
const refreshToken = localStorage.getItem('refresh_token')
|
||||||
|
if (!refreshToken) return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/auth/refresh`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ refresh_token: refreshToken }),
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
// Refresh fehlgeschlagen → ausloggen
|
||||||
|
localStorage.removeItem('access_token')
|
||||||
|
localStorage.removeItem('refresh_token')
|
||||||
|
window.location.href = '/login'
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const data = await res.json()
|
||||||
|
localStorage.setItem('access_token', data.access_token)
|
||||||
|
if (data.refresh_token) localStorage.setItem('refresh_token', data.refresh_token)
|
||||||
|
return data.access_token
|
||||||
|
} catch {
|
||||||
|
localStorage.removeItem('access_token')
|
||||||
|
localStorage.removeItem('refresh_token')
|
||||||
|
window.location.href = '/login'
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||||
const token = localStorage.getItem('access_token')
|
const token = localStorage.getItem('access_token')
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
@@ -10,6 +42,30 @@ async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|||||||
|
|
||||||
const res = await fetch(`${BASE_URL}${path}`, { ...options, headers })
|
const res = await fetch(`${BASE_URL}${path}`, { ...options, headers })
|
||||||
|
|
||||||
|
// 401 → Token-Refresh versuchen, dann Request wiederholen
|
||||||
|
if (res.status === 401 && !path.startsWith('/auth/')) {
|
||||||
|
if (!_refreshing) {
|
||||||
|
_refreshing = refreshAccessToken().finally(() => { _refreshing = null })
|
||||||
|
}
|
||||||
|
const newToken = await _refreshing
|
||||||
|
if (!newToken) return undefined as T // Redirect zu /login läuft bereits
|
||||||
|
|
||||||
|
// Request mit neuem Token wiederholen
|
||||||
|
const retryHeaders = { ...headers, Authorization: `Bearer ${newToken}` }
|
||||||
|
const retry = await fetch(`${BASE_URL}${path}`, { ...options, headers: retryHeaders })
|
||||||
|
if (!retry.ok) {
|
||||||
|
const err = await retry.json().catch(() => ({ detail: retry.statusText }))
|
||||||
|
const msg = typeof err.detail === 'string'
|
||||||
|
? err.detail
|
||||||
|
: Array.isArray(err.detail)
|
||||||
|
? err.detail.map((e: { msg: string }) => e.msg).join(', ')
|
||||||
|
: retry.statusText
|
||||||
|
throw new Error(msg)
|
||||||
|
}
|
||||||
|
if (retry.status === 204) return undefined as T
|
||||||
|
return retry.json()
|
||||||
|
}
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = await res.json().catch(() => ({ detail: res.statusText }))
|
const err = await res.json().catch(() => ({ detail: res.statusText }))
|
||||||
const msg = typeof err.detail === 'string'
|
const msg = typeof err.detail === 'string'
|
||||||
|
|||||||
Reference in New Issue
Block a user