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'
|
||||
|
||||
// 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> {
|
||||
const token = localStorage.getItem('access_token')
|
||||
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 })
|
||||
|
||||
// 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) {
|
||||
const err = await res.json().catch(() => ({ detail: res.statusText }))
|
||||
const msg = typeof err.detail === 'string'
|
||||
|
||||
Reference in New Issue
Block a user