271 lines
7.9 KiB
TypeScript
271 lines
7.9 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 { 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);
|
|
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);
|
|
|
|
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;
|
|
};
|