ifixkart-admin/services/api/client.ts

202 lines
5.6 KiB
TypeScript

import { API_BASE_URL } from './config';
// Token management
let accessToken: string | null = null;
export function setAccessToken(token: string | null): void {
accessToken = token;
if (token) {
localStorage.setItem('access_token', token);
} else {
localStorage.removeItem('access_token');
}
}
export function getAccessToken(): string | null {
if (accessToken) return accessToken;
if (typeof window !== 'undefined') {
accessToken = localStorage.getItem('access_token');
}
return accessToken;
}
export function clearTokens(): void {
accessToken = null;
if (typeof window !== 'undefined') {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
}
}
// Parse JWT payload without library dependency
export function parseJwt(token: string): Record<string, unknown> | null {
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
);
return JSON.parse(jsonPayload);
} catch {
return null;
}
}
interface FetchOptions extends RequestInit {
skipAuth?: boolean;
}
export function invalidateApiCache() {
// No-op: Caching disabled for real-time admin management
}
// Core API fetch wrapper with auth header injection
export async function apiFetch<T>(
endpoint: string,
options: FetchOptions = {}
): Promise<T> {
const { skipAuth = false, headers: customHeaders, ...rest } = options;
const method = (rest.method || 'GET').toUpperCase();
return runApiFetch<T>(endpoint, { skipAuth, headers: customHeaders, ...rest }, method);
}
async function runApiFetch<T>(
endpoint: string,
options: FetchOptions,
method: string
): Promise<T> {
const { skipAuth = false, headers: customHeaders, ...rest } = options;
const headers: Record<string, string> = {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
...(customHeaders as Record<string, string>),
};
if (method !== 'GET' && method !== 'HEAD' && !headers['Content-Type']) {
if (!(rest.body instanceof FormData)) {
headers['Content-Type'] = 'application/json';
}
}
if (!skipAuth) {
const token = getAccessToken();
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
} else {
delete headers['Authorization'];
}
try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
cache: 'no-store',
...rest,
headers,
credentials: 'include',
});
if (response.status === 401 && !skipAuth) {
// Try token refresh
const refreshed = await refreshAccessToken();
if (refreshed) {
headers['Authorization'] = `Bearer ${getAccessToken()}`;
const retryResponse = await fetch(`${API_BASE_URL}${endpoint}`, {
...rest,
headers,
credentials: 'include',
});
if (!retryResponse.ok) {
const err = await retryResponse.json().catch(() => ({ detail: 'Request failed' }));
throw new ApiError(retryResponse.status, formatErrorMessage(err.detail));
}
return retryResponse.json();
}
// Refresh failed — redirect to login
clearTokens();
if (typeof window !== 'undefined') {
window.location.href = '/login';
}
throw new ApiError(401, 'Session expired');
}
if (!response.ok) {
const err = await response.json().catch(() => ({ detail: 'Request failed' }));
throw new ApiError(response.status, formatErrorMessage(err.detail));
}
return response.json();
} catch (error: any) {
if (error instanceof ApiError) {
throw error;
}
console.error('apiFetch network/CORS error:', error);
throw new ApiError(
503,
'Network issue — please refresh page or check connection'
);
}
}
async function refreshAccessToken(): Promise<boolean> {
try {
const refreshToken = typeof window !== 'undefined' ? localStorage.getItem('refresh_token') : null;
const headers: Record<string, string> = {
'Content-Type': 'application/json'
};
if (refreshToken) {
headers['Authorization'] = `Bearer ${refreshToken}`;
headers['X-Refresh-Token'] = refreshToken;
}
const response = await fetch(`${API_BASE_URL}/api/v1/auth/refresh`, {
method: 'POST',
headers,
credentials: 'include',
});
if (!response.ok) return false;
const data = await response.json();
if (data.access_token) {
setAccessToken(data.access_token);
}
if (data.refresh_token) {
localStorage.setItem('refresh_token', data.refresh_token);
}
return true;
} catch {
return false;
}
}
export function formatErrorMessage(detail: any): string {
if (!detail) return 'Request failed';
if (typeof detail === 'string') return detail;
if (Array.isArray(detail)) {
return detail
.map((item: any) => {
if (typeof item === 'string') return item;
if (item && typeof item === 'object') {
const loc = Array.isArray(item.loc) ? item.loc.slice(1).join('.') : '';
const msg = item.msg || item.message || JSON.stringify(item);
return loc ? `${loc}: ${msg}` : msg;
}
return String(item);
})
.join(' | ');
}
if (typeof detail === 'object') {
return detail.message || detail.msg || JSON.stringify(detail);
}
return String(detail);
}
export class ApiError extends Error {
status: number;
constructor(status: number, message: string) {
super(message);
this.status = status;
this.name = 'ApiError';
}
}