Updated frontend
This commit is contained in:
@@ -1,5 +1,49 @@
|
||||
const BASE = import.meta.env.VITE_API_BASE_URL || '';
|
||||
|
||||
// Where the access token lives. sessionStorage, not localStorage: the token is
|
||||
// a bearer credential, and a tab-scoped store means closing the tab ends the
|
||||
// session rather than leaving a working credential on disk.
|
||||
export const TOKEN_STORAGE_KEY = 'app_access_token';
|
||||
|
||||
// Read at module load so a page refresh is already authenticated before
|
||||
// AuthContext mounts and the first request goes out.
|
||||
let authToken = (() => {
|
||||
try {
|
||||
return sessionStorage.getItem(TOKEN_STORAGE_KEY);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
let onUnauthorized = null;
|
||||
|
||||
/** Called by AuthContext on login/logout. Pass null to clear. */
|
||||
export function setAuthToken(token) {
|
||||
authToken = token || null;
|
||||
try {
|
||||
if (token) sessionStorage.setItem(TOKEN_STORAGE_KEY, token);
|
||||
else sessionStorage.removeItem(TOKEN_STORAGE_KEY);
|
||||
} catch {
|
||||
/* private browsing with storage disabled - the in-memory copy still works */
|
||||
}
|
||||
}
|
||||
|
||||
export function getAuthToken() {
|
||||
return authToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registered by AuthContext so an expired token anywhere in the app drops the
|
||||
* session once, rather than leaving every panel to render its own 401 error.
|
||||
*/
|
||||
export function setUnauthorizedHandler(fn) {
|
||||
onUnauthorized = fn;
|
||||
}
|
||||
|
||||
function authHeaders() {
|
||||
return authToken ? { Authorization: `Bearer ${authToken}` } : {};
|
||||
}
|
||||
|
||||
class ApiError extends Error {
|
||||
constructor(message, status) {
|
||||
super(message);
|
||||
@@ -11,7 +55,11 @@ async function request(path, options = {}) {
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(`${BASE}${path}`, {
|
||||
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authHeaders(),
|
||||
...(options.headers || {}),
|
||||
},
|
||||
...options,
|
||||
});
|
||||
} catch {
|
||||
@@ -30,6 +78,9 @@ async function request(path, options = {}) {
|
||||
} catch {
|
||||
/* ignore parse errors, keep generic message */
|
||||
}
|
||||
// 401 means the token is missing, expired or invalid - the session is over.
|
||||
// 403 is a live session lacking a permission, so it must NOT log you out.
|
||||
if (res.status === 401 && onUnauthorized) onUnauthorized(detail);
|
||||
throw new ApiError(detail, res.status);
|
||||
}
|
||||
|
||||
@@ -58,6 +109,15 @@ const SHOW_ALL_PRODUCTS_LIMIT = 100000;
|
||||
export const api = {
|
||||
getHealth: () => request('/api/health'),
|
||||
|
||||
// --- Auth ---
|
||||
login: (username, password) =>
|
||||
request('/api/auth/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ username, password }),
|
||||
}),
|
||||
getMe: () => request('/api/auth/me'),
|
||||
getRoles: () => request('/api/auth/roles'),
|
||||
|
||||
getBrands: () => request('/api/brands'),
|
||||
|
||||
getBrandCategories: (brand) => request(`/api/brands/${encodeURIComponent(brand)}/categories`),
|
||||
@@ -159,11 +219,15 @@ export const api = {
|
||||
getNutritionEnrichmentStatus: () => request('/api/admin/nutrition-intelligence/status'),
|
||||
|
||||
// --- Excel / CSV Upload API ---
|
||||
// Raw fetch rather than request(): the browser must set its own multipart
|
||||
// Content-Type with the boundary, so the JSON default in request() would
|
||||
// corrupt the body. The auth header still has to be added by hand here.
|
||||
uploadFile: async (tabType, file) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const res = await fetch(`${BASE}/api/upload/${encodeURIComponent(tabType)}`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: formData,
|
||||
});
|
||||
if (!res.ok) {
|
||||
@@ -172,6 +236,7 @@ export const api = {
|
||||
const body = await res.json();
|
||||
detail = body.detail || JSON.stringify(body);
|
||||
} catch {}
|
||||
if (res.status === 401 && onUnauthorized) onUnauthorized(detail);
|
||||
throw new ApiError(detail, res.status);
|
||||
}
|
||||
return res.json();
|
||||
|
||||
Reference in New Issue
Block a user