riseup-squad18/src/context/AuthContext.tsx
Fernando Pirichowski Aguiar 389a191f20 fix: corrige persistência de avatar, agendamento de consulta e download de PDF
- Avatar do paciente agora persiste após reload (adiciona timestamp para evitar cache)
- Agendamento usa patient_id correto ao invés de user_id
- Botão de download de PDF desbloqueado com logs detalhados
2025-11-15 08:36:41 -03:00

666 lines
20 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, {
createContext,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import toast from "react-hot-toast";
import { authService, userService } from "../services";
import { supabase } from "../lib/supabase";
// Tipos auxiliares
interface UserInfoFullResponse {
access_token: string;
refresh_token: string;
user: {
id: string;
email?: string;
user_metadata?: any;
};
roles?: string[];
permissions?: any;
profile?: {
full_name?: string;
};
}
// Mock temporário para compatibilidade
const doctorService = {
loginMedico: async (email: string, senha: string) => ({
success: false,
error: "Use login unificado",
data: null as any,
}),
};
type Medico = any;
// tokenManager removido no modelo somente Supabase (sem usuário técnico)
// Tipos de roles suportados
export type UserRole =
| "secretaria"
| "medico"
| "paciente"
| "admin"
| "gestor"
| "user"; // Role genérica para pacientes
export interface SessionUserBase {
id: string;
nome: string;
email?: string;
role: UserRole;
roles?: UserRole[];
permissions?: { [k: string]: boolean | undefined };
}
export interface SecretariaUser extends SessionUserBase {
role: "secretaria";
}
export interface MedicoUser extends SessionUserBase {
role: "medico";
crm?: string;
especialidade?: string;
}
export interface PacienteUser extends SessionUserBase {
role: "paciente";
pacienteId?: string;
}
export interface AdminUser extends SessionUserBase {
role: "admin";
}
export type SessionUser =
| SecretariaUser
| MedicoUser
| PacienteUser
| AdminUser
| (SessionUserBase & { role: "gestor" });
interface AuthContextValue {
user: SessionUser | null;
isAuthenticated: boolean;
loading: boolean;
loginSecretaria: (email: string, senha: string) => Promise<boolean>;
loginMedico: (email: string, senha: string) => Promise<boolean>;
loginComEmailSenha: (email: string, senha: string) => Promise<boolean>; // fluxo unificado real
loginPaciente: (paciente: {
id: string;
nome: string;
email?: string;
}) => Promise<boolean>;
logout: () => void;
role: UserRole | null;
roles: UserRole[];
permissions: Record<string, boolean | undefined>;
refreshSession: () => Promise<void>;
}
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
const STORAGE_KEY = "appSession";
const SESSION_VERSION = "2.0"; // Incrementar quando mudar estrutura de roles
interface PersistedSession {
user: SessionUser;
token?: string; // para quando integrar authService real
refreshToken?: string;
savedAt: string;
version?: string; // Versão da estrutura da sessão
}
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [user, setUser] = useState<SessionUser | null>(null);
const [loading, setLoading] = useState(true);
// Log sempre que user ou loading mudar
useEffect(() => {
console.log("[AuthContext] 🔄 ESTADO MUDOU:", {
user: user ? { id: user.id, nome: user.nome, role: user.role } : null,
loading,
isAuthenticated: !!user,
timestamp: new Date().toISOString(),
});
}, [user, loading]);
// RE-VERIFICAR sessão quando user estiver null mas localStorage tiver dados
// Isso corrige o problema de navegação entre páginas perdendo o estado
useEffect(() => {
if (!loading && !user) {
console.log(
"[AuthContext] 🔍 User é null mas loading false, verificando localStorage..."
);
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
try {
const parsed = JSON.parse(raw) as PersistedSession;
if (parsed?.user?.role) {
console.log(
"[AuthContext] 🔧 RECUPERANDO sessão perdida:",
parsed.user.nome
);
setUser(parsed.user);
// Token restoration is handled automatically by authService
}
} catch (e) {
console.error("[AuthContext] Erro ao recuperar sessão:", e);
}
}
}
}, [user, loading]);
// Monitorar mudanças no localStorage para debug
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === STORAGE_KEY) {
console.log("[AuthContext] 📢 localStorage MUDOU externamente!", {
oldValue: e.oldValue ? "TINHA DADOS" : "VAZIO",
newValue: e.newValue ? "TEM DADOS" : "VAZIO",
url: e.url,
});
}
};
window.addEventListener("storage", handleStorageChange);
return () => window.removeEventListener("storage", handleStorageChange);
}, []);
// Restaurar sessão do localStorage e verificar token
// IMPORTANTE: Este useEffect roda apenas UMA VEZ quando o AuthProvider monta
useEffect(() => {
console.log("[AuthContext] 🚀 INICIANDO RESTAURAÇÃO DE SESSÃO (mount)");
console.log("[AuthContext] 🔍 Verificando TODOS os itens no localStorage:");
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) {
const value = localStorage.getItem(key);
console.log(` - ${key}: ${value?.substring(0, 50)}...`);
}
}
const restoreSession = async () => {
try {
// Tentar localStorage primeiro, depois sessionStorage como backup
let raw = localStorage.getItem(STORAGE_KEY);
console.log(
"[AuthContext] localStorage raw:",
raw ? "EXISTE" : "VAZIO"
);
if (!raw) {
console.log(
"[AuthContext] 🔍 localStorage vazio, tentando sessionStorage..."
);
raw = sessionStorage.getItem(STORAGE_KEY);
console.log(
"[AuthContext] sessionStorage raw:",
raw ? "EXISTE" : "VAZIO"
);
if (raw) {
// Restaurar do sessionStorage para localStorage
console.log(
"[AuthContext] 🔄 Restaurando do sessionStorage para localStorage"
);
localStorage.setItem(STORAGE_KEY, raw);
}
}
if (raw) {
console.log(
"[AuthContext] Conteúdo completo:",
raw.substring(0, 100) + "..."
);
}
if (raw) {
const parsed = JSON.parse(raw) as PersistedSession;
// Verificar versão da sessão - se for antiga, atualizar a role do JWT
if (!parsed.version || parsed.version !== SESSION_VERSION) {
console.log(
"[AuthContext] ⚠️ Sessão antiga detectada (versão:",
parsed.version || "sem versão",
"vs atual:",
SESSION_VERSION,
"). Atualizando role do JWT..."
);
// Pegar o token JWT do localStorage
const accessToken = localStorage.getItem(
"mediconnect_access_token"
);
if (accessToken) {
try {
// Decodificar JWT para pegar app_metadata
const payload = JSON.parse(atob(accessToken.split(".")[1]));
const userRole =
payload.app_metadata?.user_role ||
payload.user_metadata?.role;
console.log("[AuthContext] 🔑 Role do JWT:", userRole);
if (userRole) {
const normalizedRole = normalizeRole(userRole);
console.log(
"[AuthContext] ✅ Atualizando role de",
parsed.user.role,
"para",
normalizedRole
);
// Atualizar a sessão com a role correta
const updatedUser = {
...parsed.user,
role: normalizedRole,
roles: [normalizedRole],
} as SessionUser;
setUser(updatedUser);
persist({
user: updatedUser,
token: parsed.token,
version: SESSION_VERSION,
});
setLoading(false);
return;
}
} catch (error) {
console.error(
"[AuthContext] ❌ Erro ao decodificar JWT:",
error
);
}
}
console.log(
"[AuthContext] ⚠️ Não conseguiu atualizar role do JWT, usando role da sessão antiga"
);
// Usar sessão antiga mesmo sem conseguir atualizar
const updatedUser = {
...parsed.user,
version: SESSION_VERSION,
} as SessionUser;
setUser(updatedUser);
persist({
user: updatedUser,
token: parsed.token,
version: SESSION_VERSION,
});
setLoading(false);
return;
}
if (parsed?.user?.role) {
console.log("[AuthContext] ✅ Restaurando sessão:", {
nome: parsed.user.nome,
role: parsed.user.role,
hasToken: !!parsed.token,
});
// Token management is handled automatically by authService
if (parsed.token) {
console.log("[AuthContext] Sessão com token encontrada");
} else {
console.warn(
"[AuthContext] ⚠️ Sessão encontrada mas sem token. Pode estar inválida."
);
}
console.log(
"[AuthContext] 📝 Chamando setUser com:",
parsed.user.nome
);
setUser(parsed.user);
} else {
console.log(
"[AuthContext] ⚠️ Sessão parseada mas sem user.role válido"
);
}
} else {
console.log(
"[AuthContext] Nenhuma sessão salva encontrada no localStorage"
);
}
} catch (error) {
console.error("[AuthContext] ❌ Erro ao restaurar sessão:", error);
// Se houver erro ao restaurar, limpar tudo para evitar loops
console.log("[AuthContext] 🧹 Limpando localStorage devido a erro");
localStorage.removeItem(STORAGE_KEY);
sessionStorage.removeItem(STORAGE_KEY);
} finally {
console.log(
"[AuthContext] 🏁 Finalizando restauração, setLoading(false)"
);
setLoading(false);
}
};
void restoreSession();
}, []);
const persist = useCallback((session: PersistedSession) => {
try {
console.log(
"[AuthContext] 💾 SALVANDO sessão no localStorage E sessionStorage:",
{
user: session.user.nome,
role: session.user.role,
hasToken: !!session.token,
}
);
const sessionWithVersion = { ...session, version: SESSION_VERSION };
const sessionStr = JSON.stringify(sessionWithVersion);
localStorage.setItem(STORAGE_KEY, sessionStr);
sessionStorage.setItem(STORAGE_KEY, sessionStr); // BACKUP em sessionStorage
console.log(
"[AuthContext] ✅ Sessão salva com sucesso em ambos storages!"
);
} catch (error) {
console.error("[AuthContext] ❌ ERRO ao salvar sessão:", error);
}
}, []);
const clearPersisted = useCallback(() => {
try {
console.log(
"[AuthContext] 🗑️ REMOVENDO sessão do localStorage E sessionStorage"
);
localStorage.removeItem(STORAGE_KEY);
sessionStorage.removeItem(STORAGE_KEY);
console.log(
"[AuthContext] ✅ Sessão removida com sucesso de ambos storages!"
);
} catch (error) {
console.error("[AuthContext] ❌ ERRO ao remover sessão:", error);
}
}, []);
const normalizeRole = (r: string | undefined): UserRole | undefined => {
if (!r) return undefined;
const map: Record<string, UserRole> = {
medico: "medico",
doctor: "medico",
secretaria: "secretaria",
assistant: "secretaria",
paciente: "paciente",
patient: "paciente",
user: "paciente", // Role genérica mapeada para paciente
admin: "admin",
gestor: "gestor",
manager: "gestor",
};
return map[r.toLowerCase()] || undefined;
};
const pickPrimaryRole = (rolesArr: UserRole[]): UserRole => {
const priority: UserRole[] = [
"admin",
"gestor",
"medico",
"secretaria",
"paciente",
];
for (const p of priority) if (rolesArr.includes(p)) return p;
return rolesArr[0] || "paciente";
};
const buildSessionUser = React.useCallback(
(info: UserInfoFullResponse): SessionUser => {
// ⚠️ SEGURANÇA: Nunca logar tokens ou dados sensíveis em produção
// Tentar pegar role do app_metadata primeiro (mais confiável)
let rolesFromMetadata: UserRole[] = [];
if (info.user?.user_metadata?.app_metadata?.user_role) {
const roleFromApp = normalizeRole(
info.user.user_metadata.app_metadata.user_role
);
if (roleFromApp) rolesFromMetadata.push(roleFromApp);
}
// Depois do user_metadata.role
if (info.user?.user_metadata?.role) {
const roleFromUser = normalizeRole(info.user.user_metadata.role);
if (roleFromUser) rolesFromMetadata.push(roleFromUser);
}
const rolesNormalized = (info.roles || [])
.map(normalizeRole)
.filter(Boolean) as UserRole[];
// Combinar roles do metadata com roles do array
const allRoles = [...new Set([...rolesFromMetadata, ...rolesNormalized])];
const permissions = info.permissions || {};
const primaryRole = pickPrimaryRole(
allRoles.length
? allRoles
: [normalizeRole((info.roles || [])[0]) || "paciente"]
);
console.log("[buildSessionUser] Roles detectados:", {
fromMetadata: rolesFromMetadata,
fromArray: rolesNormalized,
allRoles,
primaryRole,
});
const base = {
id: info.user?.id || "",
nome:
info.profile?.full_name ||
info.user?.email?.split("@")[0] ||
"Usuário",
email: info.user?.email,
role: primaryRole,
roles: allRoles,
permissions,
} as SessionUserBase;
if (primaryRole === "medico") {
return { ...base, role: "medico" } as MedicoUser;
}
if (primaryRole === "secretaria") {
return { ...base, role: "secretaria" } as SecretariaUser;
}
if (primaryRole === "admin") {
return { ...base, role: "admin" } as AdminUser;
}
if (primaryRole === "gestor") {
return { ...base, role: "gestor" } as SessionUser;
}
return { ...base, role: "paciente" } as PacienteUser;
},
[]
);
// LEGADO: manter até que todos os usuários passem a existir no auth real
const loginSecretaria = useCallback(
async (email: string, senha: string) => {
// Mock atual: validar contra credenciais fixas (pode evoluir para authService.login)
if (email === "secretaria@clinica.com" && senha === "secretaria123") {
const newUser: SecretariaUser = {
id: "sec-1",
nome: "Secretária",
email,
role: "secretaria",
roles: ["secretaria"],
permissions: {},
};
setUser(newUser);
persist({ user: newUser, savedAt: new Date().toISOString() });
return true;
}
toast.error("Credenciais inválidas");
return false;
},
[persist]
);
// LEGADO: usa service de médicos sem validar senha real (apenas existência)
const loginMedico = useCallback(
async (email: string, senha: string) => {
const resp = await doctorService.loginMedico(email, senha);
if (!resp.success || !resp.data) {
toast.error(resp.error || "Erro ao autenticar médico");
return false;
}
const m: Medico = resp.data;
const newUser: MedicoUser = {
id: m.id,
nome: m.nome,
email: m.email,
role: "medico",
crm: m.crm,
especialidade: m.especialidade,
roles: ["medico"],
permissions: {},
};
setUser(newUser);
persist({ user: newUser, savedAt: new Date().toISOString() });
toast.success(`Bem-vindo(a) Dr(a). ${m.nome}`);
return true;
},
[persist]
);
// Fluxo unificado real usando authService
const loginComEmailSenha = useCallback(
async (email: string, senha: string) => {
try {
const loginResp = await authService.login({ email, password: senha });
// Fetch full user info with roles and permissions
const userInfo = await userService.getUserInfo();
// Build session user from full user info
const sessionUser = buildSessionUser({
access_token: loginResp.access_token,
refresh_token: loginResp.refresh_token,
user: userInfo.user,
roles: userInfo.roles,
permissions: userInfo.permissions,
profile: userInfo.profile
? { full_name: userInfo.profile.full_name }
: undefined,
} as UserInfoFullResponse);
setUser(sessionUser);
persist({
user: sessionUser,
savedAt: new Date().toISOString(),
token: loginResp.access_token,
refreshToken: loginResp.refresh_token,
});
return true;
} catch (error) {
console.error("[AuthContext] Login falhou:", error);
toast.error("Falha no login");
return false;
}
},
[persist, buildSessionUser]
);
// Para paciente, aproveitamos fluxo existente: quando o paciente já foi validado externamente no loginPaciente
const loginPaciente = useCallback(
async (paciente: { id: string; nome: string; email?: string }) => {
console.log("[AuthContext] loginPaciente chamado com:", paciente);
const newUser: PacienteUser = {
id: paciente.id,
nome: paciente.nome,
email: paciente.email,
role: "paciente",
pacienteId: paciente.id,
roles: ["paciente"],
permissions: {},
};
console.log("[AuthContext] Usuário criado:", newUser);
setUser(newUser);
persist({ user: newUser, savedAt: new Date().toISOString() });
// Bridge para páginas que ainda leem localStorage("pacienteLogado")
try {
const legacy = {
_id: paciente.id,
nome: paciente.nome,
email: paciente.email ?? "",
cpf: "",
telefone: "",
};
localStorage.setItem("pacienteLogado", JSON.stringify(legacy));
} catch {
// ignore
}
console.log("[AuthContext] Usuário persistido no localStorage");
toast.success(`Bem-vindo(a), ${paciente.nome}`);
return true;
},
[persist]
);
const logout = useCallback(async () => {
console.log("[AuthContext] Iniciando logout...");
try {
await authService.logout(); // Returns void on success
console.log("[AuthContext] Logout remoto bem-sucedido");
} catch (e) {
console.warn(
"[AuthContext] Erro ao executar logout remoto (continuando limpeza local)",
e
);
} finally {
// Limpa contexto local
console.log("[AuthContext] Limpando estado local...");
setUser(null);
clearPersisted();
try {
localStorage.removeItem("pacienteLogado");
} catch {
// ignore
}
console.log("[AuthContext] Logout completo - usuário removido do estado");
}
}, [clearPersisted]);
const refreshSession = useCallback(async () => {
// Futuro: usar refresh token real. Agora apenas revalida estrutura.
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return;
const parsed = JSON.parse(raw) as PersistedSession;
if (!parsed?.user?.role) return;
setUser(parsed.user);
} catch {
// ignorar
}
}, []);
const value: AuthContextValue = useMemo(
() => ({
user,
role: user?.role ?? null,
roles: user?.roles || (user?.role ? [user.role] : []),
permissions: user?.permissions || {},
isAuthenticated: !!user,
loading,
loginSecretaria,
loginMedico,
loginComEmailSenha,
loginPaciente,
logout,
refreshSession,
}),
[
user,
loading,
loginSecretaria,
loginMedico,
loginComEmailSenha,
loginPaciente,
logout,
refreshSession,
]
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
export default AuthContext;