184 lines
5.1 KiB
TypeScript
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;
|
|
}
|
|
}
|