|
|
@@ -0,0 +1,88 @@
|
|
|
+// Client HTTP central da aplicação.
|
|
|
+//
|
|
|
+// Responsabilidades:
|
|
|
+// - montar a URL com query params (ignorando valores vazios);
|
|
|
+// - injetar o header `Authorization: Bearer <jwt>` nas chamadas privadas;
|
|
|
+// - desempacotar o envelope padrão do backend ({ status, code, message, data });
|
|
|
+// - lançar `ApiError` em falhas (incluindo erro de rede);
|
|
|
+// - tratar 401 globalmente: limpa o token e redireciona ao login.
|
|
|
+import { browser } from '$app/environment';
|
|
|
+import { goto } from '$app/navigation';
|
|
|
+import { API_BASE_URL } from './config.js';
|
|
|
+import { getToken, clearToken } from './token.js';
|
|
|
+
|
|
|
+/** Erro normalizado de API. `code` segue a tabela do backend (E_VALIDATE, etc.). */
|
|
|
+export class ApiError extends Error {
|
|
|
+ constructor(message, code = 'E_GENERIC', httpStatus = 0, data = null) {
|
|
|
+ super(message);
|
|
|
+ this.name = 'ApiError';
|
|
|
+ this.code = code;
|
|
|
+ this.httpStatus = httpStatus;
|
|
|
+ this.data = data;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function buildUrl(path, query) {
|
|
|
+ const url = new URL(`${API_BASE_URL}${path}`);
|
|
|
+ if (query) {
|
|
|
+ for (const [key, value] of Object.entries(query)) {
|
|
|
+ if (value === undefined || value === null || value === '') continue;
|
|
|
+ url.searchParams.set(key, String(value));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return url.toString();
|
|
|
+}
|
|
|
+
|
|
|
+async function request(method, path, { query, body, auth = true } = {}) {
|
|
|
+ const headers = { 'Content-Type': 'application/json' };
|
|
|
+
|
|
|
+ if (auth) {
|
|
|
+ const token = getToken();
|
|
|
+ if (token) headers.Authorization = `Bearer ${token}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ let response;
|
|
|
+ try {
|
|
|
+ response = await fetch(buildUrl(path, query), {
|
|
|
+ method,
|
|
|
+ headers,
|
|
|
+ body: body !== undefined ? JSON.stringify(body) : undefined
|
|
|
+ });
|
|
|
+ } catch {
|
|
|
+ throw new ApiError('Falha de conexão com o servidor.', 'E_NETWORK', 0, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Tratamento global de sessão expirada/ inválida.
|
|
|
+ if (response.status === 401) {
|
|
|
+ clearToken();
|
|
|
+ if (browser) goto('/login');
|
|
|
+ throw new ApiError('Sessão expirada. Faça login novamente.', 'E_UNAUTHORIZED', 401, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ // O backend omite `data` quando vazio; algumas respostas podem não ser JSON.
|
|
|
+ let envelope = null;
|
|
|
+ try {
|
|
|
+ envelope = await response.json();
|
|
|
+ } catch {
|
|
|
+ envelope = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!envelope || envelope.status === 'failed') {
|
|
|
+ throw new ApiError(
|
|
|
+ envelope?.message ?? 'Erro inesperado ao comunicar com o servidor.',
|
|
|
+ envelope?.code ?? 'E_GENERIC',
|
|
|
+ response.status,
|
|
|
+ envelope?.data ?? null
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return envelope.data ?? {};
|
|
|
+}
|
|
|
+
|
|
|
+export const api = {
|
|
|
+ get: (path, options) => request('GET', path, options),
|
|
|
+ post: (path, options) => request('POST', path, options),
|
|
|
+ put: (path, options) => request('PUT', path, options),
|
|
|
+ patch: (path, options) => request('PATCH', path, options),
|
|
|
+ delete: (path, options) => request('DELETE', path, options)
|
|
|
+};
|