ifixkart-storefront/services/api/client.ts

184 lines
5.1 KiB
TypeScript

export const getBaseUrl = (): string => {
if (typeof window !== 'undefined') {
const host = window.location.hostname;
if (host === 'localhost' || host === '127.0.0.1') {
return process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
}
return 'https://ifixkartbe.trionixsolution.com';
}
return process.env.NEXT_PUBLIC_API_URL || 'https://ifixkartbe.trionixsolution.com';
};
export const API_BASE_URL = getBaseUrl();
export function getMediaUrl(url: string | null | undefined): string {
if (!url) return '';
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//')) return url;
const base = getBaseUrl();
return `${base}${url.startsWith('/') ? '' : '/'}${url}`;
}
let accessToken: string | null = null;
export function setAccessToken(token: string | null): void {
accessToken = token;
}
export function getAccessToken(): string | null {
return accessToken;
}
export function clearTokens(): void {
accessToken = null;
}
export class ApiError extends Error {
status: number;
constructor(status: number, message: string) {
super(message);
this.status = status;
this.name = 'ApiError';
}
}
function formatApiErrorDetail(err: unknown): string {
const detail =
err && typeof err === 'object'
? (err as { detail?: unknown; message?: unknown }).detail ??
(err as { message?: unknown }).message
: err;
if (typeof detail === 'string' && detail.trim()) return detail;
if (Array.isArray(detail)) {
return detail
.map((item) => {
if (typeof item === 'string') return item;
if (item && typeof item === 'object') {
const row = item as { msg?: string; message?: string };
return row.msg || row.message || '';
}
return '';
})
.filter(Boolean)
.join('; ');
}
if (detail && typeof detail === 'object') {
const row = detail as { msg?: string; message?: string };
if (row.msg || row.message) return String(row.msg || row.message);
}
return '';
}
interface FetchOptions extends RequestInit {
skipAuth?: boolean;
}
export async function apiFetch<T>(
endpoint: string,
options: FetchOptions = {}
): Promise<T> {
const { skipAuth = false, headers: customHeaders, ...rest } = options;
const baseUrl = getBaseUrl();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(customHeaders as Record<string, string>),
};
if (!skipAuth) {
const token = getAccessToken();
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
}
try {
const response = await fetch(`${baseUrl}${endpoint}`, {
...rest,
headers,
credentials: 'include',
});
if (response.status === 401 && !skipAuth) {
const refreshed = await refreshAccessToken();
if (refreshed) {
headers['Authorization'] = `Bearer ${getAccessToken()}`;
const retryResponse = await fetch(`${baseUrl}${endpoint}`, {
...rest,
headers,
credentials: 'include',
});
if (!retryResponse.ok) {
const err = await retryResponse.json().catch(() => ({ detail: 'Request failed' }));
throw new ApiError(retryResponse.status, formatApiErrorDetail(err) || 'Request failed');
}
return retryResponse.json();
}
clearTokens();
throw new ApiError(401, 'Session expired');
}
if (!response.ok) {
const err = await response.json().catch(() => ({ detail: 'Request failed' }));
throw new ApiError(response.status, formatApiErrorDetail(err) || 'Request failed');
}
return response.json();
} catch (error: any) {
if (error instanceof ApiError) {
throw error;
}
throw new ApiError(
503,
`API Server is offline or unreachable. Please ensure the backend service at ${baseUrl} is running.`
);
}
}
export async function refreshAccessToken(): Promise<boolean> {
const baseUrl = getBaseUrl();
try {
const response = await fetch(`${baseUrl}/api/v1/customer/auth/refresh`, {
method: 'POST',
credentials: 'include',
});
if (!response.ok) return false;
const data = await response.json();
setAccessToken(data.access_token);
return true;
} catch {
return false;
}
}
export interface RestoredSession {
access_token: string;
customer_id: string;
email: string;
first_name: string;
}
/**
* Bootstrap customer session from HttpOnly refresh_token cookie.
* Returns token payload on success, null if no valid session.
*/
export async function restoreCustomerSession(): Promise<RestoredSession | null> {
const baseUrl = getBaseUrl();
try {
const response = await fetch(`${baseUrl}/api/v1/customer/auth/refresh`, {
method: 'POST',
credentials: 'include',
});
if (!response.ok) return null;
const data = await response.json();
if (!data?.access_token) return null;
setAccessToken(data.access_token);
return {
access_token: data.access_token,
customer_id: data.customer_id,
email: data.email,
first_name: data.first_name,
};
} catch {
return null;
}
}