quiniela-sembradores-frontend/lib/api/client.ts

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),
};