feat(api): Ajustar fluxo de criação de usuários e mensagens de erro

- Removido fallback Direct Auth no frontend

- Removida tentativa de atribuir role no cliente

- Mensagens de erro aprimoradas para 'failed to assign user role' e email duplicado

- Atualizados formulários de médico e paciente para instruções claras
This commit is contained in:
M-Gabrielly 2025-10-10 16:40:04 -03:00
parent e5df9918b2
commit aeed6f3f0d
10 changed files with 783 additions and 478 deletions

View File

@ -65,13 +65,6 @@ export function CredentialsDialog({
</AlertDescription>
</Alert>
<Alert className="bg-blue-50 border-blue-200">
<AlertDescription className="text-blue-900">
<strong>📧 Confirme o email:</strong> Um email de confirmação foi enviado para <strong>{email}</strong>.
O {userType} deve clicar no link de confirmação antes de fazer o primeiro login.
</AlertDescription>
</Alert>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="email">Email de Acesso</Label>
@ -129,30 +122,6 @@ export function CredentialsDialog({
</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-md p-3 text-sm text-blue-900">
<strong>Próximos passos:</strong>
<ol className="list-decimal list-inside mt-2 space-y-1">
<li>Compartilhe estas credenciais com o {userType}</li>
<li>
<strong className="text-blue-700">O {userType} deve confirmar o email</strong> clicando no link enviado para{" "}
<strong>{email}</strong> (verifique também a pasta de spam)
</li>
<li>
Após confirmar o email, o {userType} deve acessar:{" "}
<code className="bg-blue-100 px-1 py-0.5 rounded text-xs font-mono">
{userType === "médico" ? "/login" : "/login-paciente"}
</code>
</li>
<li>
Após o login, terá acesso à área:{" "}
<code className="bg-blue-100 px-1 py-0.5 rounded text-xs font-mono">
{userType === "médico" ? "/profissional" : "/paciente"}
</code>
</li>
<li>Recomende trocar a senha no primeiro acesso</li>
</ol>
</div>
<DialogFooter className="flex-col sm:flex-row gap-2">
<Button
type="button"

View File

@ -22,10 +22,10 @@ import {
listarAnexosMedico,
adicionarAnexoMedico,
removerAnexoMedico,
MedicoInput, // 👈 importado do lib/api
Medico, // 👈 adicionado import do tipo Medico
criarUsuarioMedico,
CreateUserWithPasswordResponse,
MedicoInput,
Medico,
criarUsuario,
gerarSenhaAleatoria,
} from "@/lib/api";
;
@ -155,9 +155,8 @@ export function DoctorRegistrationForm({
const [serverAnexos, setServerAnexos] = useState<any[]>([]);
// Estados para o dialog de credenciais
const [showCredentials, setShowCredentials] = useState(false);
const [credentials, setCredentials] = useState<CreateUserWithPasswordResponse | null>(null);
const [savedDoctor, setSavedDoctor] = useState<Medico | null>(null);
const [dialogOpen, setDialogOpen] = useState(false);
const [tempCredentials, setTempCredentials] = useState<{ email: string; password: string } | null>(null);
const title = useMemo(() => (mode === "create" ? "Cadastro de Médico" : "Editar Médico"), [mode]);
@ -337,150 +336,157 @@ function setField<T extends keyof FormData>(k: T, v: FormData[T]) {
return Object.keys(e).length === 0;
}
function toPayload(): MedicoInput {
// Converte dd/MM/yyyy para ISO (yyyy-MM-dd) se possível
let isoDate: string | null = null;
try {
const parts = String(form.data_nascimento).split(/\D+/).filter(Boolean);
if (parts.length === 3) {
const [d, m, y] = parts;
const date = new Date(Number(y), Number(m) - 1, Number(d));
if (!isNaN(date.getTime())) {
isoDate = date.toISOString().slice(0, 10);
}
}
} catch {}
return {
user_id: null,
crm: form.crm || "",
crm_uf: form.estado_crm || "",
specialty: form.especialidade || "",
full_name: form.full_name || "",
cpf: form.cpf || "",
email: form.email || "",
phone_mobile: form.celular || "",
phone2: form.telefone || null,
cep: form.cep || "",
street: form.logradouro || "",
number: form.numero || "",
complement: form.complemento || undefined,
neighborhood: form.bairro || undefined,
city: form.cidade || "",
state: form.estado || "",
birth_date: isoDate,
rg: form.rg || null,
active: true,
created_by: null,
updated_by: null,
};
}
async function handleSubmit(ev: React.FormEvent) {
ev.preventDefault();
console.log("Submitting the form..."); // Verifique se a função está sendo chamada
if (!validateLocal()) {
console.log("Validation failed");
return; // Se a validação falhar, saia da função.
}
if (!validateLocal()) return;
setSubmitting(true);
setErrors((e) => ({ ...e, submit: "" }));
const payload: MedicoInput = {
user_id: null,
crm: form.crm || "",
crm_uf: form.estado_crm || "",
specialty: form.especialidade || "",
full_name: form.full_name || "",
cpf: form.cpf || "",
email: form.email || "",
phone_mobile: form.celular || "",
phone2: form.telefone || null,
cep: form.cep || "",
street: form.logradouro || "",
number: form.numero || "",
complement: form.complemento || undefined,
neighborhood: form.bairro || undefined,
city: form.cidade || "",
state: form.estado || "",
// converte dd/MM/yyyy para ISO
birth_date: (() => {
try {
const parts = String(form.data_nascimento).split(/\D+/).filter(Boolean);
if (parts.length === 3) {
const [d, m, y] = parts;
const date = new Date(Number(y), Number(m) - 1, Number(d));
if (!isNaN(date.getTime())) return date.toISOString().slice(0, 10);
}
} catch {}
return null;
})(),
rg: form.rg || null,
active: true,
created_by: null,
updated_by: null,
};
// Validação dos campos obrigatórios
const requiredFields = ['crm', 'crm_uf', 'specialty', 'full_name', 'cpf', 'email', 'phone_mobile', 'cep', 'street', 'number', 'city', 'state'];
const missingFields = requiredFields.filter(field => !payload[field as keyof MedicoInput]);
if (missingFields.length > 0) {
console.warn('⚠️ Campos obrigatórios vazios:', missingFields);
}
console.log("📤 Payload being sent:", payload);
console.log("🔧 Mode:", mode, "DoctorId:", doctorId);
setErrors({});
try {
if (mode === "edit" && !doctorId) {
throw new Error("ID do médico não fornecido para edição");
}
const saved = mode === "create"
? await criarMedico(payload)
: await atualizarMedico(String(doctorId), payload);
console.log("✅ Médico salvo com sucesso:", saved);
// Se for criação de novo médico e tiver email válido, cria usuário
if (mode === "create" && form.email && form.email.includes('@')) {
console.log("🔐 Iniciando criação de usuário para o médico...");
console.log("📧 Email:", form.email);
console.log("👤 Nome:", form.full_name);
console.log("📱 Telefone:", form.celular);
try {
const userCredentials = await criarUsuarioMedico({
email: form.email,
full_name: form.full_name,
phone_mobile: form.celular,
});
console.log("✅ Usuário criado com sucesso!", userCredentials);
console.log("🔑 Senha gerada:", userCredentials.password);
// Armazena as credenciais e mostra o dialog
setCredentials(userCredentials);
setShowCredentials(true);
setSavedDoctor(saved); // Salva médico para chamar onSaved depois
console.log("📋 Credenciais definidas, dialog deve aparecer!");
// NÃO chama onSaved aqui! Isso fecha o formulário.
// O dialog vai chamar onSaved quando o usuário fechar
setSubmitting(false);
return; // ← IMPORTANTE: Impede que o código abaixo seja executado
} catch (userError: any) {
console.error("❌ ERRO ao criar usuário:", userError);
console.error("📋 Stack trace:", userError?.stack);
const errorMessage = userError?.message || "Erro desconhecido";
console.error("💬 Mensagem:", errorMessage);
// Mostra erro mas fecha o formulário normalmente
alert(`Médico cadastrado com sucesso!\n\n⚠ Porém, houve erro ao criar usuário de acesso:\n${errorMessage}\n\nVerifique os logs do console (F12) para mais detalhes.`);
// Fecha o formulário mesmo com erro na criação de usuário
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
onSaved?.(saved);
if (inline) onClose?.();
else onOpenChange?.(false);
setSubmitting(false);
return;
}
} else {
console.log("⚠️ Não criará usuário. Motivo:");
console.log(" - Mode:", mode);
console.log(" - Email:", form.email);
console.log(" - Tem @:", form.email?.includes('@'));
// Se não for criar usuário, fecha normalmente
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
if (mode === "edit") {
if (!doctorId) throw new Error("ID do médico não fornecido para edição");
const payload = toPayload();
const saved = await atualizarMedico(String(doctorId), payload);
onSaved?.(saved);
alert("Médico atualizado com sucesso!");
if (inline) onClose?.();
else onOpenChange?.(false);
setSubmitting(false);
} else {
// --- NOVA LÓGICA DE CRIAÇÃO ---
const medicoPayload = toPayload();
const savedDoctorProfile = await criarMedico(medicoPayload);
console.log("✅ Perfil do médico criado:", savedDoctorProfile);
if (form.email && form.email.includes('@')) {
const tempPassword = gerarSenhaAleatoria();
const userInput = {
email: form.email,
password: tempPassword,
full_name: form.full_name,
phone: form.celular,
role: 'medico' as const,
};
console.log("🔐 Criando usuário de autenticação com payload:", userInput);
try {
const userResponse = await criarUsuario(userInput);
if (userResponse.success && userResponse.user) {
console.log("✅ Usuário de autenticação criado:", userResponse.user);
// Mostra credenciais (NÃO fecha o formulário ainda)
setTempCredentials({ email: form.email, password: tempPassword });
setDialogOpen(true);
// Limpa formulário mas NÃO fecha ainda - fechará quando o dialog de credenciais fechar
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
onSaved?.(savedDoctorProfile);
// NÃO chama onClose ou onOpenChange aqui - deixa o dialog de credenciais fazer isso
return;
} else {
throw new Error((userResponse as any).message || "Falhou ao criar o usuário de acesso.");
}
} catch (userError: any) {
console.error("❌ Erro ao criar usuário via função server-side:", userError);
// Mensagem de erro específica para email duplicado
const errorMsg = userError?.message || String(userError);
if (errorMsg.toLowerCase().includes('already registered') ||
errorMsg.toLowerCase().includes('já está cadastrado') ||
errorMsg.toLowerCase().includes('já existe')) {
alert(
`⚠️ Este email já está cadastrado no sistema.\n\n` +
`✅ O perfil do médico foi salvo com sucesso.\n\n` +
`Para criar acesso ao sistema, use um email diferente ou recupere a senha do email existente.`
);
} else if (errorMsg.toLowerCase().includes('failed to assign user role') ||
errorMsg.toLowerCase().includes('atribuir permissões')) {
alert(
`⚠️ PROBLEMA NA CONFIGURAÇÃO DO SISTEMA\n\n` +
`✅ O perfil do médico foi salvo com sucesso.\n\n` +
`❌ Porém, houve falha ao atribuir permissões de acesso.\n\n` +
`Esse erro indica que a Edge Function do Supabase não está configurada corretamente.\n\n` +
`Entre em contato com o administrador do sistema para:\n` +
`1. Verificar se a service role key está configurada\n` +
`2. Verificar as permissões da tabela user_roles\n` +
`3. Revisar o código da Edge Function create-user`
);
} else {
alert(
`✅ Médico cadastrado com sucesso!\n\n` +
`⚠️ Porém houve um problema ao criar o acesso:\n${errorMsg}\n\n` +
`O cadastro do médico foi salvo, mas será necessário criar o acesso manualmente.`
);
}
// Limpa formulário e fecha
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
onSaved?.(savedDoctorProfile);
if (inline) onClose?.();
else onOpenChange?.(false);
return;
}
} else {
alert("Médico cadastrado com sucesso (sem usuário de acesso - email não fornecido).");
onSaved?.(savedDoctorProfile);
if (inline) onClose?.();
else onOpenChange?.(false);
}
}
} catch (err: any) {
console.error("❌ Erro ao salvar médico:", err);
console.error("❌ Detalhes do erro:", {
message: err?.message,
status: err?.status,
stack: err?.stack
});
setErrors((e) => ({ ...e, submit: err?.message || "Erro ao salvar médico" }));
console.error("❌ Erro no handleSubmit:", err);
// Exibe mensagem amigável ao usuário
const userMessage = err?.message?.includes("toPayload")
? "Erro ao processar os dados do formulário. Por favor, verifique os campos e tente novamente."
: err?.message || "Erro ao salvar médico. Por favor, tente novamente.";
setErrors({ submit: userMessage });
} finally {
setSubmitting(false);
}
@ -1036,32 +1042,23 @@ if (missingFields.length > 0) {
<div className="space-y-6">{content}</div>
{/* Dialog de credenciais */}
{credentials && (
{tempCredentials && (
<CredentialsDialog
open={showCredentials}
open={dialogOpen}
onOpenChange={(open) => {
console.log("🔄 CredentialsDialog (inline) onOpenChange:", open);
setShowCredentials(open);
setDialogOpen(open);
if (!open) {
console.log("🔄 Dialog fechando - chamando onSaved e limpando formulário");
// Chama onSaved com o médico salvo
if (savedDoctor) {
console.log("✅ Chamando onSaved com médico:", savedDoctor.id);
onSaved?.(savedDoctor);
// Quando o dialog de credenciais fecha, fecha o formulário também
setTempCredentials(null);
if (inline) {
onClose?.();
} else {
onOpenChange?.(false);
}
// Limpa o formulário e fecha
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
setCredentials(null);
setSavedDoctor(null);
onClose?.();
}
}}
email={credentials.email}
password={credentials.password}
email={tempCredentials.email}
password={tempCredentials.password}
userName={form.full_name}
userType="médico"
/>
@ -1084,32 +1081,18 @@ if (missingFields.length > 0) {
</Dialog>
{/* Dialog de credenciais */}
{credentials && (
{tempCredentials && (
<CredentialsDialog
open={showCredentials}
open={dialogOpen}
onOpenChange={(open) => {
console.log("🔄 CredentialsDialog (dialog mode) onOpenChange:", open);
setShowCredentials(open);
setDialogOpen(open);
if (!open) {
console.log("🔄 Dialog fechando - chamando onSaved e fechando modal principal");
// Chama onSaved com o médico salvo
if (savedDoctor) {
console.log("✅ Chamando onSaved com médico:", savedDoctor.id);
onSaved?.(savedDoctor);
}
// Limpa o formulário e fecha o modal principal
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
setCredentials(null);
setSavedDoctor(null);
setTempCredentials(null);
onOpenChange?.(false);
}
}}
email={credentials.email}
password={credentials.password}
email={tempCredentials.email}
password={tempCredentials.password}
userName={form.full_name}
userType="médico"
/>

View File

@ -1,4 +1,3 @@
"use client";
import { useEffect, useMemo, useState } from "react";
@ -18,7 +17,6 @@ import {
Paciente,
PacienteInput,
buscarCepAPI,
criarPaciente,
atualizarPaciente,
uploadFotoPaciente,
removerFotoPaciente,
@ -26,15 +24,15 @@ import {
listarAnexos,
removerAnexo,
buscarPacientePorId,
criarUsuarioPaciente,
CreateUserWithPasswordResponse,
criarUsuario,
gerarSenhaAleatoria,
CreateUserResponse,
criarPaciente,
} from "@/lib/api";
import { validarCPFLocal } from "@/lib/utils";
import { verificarCpfDuplicado } from "@/lib/api";
import { CredentialsDialog } from "@/components/credentials-dialog";
import { CredentialsDialog } from "@/components/credentials-dialog";
type Mode = "create" | "edit";
@ -55,7 +53,7 @@ type FormData = {
cpf: string;
rg: string;
sexo: string;
birth_date: string; // 👈 corrigido
birth_date: string;
email: string;
telefone: string;
cep: string;
@ -76,7 +74,7 @@ const initial: FormData = {
cpf: "",
rg: "",
sexo: "",
birth_date: "", // 👈 corrigido
birth_date: "",
email: "",
telefone: "",
cep: "",
@ -90,8 +88,6 @@ const initial: FormData = {
anexos: [],
};
export function PatientRegistrationForm({
open = true,
onOpenChange,
@ -110,13 +106,11 @@ export function PatientRegistrationForm({
const [serverAnexos, setServerAnexos] = useState<any[]>([]);
// Estados para o dialog de credenciais
const [showCredentials, setShowCredentials] = useState(false);
const [credentials, setCredentials] = useState<CreateUserWithPasswordResponse | null>(null);
const [savedPatient, setSavedPatient] = useState<Paciente | null>(null);
const [dialogOpen, setDialogOpen] = useState(false);
const [tempCredentials, setTempCredentials] = useState<{ email: string; password: string } | null>(null);
const title = useMemo(() => (mode === "create" ? "Cadastro de Paciente" : "Editar Paciente"), [mode]);
useEffect(() => {
async function load() {
if (mode !== "edit" || patientId == null) return;
@ -125,26 +119,26 @@ export function PatientRegistrationForm({
const p = await buscarPacientePorId(String(patientId));
console.log("[PatientForm] Dados recebidos:", p);
setForm((s) => ({
...s,
nome: p.full_name || "", // 👈 trocar nome → full_name
nome_social: p.social_name || "",
cpf: p.cpf || "",
rg: p.rg || "",
sexo: p.sex || "",
birth_date: p.birth_date ? (() => {
try { return format(parseISO(String(p.birth_date)), 'dd/MM/yyyy'); } catch { return String(p.birth_date); }
})() : "",
telefone: p.phone_mobile || "",
email: p.email || "",
cep: p.cep || "",
logradouro: p.street || "",
numero: p.number || "",
complemento: p.complement || "",
bairro: p.neighborhood || "",
cidade: p.city || "",
estado: p.state || "",
observacoes: p.notes || "",
}));
...s,
nome: p.full_name || "",
nome_social: p.social_name || "",
cpf: p.cpf || "",
rg: p.rg || "",
sexo: p.sex || "",
birth_date: p.birth_date ? (() => {
try { return format(parseISO(String(p.birth_date)), 'dd/MM/yyyy'); } catch { return String(p.birth_date); }
})() : "",
telefone: p.phone_mobile || "",
email: p.email || "",
cep: p.cep || "",
logradouro: p.street || "",
numero: p.number || "",
complemento: p.complement || "",
bairro: p.neighborhood || "",
cidade: p.city || "",
estado: p.state || "",
observacoes: p.notes || "",
}));
const ax = await listarAnexos(String(patientId)).catch(() => []);
setServerAnexos(Array.isArray(ax) ? ax : []);
@ -197,181 +191,179 @@ export function PatientRegistrationForm({
const e: Record<string, string> = {};
if (!form.nome.trim()) e.nome = "Nome é obrigatório";
if (!form.cpf.trim()) e.cpf = "CPF é obrigatório";
if (mode === 'create' && !form.email.trim()) e.email = "Email é obrigatório para criar um usuário";
setErrors(e);
return Object.keys(e).length === 0;
}
function toPayload(): PacienteInput {
// converte dd/MM/yyyy para ISO (yyyy-MM-dd) se possível
let isoDate: string | null = null;
try {
const parts = String(form.birth_date).split(/\D+/).filter(Boolean);
if (parts.length === 3) {
const [d, m, y] = parts;
const date = new Date(Number(y), Number(m) - 1, Number(d));
if (!isNaN(date.getTime())) {
isoDate = date.toISOString().slice(0, 10);
let isoDate: string | null = null;
try {
const parts = String(form.birth_date).split(/\D+/).filter(Boolean);
if (parts.length === 3) {
const [d, m, y] = parts;
const date = new Date(Number(y), Number(m) - 1, Number(d));
if (!isNaN(date.getTime())) {
isoDate = date.toISOString().slice(0, 10);
}
}
}
} catch {}
return {
full_name: form.nome, // 👈 troca 'nome' por 'full_name'
social_name: form.nome_social || null,
cpf: form.cpf,
rg: form.rg || null,
sex: form.sexo || null,
birth_date: isoDate, // enviar ISO ou null
phone_mobile: form.telefone || null,
email: form.email || null,
cep: form.cep || null,
street: form.logradouro || null,
number: form.numero || null,
complement: form.complemento || null,
neighborhood: form.bairro || null,
city: form.cidade || null,
state: form.estado || null,
notes: form.observacoes || null,
};
}
} catch {}
return {
full_name: form.nome,
social_name: form.nome_social || null,
cpf: form.cpf,
rg: form.rg || null,
sex: form.sexo || null,
birth_date: isoDate,
phone_mobile: form.telefone || null,
email: form.email || null,
cep: form.cep || null,
street: form.logradouro || null,
number: form.numero || null,
complement: form.complemento || null,
neighborhood: form.bairro || null,
city: form.cidade || null,
state: form.estado || null,
notes: form.observacoes || null,
};
}
async function handleSubmit(ev: React.FormEvent) {
ev.preventDefault();
if (!validateLocal()) return;
try {
// 1) validação local
if (!validarCPFLocal(form.cpf)) {
setErrors((e) => ({ ...e, cpf: "CPF inválido" }));
return;
}
// 2) checar duplicidade no banco (apenas se criando novo paciente)
if (mode === "create") {
const existe = await verificarCpfDuplicado(form.cpf);
if (existe) {
setErrors((e) => ({ ...e, cpf: "CPF já cadastrado no sistema" }));
if (!validarCPFLocal(form.cpf)) {
setErrors((e) => ({ ...e, cpf: "CPF inválido" }));
return;
}
if (mode === "create") {
const existe = await verificarCpfDuplicado(form.cpf);
if (existe) {
setErrors((e) => ({ ...e, cpf: "CPF já cadastrado no sistema" }));
return;
}
}
} catch (err) {
console.error("Erro ao validar CPF", err);
setErrors({ submit: "Erro ao validar CPF." });
return;
}
}
} catch (err) {
console.error("Erro ao validar CPF", err);
}
setSubmitting(true);
try {
const payload = toPayload();
let saved: Paciente;
if (mode === "create") {
saved = await criarPaciente(payload);
} else {
if (mode === "edit") {
if (patientId == null) throw new Error("Paciente inexistente para edição");
saved = await atualizarPaciente(String(patientId), payload);
}
if (form.photo && saved?.id) {
try {
await uploadFotoPaciente(saved.id, form.photo);
} catch {}
}
if (form.anexos.length && saved?.id) {
for (const f of form.anexos) {
try {
await adicionarAnexo(saved.id, f);
} catch {}
}
}
// Se for criação de novo paciente e tiver email válido, cria usuário
if (mode === "create" && form.email && form.email.includes('@')) {
console.log("🔐 Iniciando criação de usuário para o paciente...");
console.log("📧 Email:", form.email);
console.log("👤 Nome:", form.nome);
console.log("📱 Telefone:", form.telefone);
const payload = toPayload();
const saved = await atualizarPaciente(String(patientId), payload);
onSaved?.(saved);
alert("Paciente atualizado com sucesso!");
try {
const userCredentials = await criarUsuarioPaciente({
email: form.email,
full_name: form.nome,
phone_mobile: form.telefone,
});
console.log("✅ Usuário criado com sucesso!", userCredentials);
console.log("🔑 Senha gerada:", userCredentials.password);
// Armazena as credenciais e mostra o dialog
console.log("📋 Antes de setCredentials - credentials atual:", credentials);
console.log("📋 Antes de setShowCredentials - showCredentials atual:", showCredentials);
setCredentials(userCredentials);
setShowCredentials(true);
console.log("📋 Depois de set - credentials:", userCredentials);
console.log("📋 Depois de set - showCredentials: true");
console.log("📋 Modo inline?", inline);
console.log("📋 userCredentials completo:", JSON.stringify(userCredentials));
// Força re-render
setTimeout(() => {
console.log("⏰ Timeout - credentials:", credentials);
console.log("⏰ Timeout - showCredentials:", showCredentials);
}, 100);
console.log("📋 Credenciais definidas, dialog deve aparecer!");
// Salva o paciente para chamar onSaved depois
setSavedPatient(saved);
// ⚠️ NÃO chama onSaved aqui! O dialog vai chamar quando fechar.
// Se chamar agora, o formulário fecha e o dialog desaparece.
console.log("⚠️ NÃO chamando onSaved ainda - aguardando dialog fechar");
// RETORNA AQUI para não executar o código abaixo
return;
} catch (userError: any) {
console.error("❌ ERRO ao criar usuário:", userError);
console.error("📋 Stack trace:", userError?.stack);
const errorMessage = userError?.message || "Erro desconhecido";
console.error("<22> Mensagem:", errorMessage);
// Mostra erro mas fecha o formulário normalmente
alert(`Paciente cadastrado com sucesso!\n\n⚠ Porém, houve erro ao criar usuário de acesso:\n${errorMessage}\n\nVerifique os logs do console (F12) para mais detalhes.`);
// Fecha o formulário mesmo com erro na criação de usuário
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
if (inline) onClose?.();
else onOpenChange?.(false);
}
} else {
console.log("⚠️ Não criará usuário. Motivo:");
console.log(" - Mode:", mode);
console.log(" - Email:", form.email);
console.log(" - Tem @:", form.email?.includes('@'));
// Se não for criar usuário, fecha normalmente
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
if (inline) onClose?.();
else onOpenChange?.(false);
alert(mode === "create" ? "Paciente cadastrado!" : "Paciente atualizado!");
}
} else {
// --- NOVA LÓGICA DE CRIAÇÃO ---
const patientPayload = toPayload();
const savedPatientProfile = await criarPaciente(patientPayload);
console.log("✅ Perfil do paciente criado:", savedPatientProfile);
onSaved?.(saved);
if (form.email && form.email.includes('@')) {
const tempPassword = gerarSenhaAleatoria();
const userInput = {
email: form.email,
password: tempPassword,
full_name: form.nome,
phone: form.telefone,
role: 'user' as const,
};
console.log("🔐 Criando usuário de autenticação com payload:", userInput);
try {
const userResponse = await criarUsuario(userInput);
if (userResponse.success && userResponse.user) {
console.log("✅ Usuário de autenticação criado:", userResponse.user);
// Mostra credenciais (NÃO fecha o formulário ainda)
setTempCredentials({ email: form.email, password: tempPassword });
setDialogOpen(true);
// Limpa formulário mas NÃO fecha ainda - fechará quando o dialog de credenciais fechar
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
onSaved?.(savedPatientProfile);
// NÃO chama onClose ou onOpenChange aqui - deixa o dialog de credenciais fazer isso
return;
} else {
throw new Error((userResponse as any).message || "Falhou ao criar o usuário de acesso.");
}
} catch (userError: any) {
console.error("❌ Erro ao criar usuário via função server-side:", userError);
// Mensagem de erro específica para email duplicado
const errorMsg = userError?.message || String(userError);
if (errorMsg.toLowerCase().includes('already registered') ||
errorMsg.toLowerCase().includes('já está cadastrado') ||
errorMsg.toLowerCase().includes('já existe')) {
alert(
`⚠️ Este email já está cadastrado no sistema.\n\n` +
`✅ O perfil do paciente foi salvo com sucesso.\n\n` +
`Para criar acesso ao sistema, use um email diferente ou recupere a senha do email existente.`
);
} else if (errorMsg.toLowerCase().includes('failed to assign user role') ||
errorMsg.toLowerCase().includes('atribuir permissões')) {
alert(
`⚠️ PROBLEMA NA CONFIGURAÇÃO DO SISTEMA\n\n` +
`✅ O perfil do paciente foi salvo com sucesso.\n\n` +
`❌ Porém, houve falha ao atribuir permissões de acesso.\n\n` +
`Esse erro indica que a Edge Function do Supabase não está configurada corretamente.\n\n` +
`Entre em contato com o administrador do sistema para:\n` +
`1. Verificar se a service role key está configurada\n` +
`2. Verificar as permissões da tabela user_roles\n` +
`3. Revisar o código da Edge Function create-user`
);
} else {
alert(
`✅ Paciente cadastrado com sucesso!\n\n` +
`⚠️ Porém houve um problema ao criar o acesso:\n${errorMsg}\n\n` +
`O cadastro do paciente foi salvo, mas será necessário criar o acesso manualmente.`
);
}
// Limpa formulário e fecha
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
onSaved?.(savedPatientProfile);
if (inline) onClose?.();
else onOpenChange?.(false);
return;
}
} else {
alert("Paciente cadastrado com sucesso (sem usuário de acesso - email não fornecido).");
onSaved?.(savedPatientProfile);
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
if (inline) onClose?.();
else onOpenChange?.(false);
}
}
} catch (err: any) {
setErrors({ submit: err?.message || "Erro ao salvar paciente." });
console.error("❌ Erro no handleSubmit:", err);
// Exibe mensagem amigável ao usuário
const userMessage = err?.message?.includes("toPayload") || err?.message?.includes("is not defined")
? "Erro ao processar os dados do formulário. Por favor, verifique os campos e tente novamente."
: err?.message || "Erro ao salvar paciente. Por favor, tente novamente.";
setErrors({ submit: userMessage });
} finally {
setSubmitting(false);
}
@ -430,7 +422,6 @@ export function PatientRegistrationForm({
)}
<form onSubmit={handleSubmit} className="space-y-6">
{}
<Collapsible open={expanded.dados} onOpenChange={() => setExpanded((s) => ({ ...s, dados: !s.dados }))}>
<Card>
<CollapsibleTrigger asChild>
@ -449,7 +440,6 @@ export function PatientRegistrationForm({
<div className="flex items-center gap-4">
<div className="w-24 h-24 border-2 border-dashed border-muted-foreground rounded-lg flex items-center justify-center overflow-hidden">
{photoPreview ? (
<img src={photoPreview} alt="Preview" className="w-full h-full object-cover" />
) : (
<FileImage className="h-8 w-8 text-muted-foreground" />
@ -524,12 +514,10 @@ export function PatientRegistrationForm({
placeholder="dd/mm/aaaa"
value={form.birth_date}
onChange={(e) => {
// permita apenas números e '/'
const v = e.target.value.replace(/[^0-9\/]/g, "").slice(0, 10);
setField("birth_date", v);
}}
onBlur={() => {
// tenta formatar automaticamente se for uma data válida
const raw = form.birth_date;
const parts = raw.split(/\D+/).filter(Boolean);
if (parts.length === 3) {
@ -545,7 +533,6 @@ export function PatientRegistrationForm({
</Card>
</Collapsible>
{}
<Collapsible open={expanded.contato} onOpenChange={() => setExpanded((s) => ({ ...s, contato: !s.contato }))}>
<Card>
<CollapsibleTrigger asChild>
@ -562,6 +549,7 @@ export function PatientRegistrationForm({
<div className="space-y-2">
<Label>E-mail</Label>
<Input value={form.email} onChange={(e) => setField("email", e.target.value)} />
{errors.email && <p className="text-sm text-destructive">{errors.email}</p>}
</div>
<div className="space-y-2">
<Label>Telefone</Label>
@ -573,7 +561,6 @@ export function PatientRegistrationForm({
</Card>
</Collapsible>
{}
<Collapsible open={expanded.endereco} onOpenChange={() => setExpanded((s) => ({ ...s, endereco: !s.endereco }))}>
<Card>
<CollapsibleTrigger asChild>
@ -642,7 +629,6 @@ export function PatientRegistrationForm({
</Card>
</Collapsible>
{}
<Collapsible open={expanded.obs} onOpenChange={() => setExpanded((s) => ({ ...s, obs: !s.obs }))}>
<Card>
<CollapsibleTrigger asChild>
@ -709,7 +695,6 @@ export function PatientRegistrationForm({
</Card>
</Collapsible>
{}
<div className="flex justify-end gap-4 pt-6 border-t">
<Button type="button" variant="outline" onClick={() => (inline ? onClose?.() : onOpenChange?.(false))} disabled={isSubmitting}>
<XCircle className="mr-2 h-4 w-4" />
@ -729,36 +714,24 @@ export function PatientRegistrationForm({
<>
<div className="space-y-6">{content}</div>
{/* Debug */}
{console.log("🎨 RENDER inline - credentials:", credentials, "showCredentials:", showCredentials)}
{/* Dialog de credenciais */}
{credentials && (
{tempCredentials && (
<CredentialsDialog
open={showCredentials}
open={dialogOpen}
onOpenChange={(open) => {
console.log("🔄 CredentialsDialog onOpenChange:", open);
setShowCredentials(open);
setDialogOpen(open);
if (!open) {
console.log("🔄 Dialog fechando - chamando onSaved e limpando formulário");
// Chama onSaved com o paciente salvo
if (savedPatient) {
console.log("✅ Chamando onSaved com paciente:", savedPatient.id);
onSaved?.(savedPatient);
// Quando o dialog de credenciais fecha, fecha o formulário também
setTempCredentials(null);
if (inline) {
onClose?.();
} else {
onOpenChange?.(false);
}
// Limpa o formulário e fecha
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
setCredentials(null);
setSavedPatient(null);
onClose?.();
}
}}
email={credentials.email}
password={credentials.password}
email={tempCredentials.email}
password={tempCredentials.password}
userName={form.nome}
userType="paciente"
/>
@ -769,8 +742,6 @@ export function PatientRegistrationForm({
return (
<>
{console.log("🎨 RENDER dialog - credentials:", credentials, "showCredentials:", showCredentials)}
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
@ -783,26 +754,22 @@ export function PatientRegistrationForm({
</Dialog>
{/* Dialog de credenciais */}
{credentials && (
{tempCredentials && (
<CredentialsDialog
open={showCredentials}
open={dialogOpen}
onOpenChange={(open) => {
setShowCredentials(open);
setDialogOpen(open);
if (!open) {
// Quando fechar o dialog, limpa o formulário e fecha o modal principal
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
setCredentials(null);
setTempCredentials(null);
onOpenChange?.(false);
}
}}
email={credentials.email}
password={credentials.password}
email={tempCredentials.email}
password={tempCredentials.password}
userName={form.nome}
userType="paciente"
/>
)}
</>
);
}
}

View File

@ -1,7 +1,7 @@
// lib/api.ts
import { ENV_CONFIG } from '@/lib/env-config';
import { API_KEY } from '@/lib/config';
// Use ENV_CONFIG for SUPABASE URL and anon key in frontend
export type ApiOk<T = any> = {
success?: boolean;
@ -152,8 +152,7 @@ export type MedicoInput = {
// ===== CONFIG =====
const API_BASE =
process.env.NEXT_PUBLIC_API_BASE ?? "https://yuanqfswhberkoevtmfr.supabase.co";
const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? ENV_CONFIG.SUPABASE_URL;
const REST = `${API_BASE}/rest/v1`;
// Token salvo no browser (aceita auth_token ou token)
@ -170,8 +169,7 @@ function getAuthToken(): string | null {
// Cabeçalhos base
function baseHeaders(): Record<string, string> {
const h: Record<string, string> = {
apikey:
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ",
apikey: ENV_CONFIG.SUPABASE_ANON_KEY,
Accept: "application/json",
};
const jwt = getAuthToken();
@ -196,13 +194,31 @@ async function parse<T>(res: Response): Promise<T> {
if (!res.ok) {
console.error("[API ERROR]", res.url, res.status, json);
const code = (json && (json.error?.code || json.code)) ?? res.status;
const msg = (json && (json.error?.message || json.message)) ?? res.statusText;
const msg = (json && (json.error?.message || json.message || json.error)) ?? res.statusText;
// Mensagens amigáveis para erros comuns
let friendlyMessage = `${code}: ${msg}`;
let friendlyMessage = msg;
// Erros de criação de usuário
if (res.url?.includes('create-user')) {
if (msg?.includes('Failed to assign user role')) {
friendlyMessage = 'O usuário foi criado mas houve falha ao atribuir permissões. Entre em contato com o administrador do sistema para verificar as configurações da Edge Function.';
} else if (msg?.includes('already registered')) {
friendlyMessage = 'Este email já está cadastrado no sistema.';
} else if (msg?.includes('Invalid role')) {
friendlyMessage = 'Tipo de acesso inválido.';
} else if (msg?.includes('Missing required fields')) {
friendlyMessage = 'Campos obrigatórios não preenchidos.';
} else if (res.status === 401) {
friendlyMessage = 'Você não está autenticado. Faça login novamente.';
} else if (res.status === 403) {
friendlyMessage = 'Você não tem permissão para criar usuários.';
} else if (res.status === 500) {
friendlyMessage = 'Erro no servidor ao criar usuário. Entre em contato com o suporte.';
}
}
// Erro de CPF duplicado
if (code === '23505' && msg.includes('patients_cpf_key')) {
else if (code === '23505' && msg.includes('patients_cpf_key')) {
friendlyMessage = 'Já existe um paciente cadastrado com este CPF. Por favor, verifique se o paciente já está registrado no sistema ou use um CPF diferente.';
}
// Erro de email duplicado (paciente)
@ -221,6 +237,15 @@ async function parse<T>(res: Response): Promise<T> {
else if (code === '23505') {
friendlyMessage = 'Registro duplicado: já existe um cadastro com essas informações no sistema.';
}
// Erro de foreign key (registro referenciado em outra tabela)
else if (code === '23503') {
// Mensagem específica para pacientes com relatórios vinculados
if (msg && msg.toString().toLowerCase().includes('reports')) {
friendlyMessage = 'Não é possível excluir este paciente porque existem relatórios vinculados a ele. Exclua ou desvincule os relatórios antes de remover o paciente.';
} else {
friendlyMessage = 'Registro referenciado em outra tabela. Remova referências dependentes antes de tentar novamente.';
}
}
throw new Error(friendlyMessage);
}
@ -355,10 +380,43 @@ export async function atualizarPaciente(id: string | number, input: PacienteInpu
}
export async function excluirPaciente(id: string | number): Promise<void> {
// Antes de excluir, verificar se existem relatórios vinculados a este paciente
try {
// Import dinâmico para evitar ciclos durante bundling
const reportsMod = await import('./reports');
if (reportsMod && typeof reportsMod.listarRelatoriosPorPaciente === 'function') {
const rels = await reportsMod.listarRelatoriosPorPaciente(String(id)).catch(() => []);
if (Array.isArray(rels) && rels.length > 0) {
throw new Error('Não é possível excluir este paciente: existem relatórios vinculados. Remova ou reatribua esses relatórios antes de excluir o paciente.');
}
}
} catch (err) {
// Se a checagem falhar por algum motivo, apenas logamos e continuamos para a tentativa de exclusão
console.warn('[API] Falha ao checar relatórios vinculados antes da exclusão:', err);
}
const url = `${REST}/patients?id=eq.${id}`;
const res = await fetch(url, { method: "DELETE", headers: baseHeaders() });
await parse<any>(res);
}
/**
* Chama o endpoint server-side seguro para atribuir roles.
* Este endpoint usa a service role key e valida se o requisitante é administrador.
*/
export async function assignRoleServerSide(userId: string, role: string): Promise<any> {
const url = `/api/assign-role`;
const token = getAuthToken();
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ user_id: userId, role }),
});
return await parse<any>(res);
}
// ===== PACIENTES (Extra: verificação de CPF duplicado) =====
export async function verificarCpfDuplicado(cpf: string): Promise<boolean> {
const clean = (cpf || "").replace(/\D/g, "");
@ -586,15 +644,19 @@ export async function excluirMedico(id: string | number): Promise<void> {
}
// ===== USUÁRIOS =====
// Roles válidos conforme documentação API
export type UserRoleEnum = "admin" | "gestor" | "medico" | "secretaria" | "user";
export type UserRole = {
id: string;
user_id: string;
role: string;
role: UserRoleEnum;
created_at: string;
};
export async function listarUserRoles(): Promise<UserRole[]> {
const url = `https://mock.apidog.com/m1/1053378-0-default/rest/v1/user_roles`;
const url = `${API_BASE}/rest/v1/user_roles`;
const res = await fetch(url, {
method: "GET",
headers: baseHeaders(),
@ -602,28 +664,61 @@ export async function listarUserRoles(): Promise<UserRole[]> {
return await parse<UserRole[]>(res);
}
export type PatientAssignment = {
id: string;
patient_id: string;
user_id: string;
role: 'medico' | 'enfermeiro';
created_at?: string;
created_by?: string | null;
};
// Listar atribuições de pacientes (GET /rest/v1/patient_assignments)
export async function listarPatientAssignments(params?: { page?: number; limit?: number; q?: string; }): Promise<PatientAssignment[]> {
const qs = new URLSearchParams();
if (params?.q) qs.set('q', params.q);
const url = `${REST}/patient_assignments${qs.toString() ? `?${qs.toString()}` : ''}`;
const res = await fetch(url, { method: 'GET', headers: { ...baseHeaders(), ...rangeHeaders(params?.page, params?.limit) } });
return await parse<PatientAssignment[]>(res);
}
// NOTE: role assignments MUST be done server-side with service role credentials.
// The client should NOT attempt to POST to /rest/v1/user_roles because this
// endpoint typically requires elevated permissions (service role) and is not
// exposed in the public OpenAPI for client usage. Any role assignment must be
// implemented in an authenticated server function (Edge Function) and called
// from the backend. Keeping a client-side POST here caused confusion with the
// API documentation which only lists GET for `/rest/v1/user_roles`.
// If you need to retry role assignment from the frontend, call your backend
// service (e.g. an Edge Function) that performs the assignment using the
// service role key. Do not add client-side POSTs to `user_roles`.
// Nota: o endpoint POST /rest/v1/patient_assignments não existe na documentação fornecida.
// Se for necessário criar assignments, isso deve ser feito via função server-side segura.
export type User = {
id: string;
email: string;
email_confirmed_at: string;
email_confirmed_at: string | null;
created_at: string;
last_sign_in_at: string;
last_sign_in_at: string | null;
};
export type CurrentUser = {
id: string;
email: string;
email_confirmed_at: string;
email_confirmed_at: string | null;
created_at: string;
last_sign_in_at: string;
last_sign_in_at: string | null;
};
export type Profile = {
id: string;
full_name: string;
email: string;
phone: string;
avatar_url: string;
full_name: string | null;
email: string | null;
phone: string | null;
avatar_url: string | null;
disabled: boolean;
created_at: string;
updated_at: string;
@ -641,13 +736,13 @@ export type Permissions = {
export type UserInfo = {
user: User;
profile: Profile;
roles: string[];
profile: Profile | null;
roles: UserRoleEnum[];
permissions: Permissions;
};
export async function getCurrentUser(): Promise<CurrentUser> {
const url = `https://mock.apidog.com/m1/1053378-0-default/auth/v1/user`;
const url = `${API_BASE}/auth/v1/user`;
const res = await fetch(url, {
method: "GET",
headers: baseHeaders(),
@ -656,7 +751,7 @@ export async function getCurrentUser(): Promise<CurrentUser> {
}
export async function getUserInfo(): Promise<UserInfo> {
const url = `https://mock.apidog.com/m1/1053378-0-default/functions/v1/user-info`;
const url = `${API_BASE}/functions/v1/user-info`;
const res = await fetch(url, {
method: "GET",
headers: baseHeaders(),
@ -666,24 +761,23 @@ export async function getUserInfo(): Promise<UserInfo> {
export type CreateUserInput = {
email: string;
password: string;
full_name: string;
phone: string;
role: string;
password?: string;
phone?: string | null;
role: UserRoleEnum;
};
export type CreatedUser = {
id: string;
email: string;
full_name: string;
phone: string;
role: string;
phone: string | null;
role: UserRoleEnum;
};
export type CreateUserResponse = {
success: boolean;
user: CreatedUser;
password?: string;
};
export type CreateUserWithPasswordResponse = {
@ -702,7 +796,7 @@ export function gerarSenhaAleatoria(): string {
}
export async function criarUsuario(input: CreateUserInput): Promise<CreateUserResponse> {
const url = `https://mock.apidog.com/m1/1053378-0-default/functions/v1/create-user`;
const url = `${API_BASE}/functions/v1/create-user`;
const res = await fetch(url, {
method: "POST",
headers: { ...baseHeaders(), "Content-Type": "application/json" },
@ -711,6 +805,82 @@ export async function criarUsuario(input: CreateUserInput): Promise<CreateUserRe
return await parse<CreateUserResponse>(res);
}
// ===== ALTERNATIVA: Criar usuário diretamente via Supabase Auth =====
// Esta função é um fallback caso a função server-side create-user falhe
export async function criarUsuarioDirectAuth(input: {
email: string;
password: string;
full_name: string;
phone?: string | null;
role: UserRoleEnum;
userType?: 'profissional' | 'paciente';
}): Promise<CreateUserWithPasswordResponse> {
console.log('🔐 [DIRECT AUTH] Criando usuário diretamente via Supabase Auth...');
const signupUrl = `${API_BASE}/auth/v1/signup`;
const payload = {
email: input.email,
password: input.password,
data: {
userType: input.userType || (input.role === 'medico' ? 'profissional' : 'paciente'),
full_name: input.full_name,
phone: input.phone || '',
}
};
try {
const response = await fetch(signupUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"apikey": ENV_CONFIG.SUPABASE_ANON_KEY,
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorText = await response.text();
let errorMsg = `Erro ao criar usuário (${response.status})`;
try {
const errorData = JSON.parse(errorText);
errorMsg = errorData.msg || errorData.message || errorData.error_description || errorMsg;
} catch (e) {
// Ignora erro de parse
}
throw new Error(errorMsg);
}
const responseData = await response.json();
const userId = responseData.user?.id || responseData.id;
console.log('✅ [DIRECT AUTH] Usuário criado:', userId);
// NOTE: Role assignments MUST be done by the backend (Edge Function or server)
// when creating the user. The frontend should NOT attempt to assign roles.
// The backend should use the service role key to insert into user_roles table.
return {
success: true,
user: {
id: userId,
email: input.email,
full_name: input.full_name,
phone: input.phone || null,
role: input.role,
},
email: input.email,
password: input.password,
};
} catch (error: any) {
console.error('❌ [DIRECT AUTH] Erro ao criar usuário:', error);
throw error;
}
}
// ============================================
// CRIAÇÃO DE USUÁRIOS NO SUPABASE AUTH
// Vínculo com pacientes/médicos por EMAIL
@ -752,7 +922,7 @@ export async function criarUsuarioMedico(medico: {
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"apikey": API_KEY,
"apikey": ENV_CONFIG.SUPABASE_ANON_KEY,
},
body: JSON.stringify(payload),
});
@ -888,7 +1058,7 @@ export async function criarUsuarioPaciente(paciente: {
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"apikey": API_KEY,
"apikey": ENV_CONFIG.SUPABASE_ANON_KEY,
},
body: JSON.stringify(payload),
});

View File

@ -0,0 +1,133 @@
// lib/assignment.ts
import { ENV_CONFIG } from '@/lib/env-config';
// ===== TIPOS =====
// Roles válidos para patient_assignments conforme documentação
export type PatientAssignmentRole = "medico" | "enfermeiro";
export interface PatientAssignment {
id: string;
patient_id: string;
user_id: string;
role: PatientAssignmentRole;
created_at: string;
created_by: string;
}
export interface CreateAssignmentInput {
patient_id: string;
user_id: string;
role: PatientAssignmentRole;
created_by?: string;
}
// ===== CONSTANTES =====
const ASSIGNMENTS_URL = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/patient_assignments`;
// ===== FUNÇÕES DA API =====
/**
* Obtém o token de autenticação do localStorage.
*/
function getAuthToken(): string | null {
if (typeof window === "undefined") return null;
return localStorage.getItem("token") || localStorage.getItem("auth_token");
}
/**
* Cria os cabeçalhos padrão para as requisições.
*/
function getHeaders(): Record<string, string> {
const token = getAuthToken();
const headers: Record<string, string> = {
"apikey": ENV_CONFIG.SUPABASE_ANON_KEY,
"Content-Type": "application/json",
"Accept": "application/json",
};
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
return headers;
}
/**
* Atribui uma função (role) a um usuário para um paciente específico.
* @param input - Os dados para a nova atribuição.
* @returns A atribuição criada.
*/
export async function assignRoleToUser(input: CreateAssignmentInput): Promise<PatientAssignment> {
console.log("📝 [ASSIGNMENT] Atribuindo função:", input);
try {
const response = await fetch(ASSIGNMENTS_URL, {
method: 'POST',
headers: {
...getHeaders(),
'Prefer': 'return=representation', // Pede ao Supabase para retornar o objeto criado
},
body: JSON.stringify(input),
});
if (!response.ok) {
const errorBody = await response.text();
console.error("❌ [ASSIGNMENT] Erro na resposta da API:", {
status: response.status,
statusText: response.statusText,
body: errorBody,
});
throw new Error(`Erro ao atribuir função: ${response.statusText} (${response.status})`);
}
const createdAssignment = await response.json();
// O Supabase retorna um array com o item criado
if (Array.isArray(createdAssignment) && createdAssignment.length > 0) {
console.log("✅ [ASSIGNMENT] Função atribuída com sucesso:", createdAssignment[0]);
return createdAssignment[0];
}
throw new Error("A API não retornou a atribuição criada.");
} catch (error) {
console.error("❌ [ASSIGNMENT] Erro inesperado ao atribuir função:", error);
throw error;
}
}
/**
* Lista todas as atribuições de um paciente.
* @param patientId - O ID do paciente.
* @returns Uma lista de atribuições.
*/
export async function listAssignmentsForPatient(patientId: string): Promise<PatientAssignment[]> {
console.log(`🔍 [ASSIGNMENT] Listando atribuições para o paciente: ${patientId}`);
const url = `${ASSIGNMENTS_URL}?patient_id=eq.${patientId}`;
try {
const response = await fetch(url, {
method: 'GET',
headers: getHeaders(),
});
if (!response.ok) {
const errorBody = await response.text();
console.error("❌ [ASSIGNMENT] Erro ao listar atribuições:", {
status: response.status,
body: errorBody,
});
throw new Error(`Erro ao listar atribuições: ${response.statusText}`);
}
const assignments = await response.json();
console.log(`✅ [ASSIGNMENT] ${assignments.length} atribuições encontradas.`);
return assignments;
} catch (error) {
console.error("❌ [ASSIGNMENT] Erro inesperado ao listar atribuições:", error);
throw error;
}
}

View File

@ -6,7 +6,7 @@ import type {
UserData
} from '@/types/auth';
import { API_CONFIG, AUTH_ENDPOINTS, DEFAULT_HEADERS, API_KEY, buildApiUrl } from '@/lib/config';
import { API_CONFIG, AUTH_ENDPOINTS, DEFAULT_HEADERS, buildApiUrl } from '@/lib/config';
import { debugRequest } from '@/lib/debug-utils';
import { ENV_CONFIG } from '@/lib/env-config';
@ -31,7 +31,7 @@ function getAuthHeaders(token: string): Record<string, string> {
return {
"Content-Type": "application/json",
"Accept": "application/json",
"apikey": API_KEY,
"apikey": ENV_CONFIG.SUPABASE_ANON_KEY,
"Authorization": `Bearer ${token}`,
};
}
@ -43,7 +43,7 @@ function getLoginHeaders(): Record<string, string> {
return {
"Content-Type": "application/json",
"Accept": "application/json",
"apikey": API_KEY,
"apikey": ENV_CONFIG.SUPABASE_ANON_KEY,
};
}
@ -267,7 +267,7 @@ export async function refreshAuthToken(refreshToken: string): Promise<RefreshTok
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"apikey": API_KEY,
"apikey": ENV_CONFIG.SUPABASE_ANON_KEY,
},
body: JSON.stringify({ refresh_token: refreshToken }),
});

View File

@ -5,7 +5,7 @@
import { AUTH_STORAGE_KEYS } from '@/types/auth'
import { isExpired } from '@/lib/jwt'
import { API_KEY } from '@/lib/config'
import { ENV_CONFIG } from '@/lib/env-config'
interface QueuedRequest {
resolve: (value: any) => void
@ -58,11 +58,11 @@ class HttpClient {
timestamp: new Date().toLocaleTimeString()
})
const response = await fetch('https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/token?grant_type=refresh_token', {
const response = await fetch(ENV_CONFIG.AUTH_ENDPOINTS.REFRESH, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': API_KEY // API Key sempre necessária
'apikey': ENV_CONFIG.SUPABASE_ANON_KEY // API Key sempre necessária
},
body: JSON.stringify({ refresh_token: refreshToken })
})
@ -141,7 +141,7 @@ class HttpClient {
// Reexecutar requisição original
const newHeaders = {
...config.headers,
'apikey': API_KEY, // Garantir API Key
'apikey': ENV_CONFIG.SUPABASE_ANON_KEY, // Garantir API Key
Authorization: `Bearer ${newToken}`
}
@ -203,11 +203,11 @@ class HttpClient {
timestamp: new Date().toLocaleTimeString()
})
const config: RequestInit & { url: string } = {
const config: RequestInit & { url: string } = {
url: url.startsWith('http') ? url : `${this.baseURL}${url}`,
headers: {
'Content-Type': 'application/json',
'apikey': API_KEY, // API Key da Supabase sempre presente
'apikey': ENV_CONFIG.SUPABASE_ANON_KEY, // API Key da Supabase sempre presente
...(token && { Authorization: `Bearer ${token}` }), // Bearer Token quando usuário logado
...options.headers
},

View File

@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@ -0,0 +1,18 @@
(function () {
// Snippet to paste into browser console to forward console errors to the dev server
const origError = console.error;
window.__forwardClientLogs = true;
console.error = function (...args) {
try {
fetch('/api/client-logs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ level: 'error', args: args, url: location.href, timestamp: new Date().toISOString() })
}).catch(() => {})
} catch (e) {}
origError.apply(console, args);
}
console.log('✅ Client log forwarder installed. console.error() will be forwarded to /api/client-logs')
})();

View File

@ -0,0 +1,64 @@
import { NextResponse } from 'next/server'
import { ENV_CONFIG } from '@/lib/env-config'
type Body = {
user_id: string
role: string
}
async function getRequesterIdFromToken(token: string | null): Promise<string | null> {
if (!token) return null
try {
const url = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/user`
const res = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'apikey': ENV_CONFIG.SUPABASE_ANON_KEY, Authorization: `Bearer ${token}` } })
if (!res.ok) return null
const data = await res.json().catch(() => null)
return data?.id ?? null
} catch (err) {
console.error('[assign-role] erro ao obter requester id', err)
return null
}
}
export async function POST(req: Request) {
try {
const body = (await req.json()) as Body
if (!body || !body.user_id || !body.role) return NextResponse.json({ error: 'user_id and role required' }, { status: 400 })
const authHeader = req.headers.get('authorization')
const token = authHeader?.startsWith('Bearer ') ? authHeader.split(' ')[1] : null
const requesterId = await getRequesterIdFromToken(token)
if (!requesterId) return NextResponse.json({ error: 'unauthenticated' }, { status: 401 })
// Check if requester is administrador
const checkUrl = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/user_roles?user_id=eq.${requesterId}&role=eq.administrador`
const checkRes = await fetch(checkUrl, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', apikey: ENV_CONFIG.SUPABASE_ANON_KEY, Authorization: `Bearer ${token}` } })
if (!checkRes.ok) return NextResponse.json({ error: 'forbidden' }, { status: 403 })
const arr = await checkRes.json().catch(() => [])
if (!Array.isArray(arr) || arr.length === 0) return NextResponse.json({ error: 'forbidden' }, { status: 403 })
// Insert role using service role key from environment (must be set on the server)
const svcKey = process.env.SUPABASE_SERVICE_ROLE_KEY
if (!svcKey) return NextResponse.json({ error: 'server misconfigured' }, { status: 500 })
const insertUrl = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/user_roles`
const insertRes = await fetch(insertUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json', apikey: svcKey, Authorization: `Bearer ${svcKey}` },
body: JSON.stringify({ user_id: body.user_id, role: body.role }),
})
if (!insertRes.ok) {
const errBody = await insertRes.text().catch(() => null)
console.error('[assign-role] insert failed', insertRes.status, errBody)
return NextResponse.json({ error: 'failed to assign role', details: errBody }, { status: insertRes.status })
}
const result = await insertRes.json().catch(() => null)
return NextResponse.json({ ok: true, data: result })
} catch (err) {
console.error('[assign-role] unexpected error', err)
return NextResponse.json({ error: 'internal error' }, { status: 500 })
}
}