diff --git a/paperless-frontend/src/api/client.ts b/paperless-frontend/src/api/client.ts index 6623c72..75eb460 100644 --- a/paperless-frontend/src/api/client.ts +++ b/paperless-frontend/src/api/client.ts @@ -1,8 +1,12 @@ -import axios from 'axios'; -import { getAccessToken } from '../auth/oidc'; +import axios, { type InternalAxiosRequestConfig } from 'axios'; +import { getAccessToken, renewToken } from '../auth/oidc'; import { triggerLoginRedirect } from '../auth/sessionRedirect'; import { getEnv } from '../utils/env'; +// Markiert eine bereits einmal wiederholte Anfrage, um Endlosschleifen zu +// vermeiden. +type RetryConfig = InternalAxiosRequestConfig & { _retried?: boolean }; + const api = axios.create({ baseURL: getEnv('VITE_API_URL') || '', timeout: 30000, @@ -19,7 +23,18 @@ api.interceptors.request.use(async (config) => { api.interceptors.response.use( (response) => response, async (error) => { - if (error.response?.status === 401) { + const original = error.config as RetryConfig | undefined; + if (error.response?.status === 401 && original && !original._retried) { + original._retried = true; + // Erst still erneuern und die Anfrage einmal wiederholen, bevor wir den + // Nutzer zu einer kompletten Neuanmeldung zwingen. + const user = await renewToken(); + if (user?.access_token) { + original.headers.Authorization = `Bearer ${user.access_token}`; + return api(original); + } + // Erneuerung fehlgeschlagen (Refresh-Token ungültig/abgelaufen) → + // echte Neuanmeldung nötig. await triggerLoginRedirect(); } return Promise.reject(error); diff --git a/paperless-frontend/src/auth/oidc.ts b/paperless-frontend/src/auth/oidc.ts index ad55866..a5de680 100644 --- a/paperless-frontend/src/auth/oidc.ts +++ b/paperless-frontend/src/auth/oidc.ts @@ -34,9 +34,36 @@ export async function getUser(): Promise { return userManager.getUser(); } +// Laufender Silent-Renew-Vorgang – mehrere gleichzeitige Aufrufe teilen sich +// denselben Refresh (Dedup), damit nicht parallele signinSilent-Aufrufe mit +// einem rotierenden Refresh-Token kollidieren (invalid_grant). +let renewPromise: Promise | null = null; + +/** + * Erneuert das Token still über den Refresh-Token (signinSilent). Schlägt die + * Erneuerung fehl, wird null geliefert (statt zu werfen). Gleichzeitige Aufrufe + * werden dedupliziert. + */ +export function renewToken(): Promise { + if (!renewPromise) { + renewPromise = userManager.signinSilent().catch((err: unknown) => { + console.error('OIDC: signinSilent fehlgeschlagen', err); + return null; + }); + void renewPromise.finally(() => { + renewPromise = null; + }); + } + return renewPromise; +} + export async function getAccessToken(): Promise { const user = await getUser(); - return user?.access_token ?? null; + if (!user) return null; // nicht eingeloggt + if (!user.expired) return user.access_token ?? null; + // Token ist abgelaufen → vor dem Senden still erneuern. + const renewed = await renewToken(); + return renewed?.access_token ?? null; } export { userManager };