forked from RiseUP/riseup-squad18
- 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
666 lines
20 KiB
TypeScript
666 lines
20 KiB
TypeScript
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;
|