Replace Axios with native fetch; add global 401 session-expiry redirect

All API calls now go through a thin request() wrapper around native fetch.
Removes the axios dependency entirely. The wrapper injects the JWT on every
request and — the key fix — clears localStorage and redirects to /login on
any 401 response, so expired sessions no longer leave users on broken pages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-04-18 21:04:18 +02:00
parent c5976882be
commit 479108779f
7 changed files with 578 additions and 119 deletions
+21 -9
View File
@@ -22,7 +22,7 @@ frontend/
├── src/
│ ├── main.tsx ← React root, QueryClientProvider, BrowserRouter
│ ├── App.tsx ← Route tree, PrivateRoute, AdminRoute
│ ├── api/client.ts ← Axios instance + ALL API functions (single source of truth)
│ ├── api/client.ts ← Native fetch wrapper (`request()`) + ALL API functions (single source of truth); no Axios
│ ├── hooks/
│ │ ├── useAuth.ts ← Token state (localStorage), login/logout
│ │ └── useTheme.ts ← Theme toggle
@@ -86,21 +86,33 @@ frontend/
### API client (`src/api/client.ts`)
Single Axios instance — **all** API calls live here, nowhere else:
**No Axios** — uses a thin native `fetch` wrapper. All API calls live here, nowhere else.
The core `request()` function handles:
- Prepends `/api` base URL
- Injects `Authorization: Bearer {token}` from `localStorage` on every request
- **Global 401 handler**: clears `localStorage` token and redirects to `/login` via `window.location.href` — this is the expired-session redirect
- Throws `ApiError(status, detail)` on non-2xx responses (detail parsed from JSON body)
- Returns `undefined` on 204 No Content
- Supports `blob: true` for file download/preview responses
```typescript
const api = axios.create({ baseURL: "/api" });
api.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// The internal api object — use these methods in exported functions:
api.get<T>(path, params?) // GET with optional query params object
api.post<T>(path, json?) // POST with JSON body
api.postForm<T>(path, URLSearchParams) // POST with form-encoded body (login)
api.postFile<T>(path, FormData) // POST with multipart body (file upload)
api.patch<T>(path, json?) // PATCH with JSON body
api.delete<T>(path) // DELETE
api.getBlob(path) // GET → Blob (download / view)
```
Adding a new API call:
1. Define a TypeScript interface for the response if it's new.
2. Add a named export function (`getX`, `createX`, `updateX`, `deleteX`).
3. Use `api.get<T>(...)`, `api.post<T>(...)`, etc.; always `.then((r) => r.data)`.
3. Use the appropriate `api.*` method — return the promise directly (no `.then((r) => r.data)`).
Error handling in components: catch blocks receive an `ApiError` instance with `.status` and `.message` (the detail string).
### TanStack Query conventions