Fernando Pirichowski Aguiar d082028c5a feat: Sistema completo de agendamento médico com correções RLS e melhorias UI
- Fix: Avatar upload usando Supabase Client com RLS policies
- Fix: Profile update usando Supabase Client
- Fix: Timezone handling em datas de consultas
- Fix: Filtros de consultas passadas/futuras
- Fix: Appointment cancellation com Supabase Client
- Fix: Navegação após booking de consulta
- Fix: Report service usando Supabase Client
- Fix: Campo created_by em relatórios
- Fix: URL pública de avatares no Storage
- Fix: Modal de criação de usuário com scroll
- Feat: Sistema completo de gestão de consultas
- Feat: Painéis para paciente, médico, secretária e admin
- Feat: Upload de avatares
- Feat: Sistema de relatórios médicos
- Feat: Gestão de disponibilidade de médicos
2025-11-05 23:38:31 -03:00

367 lines
10 KiB
TypeScript

/**
* Cliente HTTP usando Axios
* Chamadas diretas ao Supabase
*/
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
import { API_CONFIG } from "./config";
class ApiClient {
private client: AxiosInstance;
constructor() {
this.client = axios.create({
baseURL: API_CONFIG.REST_URL,
timeout: API_CONFIG.TIMEOUT,
headers: {
"Content-Type": "application/json",
apikey: API_CONFIG.SUPABASE_ANON_KEY,
},
});
this.setupInterceptors();
}
private setupInterceptors() {
// Request interceptor - adiciona token automaticamente
this.client.interceptors.request.use(
(config) => {
// Não adicionar token se a flag _skipAuth estiver presente
if ((config as any)._skipAuth) {
delete (config as any)._skipAuth;
return config;
}
const token = localStorage.getItem(
API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN
);
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
console.log(
`[ApiClient] Request: ${config.method?.toUpperCase()} ${
config.url
} - Token presente: ${token.substring(0, 20)}...`
);
} else {
console.warn(
`[ApiClient] Request: ${config.method?.toUpperCase()} ${
config.url
} - SEM TOKEN!`
);
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor - trata erros globalmente
this.client.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
console.error(`[ApiClient] Erro na requisição:`, {
url: originalRequest?.url,
status: error.response?.status,
message: error.message,
});
// Se retornar 401 e não for uma requisição de refresh/login
if (
error.response?.status === 401 &&
!originalRequest._retry &&
!originalRequest.url?.includes("auth-refresh") &&
!originalRequest.url?.includes("auth-login")
) {
originalRequest._retry = true;
console.log("[ApiClient] Recebeu 401, tentando renovar token...");
try {
// Tenta renovar o token
const refreshToken = localStorage.getItem(
API_CONFIG.STORAGE_KEYS.REFRESH_TOKEN
);
if (refreshToken) {
console.log("[ApiClient] Refresh token encontrado, renovando...");
// Chama Supabase diretamente para renovar token
const response = await axios.post(
`${API_CONFIG.AUTH_URL}/token?grant_type=refresh_token`,
{
refresh_token: refreshToken,
},
{
headers: {
"Content-Type": "application/json",
apikey: API_CONFIG.SUPABASE_ANON_KEY,
},
}
);
const {
access_token,
refresh_token: newRefreshToken,
user,
} = response.data;
console.log("[ApiClient] Token renovado com sucesso!");
// Atualiza tokens
localStorage.setItem(
API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN,
access_token
);
localStorage.setItem(
API_CONFIG.STORAGE_KEYS.REFRESH_TOKEN,
newRefreshToken
);
localStorage.setItem(
API_CONFIG.STORAGE_KEYS.USER,
JSON.stringify(user)
);
// Atualiza o header da requisição original
originalRequest.headers.Authorization = `Bearer ${access_token}`;
// Refaz a requisição original
return this.client(originalRequest);
} else {
console.warn("[ApiClient] Nenhum refresh token disponível!");
}
} catch (refreshError) {
console.error(
"[ApiClient] Falha ao renovar token, fazendo logout...",
refreshError
);
// Se refresh falhar, limpa tudo e redireciona para home
localStorage.removeItem(API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN);
localStorage.removeItem(API_CONFIG.STORAGE_KEYS.REFRESH_TOKEN);
localStorage.removeItem(API_CONFIG.STORAGE_KEYS.USER);
// Redireciona para home ao invés de login específico
if (
!window.location.pathname.includes("/login") &&
window.location.pathname !== "/"
) {
window.location.href = "/";
}
return Promise.reject(refreshError);
}
}
// Se não conseguir renovar, apenas loga o erro mas NÃO limpa a sessão
// Deixa o componente decidir o que fazer
if (error.response?.status === 401) {
console.error(
"[ApiClient] 401 não tratado - requisição:",
originalRequest?.url
);
}
return Promise.reject(error);
}
);
}
// Métodos HTTP
async get<T>(
url: string,
config?: AxiosRequestConfig
): Promise<AxiosResponse<T>> {
console.log(
"[ApiClient] GET Request:",
url,
"Params:",
JSON.stringify(config?.params)
);
const response = await this.client.get<T>(url, config);
console.log("[ApiClient] GET Response:", {
status: response.status,
dataType: typeof response.data,
isArray: Array.isArray(response.data),
dataLength: Array.isArray(response.data)
? response.data.length
: "not array",
});
console.log(
"[ApiClient] Response Data:",
JSON.stringify(response.data, null, 2)
);
return response;
}
async post<T>(
url: string,
data?: unknown,
config?: AxiosRequestConfig
): Promise<AxiosResponse<T>> {
console.log("[ApiClient] POST Request:", {
url,
fullUrl: `${this.client.defaults.baseURL}${url}`,
data,
config,
});
try {
const response = await this.client.post<T>(url, data, config);
console.log("[ApiClient] POST Response:", {
status: response.status,
data: response.data,
});
return response;
} catch (error: any) {
console.error("[ApiClient] POST Error:", {
url,
fullUrl: `${this.client.defaults.baseURL}${url}`,
status: error?.response?.status,
statusText: error?.response?.statusText,
data: error?.response?.data,
message: error?.message,
});
throw error;
}
}
/**
* POST público - não envia token de autenticação
* Usado para endpoints de auto-registro
*/
async postPublic<T>(
url: string,
data?: unknown,
config?: AxiosRequestConfig
): Promise<AxiosResponse<T>> {
const configWithoutAuth = {
...config,
headers: {
...config?.headers,
// Remove Authorization header
},
// Flag especial para o interceptor saber que não deve adicionar token
_skipAuth: true,
};
return this.client.post<T>(url, data, configWithoutAuth);
}
async patch<T>(
url: string,
data?: unknown,
config?: AxiosRequestConfig
): Promise<AxiosResponse<T>> {
console.log("[ApiClient] PATCH Request:", {
url,
data,
config,
});
try {
// Adicionar header Prefer para Supabase retornar os dados atualizados
const configWithPrefer = {
...config,
headers: {
...config?.headers,
Prefer: "return=representation",
},
};
const response = await this.client.patch<T>(url, data, configWithPrefer);
console.log("[ApiClient] PATCH Response:", {
status: response.status,
data: response.data,
});
return response;
} catch (error: any) {
console.error("[ApiClient] PATCH Error:", {
url,
status: error?.response?.status,
statusText: error?.response?.statusText,
data: error?.response?.data,
message: error?.message,
});
throw error;
}
}
async delete<T>(
url: string,
config?: AxiosRequestConfig
): Promise<AxiosResponse<T>> {
return this.client.delete<T>(url, config);
}
async put<T>(
url: string,
data?: unknown,
config?: AxiosRequestConfig
): Promise<AxiosResponse<T>> {
return this.client.put<T>(url, data, config);
}
/**
* Chama uma Edge Function do Supabase
* Usa a baseURL de Functions em vez de REST
*/
async callFunction<T>(
functionName: string,
data?: unknown,
config?: AxiosRequestConfig
): Promise<AxiosResponse<T>> {
const fullUrl = `${API_CONFIG.FUNCTIONS_URL}/${functionName}`;
console.log("[ApiClient] Calling Edge Function:", {
functionName,
fullUrl,
data,
});
// Cria uma requisição sem baseURL
const functionsClient = axios.create({
timeout: API_CONFIG.TIMEOUT,
headers: {
"Content-Type": "application/json",
apikey: API_CONFIG.SUPABASE_ANON_KEY,
},
});
// Adiciona token se disponível
const token = localStorage.getItem(API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN);
if (token) {
functionsClient.defaults.headers.common[
"Authorization"
] = `Bearer ${token}`;
}
try {
const response = await functionsClient.post<T>(fullUrl, data, config);
console.log("[ApiClient] Edge Function Response:", {
functionName,
status: response.status,
data: response.data,
});
return response;
} catch (error: any) {
console.error("[ApiClient] Edge Function Error:", {
functionName,
fullUrl,
status: error?.response?.status,
statusText: error?.response?.statusText,
data: error?.response?.data,
message: error?.message,
});
throw error;
}
}
}
export const apiClient = new ApiClient();