424 lines
13 KiB
TypeScript
424 lines
13 KiB
TypeScript
import React, {
|
|
createContext,
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
} from "react";
|
|
import toast from "react-hot-toast";
|
|
import medicoService, { type Medico } from "../services/medicoService";
|
|
import authService, {
|
|
type UserInfoFullResponse,
|
|
} from "../services/authService"; // tokens + user-info
|
|
// 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);
|
|
|
|
// Restaurar sessão do localStorage e verificar token
|
|
useEffect(() => {
|
|
const restoreSession = async () => {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
if (raw) {
|
|
const parsed = JSON.parse(raw) as PersistedSession;
|
|
if (parsed?.user?.role) {
|
|
console.log("[AuthContext] Restaurando sessão:", parsed.user);
|
|
|
|
// Verificar se há tokens válidos salvos
|
|
if (parsed.token) {
|
|
console.log("[AuthContext] Restaurando tokens no tokenStore");
|
|
// Restaurar tokens no tokenStore
|
|
const tokenStore = (await import("../services/tokenStore"))
|
|
.default;
|
|
tokenStore.setTokens(parsed.token, parsed.refreshToken);
|
|
} else {
|
|
console.warn("[AuthContext] Sessão encontrada mas sem token. Verificando tokenStore...");
|
|
// Verificar se há token no tokenStore (pode ter sido salvo diretamente)
|
|
const tokenStore = (await import("../services/tokenStore"))
|
|
.default;
|
|
const existingToken = tokenStore.getAccessToken();
|
|
if (existingToken) {
|
|
console.log("[AuthContext] Token encontrado no tokenStore, mantendo sessão");
|
|
} else {
|
|
console.warn("[AuthContext] Nenhum token encontrado. Sessão pode estar inválida.");
|
|
}
|
|
}
|
|
setUser(parsed.user);
|
|
}
|
|
} else {
|
|
console.log("[AuthContext] Nenhuma sessão salva encontrada");
|
|
}
|
|
} catch (error) {
|
|
console.error("[AuthContext] Erro ao restaurar sessão:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
void restoreSession();
|
|
}, []);
|
|
|
|
const persist = useCallback((session: PersistedSession) => {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(session));
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}, []);
|
|
|
|
const clearPersisted = useCallback(() => {
|
|
try {
|
|
localStorage.removeItem(STORAGE_KEY);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}, []);
|
|
|
|
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 => {
|
|
console.log(
|
|
"[buildSessionUser] info recebido:",
|
|
JSON.stringify(info, null, 2)
|
|
);
|
|
const rolesNormalized = (info.roles || [])
|
|
.map(normalizeRole)
|
|
.filter(Boolean) as UserRole[];
|
|
console.log("[buildSessionUser] roles normalizadas:", rolesNormalized);
|
|
const permissions = info.permissions || {};
|
|
const primaryRole = pickPrimaryRole(
|
|
rolesNormalized.length
|
|
? rolesNormalized
|
|
: [normalizeRole((info.roles || [])[0]) || "paciente"]
|
|
);
|
|
console.log("[buildSessionUser] primaryRole escolhida:", 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: rolesNormalized,
|
|
permissions,
|
|
} as SessionUserBase;
|
|
console.log("[buildSessionUser] SessionUser final:", base);
|
|
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() });
|
|
toast.success("Login realizado");
|
|
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 medicoService.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 + endpoint user-info para mapear role dinâmica
|
|
const loginComEmailSenha = useCallback(
|
|
async (email: string, senha: string) => {
|
|
console.log("[AuthContext] Iniciando login para:", email);
|
|
const loginResp = await authService.login({ email, password: senha });
|
|
console.log("[AuthContext] Resposta login:", loginResp);
|
|
|
|
if (!loginResp.success || !loginResp.data) {
|
|
console.error("[AuthContext] Login falhou:", loginResp.error);
|
|
toast.error(loginResp.error || "Falha no login");
|
|
return false;
|
|
}
|
|
|
|
console.log("[AuthContext] Token recebido, buscando user-info...");
|
|
// Buscar user-info para descobrir papel
|
|
const infoResp = await authService.getUserInfo();
|
|
console.log("[AuthContext] Resposta user-info:", infoResp);
|
|
|
|
if (!infoResp.success || !infoResp.data) {
|
|
console.error(
|
|
"[AuthContext] Falha ao obter user-info:",
|
|
infoResp.error
|
|
);
|
|
toast.error(infoResp.error || "Falha ao obter user-info");
|
|
return false;
|
|
}
|
|
|
|
const sessionUser = buildSessionUser(infoResp.data);
|
|
console.log("[AuthContext] Usuário da sessão criado:", sessionUser);
|
|
setUser(sessionUser);
|
|
persist({
|
|
user: sessionUser,
|
|
savedAt: new Date().toISOString(),
|
|
token: loginResp.data.access_token,
|
|
refreshToken: loginResp.data.refresh_token,
|
|
});
|
|
console.log("[AuthContext] Login completo!");
|
|
toast.success("Login realizado");
|
|
return true;
|
|
},
|
|
[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 () => {
|
|
try {
|
|
const resp = await authService.logout(); // chama /auth/v1/logout (204 esperado)
|
|
if (!resp.success && resp.error) {
|
|
toast.error(`Falha no logout remoto: ${resp.error}`);
|
|
} else {
|
|
toast.success("Sessão encerrada no servidor");
|
|
}
|
|
} catch (e) {
|
|
console.warn("Erro inesperado ao executar logout remoto", e);
|
|
toast("Logout local (falha remota)");
|
|
} finally {
|
|
// Limpa contexto local
|
|
setUser(null);
|
|
clearPersisted();
|
|
authService.clearLocalAuth();
|
|
try {
|
|
localStorage.removeItem("pacienteLogado");
|
|
} catch {
|
|
// ignore
|
|
}
|
|
// Modelo somente Supabase: nenhum token técnico para invalidar
|
|
}
|
|
}, [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;
|