(endpoint: string, options?: RequestInit)
| 70 | * @returns Promise with the response data |
| 71 | */ |
| 72 | export async function fetchApi<T>(endpoint: string, options?: RequestInit): Promise<T> { |
| 73 | const url = getApiUrl(endpoint) |
| 74 | |
| 75 | const token = getAuthToken() |
| 76 | |
| 77 | const headers: Record<string, string> = { |
| 78 | "Content-Type": "application/json", |
| 79 | ...(options?.headers as Record<string, string>), |
| 80 | } |
| 81 | |
| 82 | if (token) { |
| 83 | headers["Authorization"] = `Bearer ${token}` |
| 84 | } |
| 85 | |
| 86 | const response = await fetch(url, { |
| 87 | ...options, |
| 88 | headers, |
| 89 | cache: "no-store", |
| 90 | }) |
| 91 | |
| 92 | if (!response.ok) { |
| 93 | if (response.status === 401) { |
| 94 | // Token is missing, expired, or signed under a previous JWT_SECRET |
| 95 | // (rotated per-install). Drop the stale token and force a single |
| 96 | // reload so the page-level auth gate (`app/page.tsx`) can render |
| 97 | // <Login> instead of cascading 401s from every authenticated |
| 98 | // component on mount. |
| 99 | // |
| 100 | // Only react when we actually had a token to invalidate. A 401 |
| 101 | // without any token in localStorage means the caller is the |
| 102 | // Login screen itself, or a leftover fetch from a recently |
| 103 | // unmounted Dashboard — reloading there does nothing but waste |
| 104 | // the user's keystrokes and can leave the cascade flag set |
| 105 | // forever, swallowing the very 401 that we'd want to recover |
| 106 | // from after a successful re-login. The fix: bail out early |
| 107 | // if we have no token to invalidate. |
| 108 | if (typeof window !== "undefined") { |
| 109 | let hadToken = false |
| 110 | try { |
| 111 | hadToken = !!localStorage.getItem("proxmenux-auth-token") |
| 112 | } catch { |
| 113 | // private browsing — assume yes so we attempt recovery. |
| 114 | hadToken = true |
| 115 | } |
| 116 | if (!hadToken) { |
| 117 | throw new Error(`Unauthorized: ${endpoint}`) |
| 118 | } |
| 119 | try { |
| 120 | localStorage.removeItem("proxmenux-auth-token") |
| 121 | } catch { |
| 122 | // localStorage might be unavailable in private browsing — ignore. |
| 123 | } |
| 124 | try { |
| 125 | if (!sessionStorage.getItem("proxmenux-auth-401-handled")) { |
| 126 | sessionStorage.setItem("proxmenux-auth-401-handled", "1") |
| 127 | window.location.reload() |
| 128 | } |
| 129 | } catch { |
no test coverage detected