2025-10-14 18:27:07 -03:00

409 lines
13 KiB
TypeScript

"use client";
import {
createContext,
useContext,
useEffect,
useState,
ReactNode,
useCallback,
useMemo,
useRef,
} from "react";
import { useRouter } from "next/navigation";
import { loginUser, logoutUser, AuthenticationError } from "@/lib/auth";
import { getUserInfo } from "@/lib/api";
import { ENV_CONFIG } from "@/lib/env-config";
import { isExpired, parseJwt } from "@/lib/jwt";
import { httpClient } from "@/lib/http";
import type {
AuthContextType,
UserData,
AuthStatus,
UserType,
} from "@/types/auth";
import { AUTH_STORAGE_KEYS, LOGIN_ROUTES } from "@/types/auth";
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [authStatus, setAuthStatus] = useState<AuthStatus>("loading");
const [user, setUser] = useState<UserData | null>(null);
const [token, setToken] = useState<string | null>(null);
const router = useRouter();
const hasInitialized = useRef(false);
// Utilitários de armazenamento memorizados
const clearAuthData = useCallback(() => {
if (typeof window !== "undefined") {
localStorage.removeItem(AUTH_STORAGE_KEYS.TOKEN);
localStorage.removeItem(AUTH_STORAGE_KEYS.USER);
localStorage.removeItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN);
// Manter USER_TYPE para redirecionamento correto
}
setUser(null);
setToken(null);
setAuthStatus("unauthenticated");
console.log("[AUTH] Dados de autenticação limpos - logout realizado");
}, []);
const saveAuthData = useCallback(
(accessToken: string, userData: UserData, refreshToken?: string) => {
try {
if (typeof window !== "undefined") {
// Persistir dados de forma atômica
localStorage.setItem(AUTH_STORAGE_KEYS.TOKEN, accessToken);
localStorage.setItem(
AUTH_STORAGE_KEYS.USER,
JSON.stringify(userData)
);
localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, userData.userType);
if (refreshToken) {
localStorage.setItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
}
}
setToken(accessToken);
setUser(userData);
setAuthStatus("authenticated");
console.log("[AUTH] LOGIN realizado - Dados salvos!", {
userType: userData.userType,
email: userData.email,
timestamp: new Date().toLocaleTimeString(),
});
} catch (error) {
console.error("[AUTH] Erro ao salvar dados:", error);
clearAuthData();
}
},
[clearAuthData]
);
// Verificação inicial de autenticação
const checkAuth = useCallback(async (): Promise<void> => {
if (typeof window === "undefined") {
setAuthStatus("unauthenticated");
return;
}
try {
const storedToken = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN);
const storedUser = localStorage.getItem(AUTH_STORAGE_KEYS.USER);
console.log("[AUTH] Verificando sessão...", {
hasToken: !!storedToken,
hasUser: !!storedUser,
timestamp: new Date().toLocaleTimeString(),
});
// Pequeno delay para visualizar logs
await new Promise((resolve) => setTimeout(resolve, 800));
if (!storedToken || !storedUser) {
console.log("[AUTH] Dados ausentes - sessão inválida");
await new Promise((resolve) => setTimeout(resolve, 500));
clearAuthData();
return;
}
// Verificar se token está expirado
if (isExpired(storedToken)) {
console.log("[AUTH] Token expirado - tentando renovar...");
await new Promise((resolve) => setTimeout(resolve, 1000));
const refreshToken = localStorage.getItem(
AUTH_STORAGE_KEYS.REFRESH_TOKEN
);
if (refreshToken && !isExpired(refreshToken)) {
// Tentar renovar via HTTP client (que já tem a lógica)
try {
await httpClient.get("/auth/v1/me"); // Trigger refresh se necessário
// Se chegou aqui, refresh foi bem-sucedido
const newToken = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN);
const userData = JSON.parse(storedUser) as UserData;
if (newToken && newToken !== storedToken) {
setToken(newToken);
setUser(userData);
setAuthStatus("authenticated");
console.log("[AUTH] Token RENOVADO automaticamente!");
await new Promise((resolve) => setTimeout(resolve, 800));
return;
}
} catch (refreshError) {
console.log(" [AUTH] Falha no refresh automático");
await new Promise((resolve) => setTimeout(resolve, 400));
}
}
clearAuthData();
return;
}
// Restaurar sessão válida
const userData = JSON.parse(storedUser) as UserData;
setToken(storedToken);
// Tentar buscar profile consolidado (user-info) e mesclar
try {
const info = await getUserInfo();
if (info?.profile) {
const mapped = {
cpf: (info.profile as any).cpf ?? userData.profile?.cpf,
crm: (info.profile as any).crm ?? userData.profile?.crm,
telefone: info.profile.phone ?? userData.profile?.telefone,
foto_url: info.profile.avatar_url ?? userData.profile?.foto_url,
};
if (userData.profile) {
userData.profile = { ...userData.profile, ...mapped };
} else {
userData.profile = mapped;
}
// Persistir o usuário atualizado no localStorage para evitar
// que 'auth_user.profile' fique vazio após um reload completo
try {
if (typeof window !== "undefined") {
localStorage.setItem(
AUTH_STORAGE_KEYS.USER,
JSON.stringify(userData)
);
}
} catch (e) {
console.warn(
"[AUTH] Falha ao persistir user (profile) no localStorage:",
e
);
}
}
} catch (err) {
console.warn(
"[AUTH] Falha ao buscar user-info na restauração de sessão:",
err
);
}
setUser(userData);
setAuthStatus("authenticated");
console.log("[AUTH] Sessão RESTAURADA com sucesso!", {
userId: userData.id,
userType: userData.userType,
email: userData.email,
timestamp: new Date().toLocaleTimeString(),
});
await new Promise((resolve) => setTimeout(resolve, 1000));
} catch (error) {
console.error("[AUTH] Erro na verificação:", error);
clearAuthData();
}
}, [clearAuthData]);
// Login memoizado
const login = useCallback(
async (
email: string,
password: string,
userType: UserType
): Promise<boolean> => {
try {
console.log("[AUTH] Iniciando login:", { email, userType });
const response = await loginUser(email, password, userType);
// Após receber token, buscar roles/permissions reais e reconciliar userType
try {
const infoRes = await fetch(
`${ENV_CONFIG.SUPABASE_URL}/functions/v1/user-info`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${response.access_token}`,
apikey: ENV_CONFIG.SUPABASE_ANON_KEY,
},
}
);
if (infoRes.ok) {
const info = await infoRes.json().catch(() => null);
const roles: string[] = Array.isArray(info?.roles)
? info.roles
: info?.roles
? [info.roles]
: [];
// Derivar tipo de usuário a partir dos roles
// Derivar tipo de usuário a partir dos roles retornados pela API
let derived: UserType = userType; // 🔹 começa respeitando o tipo da tela de login
if (
roles.includes("admin") ||
roles.includes("gestor") ||
roles.includes("secretaria")
) {
derived = "administrador";
} else if (
roles.includes("medico") ||
roles.includes("enfermeiro")
) {
derived = "profissional";
} else if (roles.length === 0 && userType) {
// 🔹 se a API não retornou roles, mantemos o tipo informado pela tela
derived = userType;
}
// 🔹 Se o tipo da tela e o das roles divergirem, prioriza o da tela (exceto admin)
if (
response.user &&
response.user.userType !== derived &&
userType !== "administrador"
) {
response.user.userType = userType;
console.log(
"[AUTH] userType mantido da tela de login ->",
userType
);
} else if (response.user && response.user.userType !== derived) {
response.user.userType = derived;
console.log(
"[AUTH] userType reconciliado a partir das roles ->",
derived
);
}
} else {
console.warn(
"[AUTH] Falha ao obter user-info para reconciliar roles:",
infoRes.status
);
}
} catch (err) {
console.warn(
"[AUTH] Erro ao buscar user-info após login (não crítico):",
err
);
}
// Após login, tentar buscar profile consolidado e mesclar antes de persistir
try {
const info = await getUserInfo();
if (info?.profile && response.user) {
const mapped = {
cpf: (info.profile as any).cpf ?? response.user.profile?.cpf,
crm: (info.profile as any).crm ?? response.user.profile?.crm,
telefone: info.profile.phone ?? response.user.profile?.telefone,
foto_url:
info.profile.avatar_url ?? response.user.profile?.foto_url,
};
if (response.user.profile) {
response.user.profile = { ...response.user.profile, ...mapped };
} else {
response.user.profile = mapped;
}
}
} catch (err) {
console.warn(
"[AUTH] Falha ao buscar user-info após login (não crítico):",
err
);
}
saveAuthData(
response.access_token,
response.user,
response.refresh_token
);
console.log("[AUTH] Login realizado com sucesso");
return true;
} catch (error) {
console.error("[AUTH] Erro no login:", error);
if (error instanceof AuthenticationError) {
throw error;
}
throw new AuthenticationError(
"Erro inesperado durante o login",
"UNKNOWN_ERROR",
error
);
}
},
[saveAuthData]
);
// Logout memoizado
const logout = useCallback(async (): Promise<void> => {
console.log("[AUTH] Iniciando logout");
const currentUserType =
user?.userType ||
(typeof window !== "undefined"
? localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE)
: null) ||
"profissional";
try {
if (token) {
await logoutUser(token);
console.log("[AUTH] Logout realizado na API");
}
} catch (error) {
console.error("[AUTH] Erro no logout da API:", error);
}
clearAuthData();
// Redirecionamento baseado no tipo de usuário
const loginRoute = LOGIN_ROUTES[currentUserType as UserType] || "/login";
console.log("[AUTH] Redirecionando para:", loginRoute);
if (typeof window !== "undefined") {
window.location.href = loginRoute;
}
}, [user?.userType, token, clearAuthData]);
// Refresh token memoizado (usado pelo HTTP client)
const refreshToken = useCallback(async (): Promise<boolean> => {
// Esta função é principalmente para compatibilidade
// O refresh real é feito pelo HTTP client
return false;
}, []);
// Getters memorizados
const contextValue = useMemo(
() => ({
authStatus,
user,
token,
login,
logout,
refreshToken,
}),
[authStatus, user, token, login, logout, refreshToken]
);
// Inicialização única
useEffect(() => {
if (!hasInitialized.current && typeof window !== "undefined") {
hasInitialized.current = true;
checkAuth();
}
}, [checkAuth]);
return (
<AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
);
}
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth deve ser usado dentro de AuthProvider");
}
return context;
};