riseup-squad18/src/context/AuthContext.tsx
guisilvagomes 3443e46ca3 feat: implementa chatbot AI, gerenciamento de disponibilidade médica, visualização de laudos e melhorias no painel da secretária
- Adiciona chatbot AI com interface responsiva e posicionamento otimizado
- Implementa gerenciamento completo de disponibilidade e exceções médicas
- Adiciona modal de visualização detalhada de laudos no painel do paciente
- Corrige relatórios da secretária para mostrar nomes de médicos
- Implementa mensagem de boas-vindas personalizada com nome real
- Remove mensagens duplicadas de login
- Remove arquivo cleanup-deps.ps1 desnecessário
- Atualiza README com todas as novas funcionalidades
2025-11-05 16:51:33 -03:00

559 lines
17 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";
// 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";
interface PersistedSession {
user: SessionUser;
token?: string; // para quando integrar authService real
refreshToken?: string;
savedAt: string;
}
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;
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 sessionStr = JSON.stringify(session);
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
const rolesNormalized = (info.roles || [])
.map(normalizeRole)
.filter(Boolean) as UserRole[];
const permissions = info.permissions || {};
const primaryRole = pickPrimaryRole(
rolesNormalized.length
? rolesNormalized
: [normalizeRole((info.roles || [])[0]) || "paciente"]
);
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: rolesNormalized,
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;