289 lines
8.3 KiB
TypeScript
289 lines
8.3 KiB
TypeScript
/**
|
|
* Cliente HTTP com refresh automático de token e fila de requisições
|
|
* Implementa lock para evitar múltiplas chamadas de refresh simultaneamente
|
|
*/
|
|
|
|
import { AUTH_STORAGE_KEYS } from "@/types/auth";
|
|
import { isExpired } from "@/lib/jwt";
|
|
import { API_KEY } from "@/lib/config";
|
|
|
|
interface QueuedRequest {
|
|
resolve: (value: any) => void;
|
|
reject: (error: any) => void;
|
|
config: RequestInit & { url: string };
|
|
}
|
|
|
|
class HttpClient {
|
|
private isRefreshing = false;
|
|
private requestQueue: QueuedRequest[] = [];
|
|
private baseURL: string;
|
|
|
|
constructor(baseURL: string) {
|
|
this.baseURL = baseURL;
|
|
}
|
|
|
|
/**
|
|
* Processa fila de requisições após refresh bem-sucedido
|
|
*/
|
|
private processQueue(error: Error | null, token: string | null = null) {
|
|
console.log(
|
|
`[HTTP] Processando fila de ${this.requestQueue.length} requisições`,
|
|
);
|
|
|
|
this.requestQueue.forEach(({ resolve, reject, config }) => {
|
|
if (error) {
|
|
reject(error);
|
|
} else {
|
|
// Reexecutar requisição com novo token
|
|
const headers = {
|
|
...config.headers,
|
|
Authorization: `Bearer ${token}`,
|
|
};
|
|
resolve(this.executeRequest({ ...config, headers }));
|
|
}
|
|
});
|
|
|
|
this.requestQueue = [];
|
|
console.log("[HTTP] Fila de requisições processada");
|
|
}
|
|
|
|
/**
|
|
* Executa refresh de token uma única vez usando lock
|
|
*/
|
|
private async refreshToken(): Promise<string | null> {
|
|
const refreshToken = localStorage.getItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN);
|
|
if (!refreshToken) {
|
|
throw new Error("No refresh token available");
|
|
}
|
|
|
|
console.log("[HTTP] Iniciando refresh de token...", {
|
|
timestamp: new Date().toLocaleTimeString(),
|
|
});
|
|
|
|
const response = await fetch(
|
|
"https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/token?grant_type=refresh_token",
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
apikey: API_KEY, // API Key sempre necessária
|
|
},
|
|
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
},
|
|
);
|
|
|
|
if (!response.ok) {
|
|
console.log("[HTTP] Refresh falhou:", response.status);
|
|
throw new Error(`Refresh failed: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Atualizar tokens de forma atômica
|
|
localStorage.setItem(AUTH_STORAGE_KEYS.TOKEN, data.access_token);
|
|
if (data.refresh_token) {
|
|
localStorage.setItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN, data.refresh_token);
|
|
}
|
|
|
|
console.log("[HTTP] Token renovado com sucesso!", {
|
|
timestamp: new Date().toLocaleTimeString(),
|
|
});
|
|
return data.access_token;
|
|
}
|
|
|
|
/**
|
|
* Executa requisição HTTP com tratamento de erros
|
|
*/
|
|
private async executeRequest(
|
|
config: RequestInit & { url: string },
|
|
): Promise<Response> {
|
|
try {
|
|
console.log(
|
|
`[HTTP] Fazendo requisição: ${config.method || "GET"} ${config.url}`,
|
|
);
|
|
|
|
// Delay para visualizar na aba Network
|
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
|
|
const response = await fetch(config.url, config);
|
|
|
|
console.log(
|
|
`[HTTP] Resposta recebida: ${response.status} ${response.statusText}`,
|
|
{
|
|
url: config.url,
|
|
status: response.status,
|
|
timestamp: new Date().toLocaleTimeString(),
|
|
},
|
|
);
|
|
|
|
// Se for 401 e não for uma tentativa de refresh, tentar renovar token
|
|
if (response.status === 401 && !config.url.includes("/refresh")) {
|
|
console.log(
|
|
"[HTTP] Status 401 - Verificando possibilidade de refresh token...",
|
|
);
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
|
|
const token = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN);
|
|
if (token && !isExpired(token)) {
|
|
// Token ainda é válido, erro pode ser temporário
|
|
console.log("[HTTP] Token ainda válido - erro pode ser temporário");
|
|
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
return response;
|
|
}
|
|
|
|
// Token expirado, tentar refresh
|
|
if (this.isRefreshing) {
|
|
// Adicionar à fila se já está fazendo refresh
|
|
return new Promise((resolve, reject) => {
|
|
this.requestQueue.push({
|
|
resolve,
|
|
reject,
|
|
config,
|
|
});
|
|
});
|
|
}
|
|
|
|
this.isRefreshing = true;
|
|
|
|
try {
|
|
const newToken = await this.refreshToken();
|
|
this.isRefreshing = false;
|
|
|
|
// Processar fila com sucesso
|
|
this.processQueue(null, newToken);
|
|
|
|
// Reexecutar requisição original
|
|
const newHeaders = {
|
|
...config.headers,
|
|
apikey: API_KEY, // Garantir API Key
|
|
Authorization: `Bearer ${newToken}`,
|
|
};
|
|
|
|
console.log("[HTTP] Reexecutando requisição com novo token...");
|
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
|
|
return await fetch(config.url, { ...config, headers: newHeaders });
|
|
} catch (refreshError) {
|
|
this.isRefreshing = false;
|
|
this.processQueue(refreshError as Error);
|
|
|
|
// Logout único em caso de falha no refresh
|
|
console.error(
|
|
"[HTTP] Refresh FALHOU - fazendo logout automático:",
|
|
refreshError,
|
|
);
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
this.performLogout();
|
|
|
|
throw refreshError;
|
|
}
|
|
}
|
|
|
|
return response;
|
|
} catch (error) {
|
|
console.error("[HTTP] Erro na requisição:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Logout único com limpeza de estado
|
|
*/
|
|
private performLogout() {
|
|
// Limpar dados de autenticação
|
|
localStorage.removeItem(AUTH_STORAGE_KEYS.TOKEN);
|
|
localStorage.removeItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN);
|
|
localStorage.removeItem(AUTH_STORAGE_KEYS.USER);
|
|
|
|
// Redirecionar para login
|
|
if (typeof window !== "undefined") {
|
|
const userType =
|
|
localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE) || "profissional";
|
|
const loginRoutes = {
|
|
profissional: "/login",
|
|
paciente: "/login-paciente",
|
|
administrador: "/login-admin",
|
|
};
|
|
|
|
const loginRoute =
|
|
loginRoutes[userType as keyof typeof loginRoutes] || "/login";
|
|
window.location.href = loginRoute;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Método público para fazer requisições autenticadas
|
|
*/
|
|
async request(url: string, options: RequestInit = {}): Promise<Response> {
|
|
const token = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN);
|
|
|
|
console.log(
|
|
`[HTTP] Preparando requisição: ${options.method || "GET"} ${url}`,
|
|
{
|
|
hasToken: !!token,
|
|
timestamp: new Date().toLocaleTimeString(),
|
|
},
|
|
);
|
|
|
|
const config: RequestInit & { url: string } = {
|
|
url: url.startsWith("http") ? url : `${this.baseURL}${url}`,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
apikey: API_KEY, // API Key da Supabase sempre presente
|
|
...(token && { Authorization: `Bearer ${token}` }), // Bearer Token quando usuário logado
|
|
...options.headers,
|
|
},
|
|
...options,
|
|
};
|
|
|
|
const response = await this.executeRequest(config);
|
|
|
|
console.log(`[HTTP] Requisição finalizada: ${response.status}`, {
|
|
url: config.url,
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
});
|
|
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Métodos de conveniência
|
|
*/
|
|
async get(url: string, options?: RequestInit): Promise<Response> {
|
|
return this.request(url, { ...options, method: "GET" });
|
|
}
|
|
|
|
async post(
|
|
url: string,
|
|
data?: any,
|
|
options?: RequestInit,
|
|
): Promise<Response> {
|
|
return this.request(url, {
|
|
...options,
|
|
method: "POST",
|
|
body: data ? JSON.stringify(data) : undefined,
|
|
});
|
|
}
|
|
|
|
async put(url: string, data?: any, options?: RequestInit): Promise<Response> {
|
|
return this.request(url, {
|
|
...options,
|
|
method: "PUT",
|
|
body: data ? JSON.stringify(data) : undefined,
|
|
});
|
|
}
|
|
|
|
async delete(url: string, options?: RequestInit): Promise<Response> {
|
|
return this.request(url, { ...options, method: "DELETE" });
|
|
}
|
|
}
|
|
|
|
// Instância única do cliente HTTP
|
|
const API_BASE_URL =
|
|
process.env.NEXT_PUBLIC_API_URL ||
|
|
"https://mock.apidog.com/m1/1053378-0-default";
|
|
export const httpClient = new HttpClient(API_BASE_URL);
|
|
|
|
export default httpClient;
|