148 lines
4 KiB
TypeScript
148 lines
4 KiB
TypeScript
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8080';
|
|
|
|
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
|
|
type RequestOptions = {
|
|
body?: unknown;
|
|
headers?: HeadersInit;
|
|
signal?: AbortSignal;
|
|
skipAuthRedirect?: boolean;
|
|
};
|
|
|
|
export type ApiValidationErrors = Record<string, string[]>;
|
|
|
|
export class ApiError extends Error {
|
|
status: number;
|
|
errors?: ApiValidationErrors;
|
|
|
|
constructor(message: string, status: number, errors?: ApiValidationErrors) {
|
|
super(message);
|
|
this.name = 'ApiError';
|
|
this.status = status;
|
|
this.errors = errors;
|
|
}
|
|
}
|
|
|
|
const buildUrl = (path: string) => {
|
|
if (path.startsWith('http://') || path.startsWith('https://')) {
|
|
return path;
|
|
}
|
|
|
|
return `${API_BASE_URL}${path.startsWith('/') ? path : `/${path}`}`;
|
|
};
|
|
|
|
const parseJsonSafely = async (response: Response) => {
|
|
const contentType = response.headers.get('content-type') ?? '';
|
|
|
|
if (!contentType.includes('application/json')) {
|
|
return null;
|
|
}
|
|
|
|
return response.json();
|
|
};
|
|
|
|
const ensureCsrfCookie = async () => {
|
|
await fetch(`${API_BASE_URL}/sanctum/csrf-cookie`, {
|
|
method: 'GET',
|
|
credentials: 'include',
|
|
headers: {
|
|
Accept: 'application/json',
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
},
|
|
});
|
|
};
|
|
|
|
const getCookie = (name: string) => {
|
|
if (typeof document === 'undefined') {
|
|
return null;
|
|
}
|
|
|
|
const match = document.cookie
|
|
.split('; ')
|
|
.find((row) => row.startsWith(`${name}=`));
|
|
|
|
return match ? decodeURIComponent(match.split('=')[1]) : null;
|
|
};
|
|
|
|
const redirectToLogin = () => {
|
|
if (typeof window === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
const pathname = window.location.pathname;
|
|
const search = window.location.search;
|
|
|
|
const isAuthPage = pathname === '/login' || pathname === '/register';
|
|
|
|
if (isAuthPage) {
|
|
return;
|
|
}
|
|
|
|
const next = encodeURIComponent(`${pathname}${search}`);
|
|
window.location.href = `/login?next=${next}`;
|
|
};
|
|
|
|
const request = async <T>(
|
|
method: HttpMethod,
|
|
path: string,
|
|
options: RequestOptions = {},
|
|
): Promise<T> => {
|
|
const headers = new Headers(options.headers);
|
|
const hasBody = options.body !== undefined;
|
|
|
|
headers.set('Accept', 'application/json');
|
|
headers.set('X-Requested-With', 'XMLHttpRequest');
|
|
|
|
if (hasBody) {
|
|
headers.set('Content-Type', 'application/json');
|
|
}
|
|
|
|
const xsrfToken = getCookie('XSRF-TOKEN');
|
|
|
|
if (xsrfToken) {
|
|
headers.set('X-XSRF-TOKEN', xsrfToken);
|
|
}
|
|
|
|
const response = await fetch(buildUrl(path), {
|
|
method,
|
|
headers,
|
|
body: hasBody ? JSON.stringify(options.body) : undefined,
|
|
signal: options.signal,
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const payload = await parseJsonSafely(response);
|
|
const message =
|
|
payload && typeof payload.message === 'string'
|
|
? payload.message
|
|
: 'La solicitud al servidor falló.';
|
|
|
|
if (response.status === 401 && !options.skipAuthRedirect) {
|
|
redirectToLogin();
|
|
}
|
|
|
|
throw new ApiError(message, response.status, payload?.errors);
|
|
}
|
|
|
|
if (response.status === 204) {
|
|
return undefined as T;
|
|
}
|
|
|
|
return (await parseJsonSafely(response)) as T;
|
|
};
|
|
|
|
export const apiClient = {
|
|
baseUrl: API_BASE_URL,
|
|
ensureCsrfCookie,
|
|
get: <T>(path: string, options?: Omit<RequestOptions, 'body'>) =>
|
|
request<T>('GET', path, options),
|
|
post: <T>(path: string, body?: unknown, options?: Omit<RequestOptions, 'body'>) =>
|
|
request<T>('POST', path, { ...options, body }),
|
|
put: <T>(path: string, body?: unknown, options?: Omit<RequestOptions, 'body'>) =>
|
|
request<T>('PUT', path, { ...options, body }),
|
|
patch: <T>(path: string, body?: unknown, options?: Omit<RequestOptions, 'body'>) =>
|
|
request<T>('PATCH', path, { ...options, body }),
|
|
delete: <T>(path: string, options?: Omit<RequestOptions, 'body'>) =>
|
|
request<T>('DELETE', path, options),
|
|
};
|