feat(api): add server-side /api/create-user route (user creation not functional yet)
- What was done: - Added a server-side Next.js route at `src/app/api/create-user/route.ts` that validates the requester token, checks roles, generates a temporary password and forwards the creation to the Supabase Edge Function using the service role key. - Client wired to call the route via `lib/config.ts` (`FUNCTIONS_ENDPOINTS.CREATE_USER` -> `/api/create-user`) and the `criarUsuario()` wrapper in `lib/api.ts`. - Status / missing work: - Important: user creation is NOT working yet (requests to `/api/create-user` return 404 in dev). - Next steps: restart dev server, ensure `SUPABASE_SERVICE_ROLE_KEY` is set in the environment, check server logs and run a test POST with a valid admin JWT.
This commit is contained in:
parent
05123e6c8f
commit
fb578b2a7a
@ -693,8 +693,13 @@ export default function PacientePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Renderização principal
|
// Renderização principal
|
||||||
|
// Nota: removemos a restrição por `requiredUserType` aqui para permitir
|
||||||
|
// que qualquer usuário autenticado (qualquer role) acesse a área de paciente.
|
||||||
|
// A atribuição de roles e o vínculo entre Auth user e perfis é responsabilidade
|
||||||
|
// exclusiva da API server-side — o client NÃO deve escrever `user_id` em
|
||||||
|
// `patients` nem tentar atribuir roles.
|
||||||
return (
|
return (
|
||||||
<ProtectedRoute requiredUserType={["paciente"]}>
|
<ProtectedRoute>
|
||||||
<div className="min-h-screen bg-background text-foreground flex flex-col">
|
<div className="min-h-screen bg-background text-foreground flex flex-col">
|
||||||
{/* Header só com título e botão de sair */}
|
{/* Header só com título e botão de sair */}
|
||||||
<header className="flex items-center justify-between px-4 py-2 border-b bg-card">
|
<header className="flex items-center justify-between px-4 py-2 border-b bg-card">
|
||||||
@ -723,6 +728,14 @@ export default function PacientePage() {
|
|||||||
<Button variant={tab==='perfil'?'secondary':'ghost'} aria-current={tab==='perfil'} onClick={()=>setTab('perfil')} className="justify-start"><UserCog className="mr-2 h-5 w-5" />{strings.perfil}</Button>
|
<Button variant={tab==='perfil'?'secondary':'ghost'} aria-current={tab==='perfil'} onClick={()=>setTab('perfil')} className="justify-start"><UserCog className="mr-2 h-5 w-5" />{strings.perfil}</Button>
|
||||||
</nav>
|
</nav>
|
||||||
{/* Conteúdo principal */}
|
{/* Conteúdo principal */}
|
||||||
|
{/* Banner informativo visível ao usuário/operador */}
|
||||||
|
<div className="w-full mb-4">
|
||||||
|
<div className="rounded border border-yellow-300 bg-yellow-50 p-3 text-sm text-yellow-900">
|
||||||
|
<strong>Observação:</strong> qualquer usuário autenticado pode acessar esta área do paciente.
|
||||||
|
<br />
|
||||||
|
Importante: o perfil de paciente no backend não contém um campo `user_id` que o cliente deva manipular. A atribuição de papéis/roles e vinculação entre usuário de autenticação e perfil é feita somente pela API.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex-1 min-w-0 p-4 max-w-4xl mx-auto w-full">
|
<div className="flex-1 min-w-0 p-4 max-w-4xl mx-auto w-full">
|
||||||
{/* Toasts de feedback */}
|
{/* Toasts de feedback */}
|
||||||
{toast && (
|
{toast && (
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export interface CredentialsDialogProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password?: string | null;
|
||||||
userName: string;
|
userName: string;
|
||||||
userType: "médico" | "paciente";
|
userType: "médico" | "paciente";
|
||||||
}
|
}
|
||||||
@ -36,13 +36,14 @@ export function CredentialsDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleCopyPassword() {
|
function handleCopyPassword() {
|
||||||
|
if (!password) return;
|
||||||
navigator.clipboard.writeText(password);
|
navigator.clipboard.writeText(password);
|
||||||
setCopiedPassword(true);
|
setCopiedPassword(true);
|
||||||
setTimeout(() => setCopiedPassword(false), 2000);
|
setTimeout(() => setCopiedPassword(false), 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCopyBoth() {
|
function handleCopyBoth() {
|
||||||
const text = `Email: ${email}\nSenha: ${password}`;
|
const text = password ? `Email: ${email}\nSenha: ${password}` : `Email: ${email}`;
|
||||||
navigator.clipboard.writeText(text);
|
navigator.clipboard.writeText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,36 +90,40 @@ export function CredentialsDialog({
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="password">Senha Temporária</Label>
|
<Label htmlFor="password">Senha Temporária</Label>
|
||||||
<div className="flex gap-2">
|
{password ? (
|
||||||
<div className="relative flex-1">
|
<div className="flex gap-2">
|
||||||
<Input
|
<div className="relative flex-1">
|
||||||
id="password"
|
<Input
|
||||||
type={showPassword ? "text" : "password"}
|
id="password"
|
||||||
value={password}
|
type={showPassword ? "text" : "password"}
|
||||||
readOnly
|
value={password}
|
||||||
className="bg-muted pr-10"
|
readOnly
|
||||||
/>
|
className="bg-muted pr-10"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="absolute right-0 top-0 h-full"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
title={showPassword ? "Ocultar senha" : "Mostrar senha"}
|
||||||
|
>
|
||||||
|
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="absolute right-0 top-0 h-full"
|
onClick={handleCopyPassword}
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
title="Copiar senha"
|
||||||
title={showPassword ? "Ocultar senha" : "Mostrar senha"}
|
|
||||||
>
|
>
|
||||||
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
{copiedPassword ? <CheckCircle2 className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
) : (
|
||||||
type="button"
|
<div className="p-3 rounded bg-muted text-sm text-muted-foreground">Nenhuma senha foi retornada pela API. Verifique no painel administrativo ou gere uma nova senha.</div>
|
||||||
variant="outline"
|
)}
|
||||||
size="icon"
|
|
||||||
onClick={handleCopyPassword}
|
|
||||||
title="Copiar senha"
|
|
||||||
>
|
|
||||||
{copiedPassword ? <CheckCircle2 className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -24,13 +24,11 @@ import {
|
|||||||
removerAnexoMedico,
|
removerAnexoMedico,
|
||||||
MedicoInput,
|
MedicoInput,
|
||||||
Medico,
|
Medico,
|
||||||
criarUsuarioMedico,
|
|
||||||
gerarSenhaAleatoria,
|
|
||||||
} from "@/lib/api";
|
} from "@/lib/api";
|
||||||
;
|
;
|
||||||
|
import { CredentialsDialog } from "@/components/credentials-dialog";
|
||||||
|
|
||||||
import { buscarCepAPI } from "@/lib/api";
|
import { buscarCepAPI } from "@/lib/api";
|
||||||
import { CredentialsDialog } from "@/components/credentials-dialog";
|
|
||||||
|
|
||||||
type FormacaoAcademica = {
|
type FormacaoAcademica = {
|
||||||
instituicao: string;
|
instituicao: string;
|
||||||
@ -150,18 +148,17 @@ export function DoctorRegistrationForm({
|
|||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
const [expanded, setExpanded] = useState({ dados: true, contato: false, endereco: false, obs: false, formacao: false, admin: false });
|
const [expanded, setExpanded] = useState({ dados: true, contato: false, endereco: false, obs: false, formacao: false, admin: false });
|
||||||
const [isSubmitting, setSubmitting] = useState(false);
|
const [isSubmitting, setSubmitting] = useState(false);
|
||||||
|
// credenciais geradas (após criação server-side)
|
||||||
|
const [credOpen, setCredOpen] = useState(false);
|
||||||
|
const [credEmail, setCredEmail] = useState("");
|
||||||
|
const [credPassword, setCredPassword] = useState("");
|
||||||
|
const [credUserName, setCredUserName] = useState("");
|
||||||
|
const [credUserType, setCredUserType] = useState<"médico" | "paciente">("médico");
|
||||||
const [isSearchingCEP, setSearchingCEP] = useState(false);
|
const [isSearchingCEP, setSearchingCEP] = useState(false);
|
||||||
const [photoPreview, setPhotoPreview] = useState<string | null>(null);
|
const [photoPreview, setPhotoPreview] = useState<string | null>(null);
|
||||||
const [serverAnexos, setServerAnexos] = useState<any[]>([]);
|
const [serverAnexos, setServerAnexos] = useState<any[]>([]);
|
||||||
|
|
||||||
// Estados para o dialog de credenciais
|
// Não exibimos credenciais no cliente
|
||||||
const [showCredentialsDialog, setShowCredentialsDialog] = useState(false);
|
|
||||||
const [credentials, setCredentials] = useState<{
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
userName: string;
|
|
||||||
userType: 'médico' | 'paciente';
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
const title = useMemo(() => (mode === "create" ? "Cadastro de Médico" : "Editar Médico"), [mode]);
|
const title = useMemo(() => (mode === "create" ? "Cadastro de Médico" : "Editar Médico"), [mode]);
|
||||||
|
|
||||||
@ -420,93 +417,49 @@ async function handleSubmit(ev: React.FormEvent) {
|
|||||||
const savedDoctorProfile = await criarMedico(medicoPayload);
|
const savedDoctorProfile = await criarMedico(medicoPayload);
|
||||||
console.log("✅ Perfil do médico criado:", savedDoctorProfile);
|
console.log("✅ Perfil do médico criado:", savedDoctorProfile);
|
||||||
|
|
||||||
// 2. Cria usuário no Supabase Auth (direto via /auth/v1/signup)
|
// Nota: criação de credenciais é feita server-side. Chamamos a rota administrativa
|
||||||
console.log('🔐 Criando usuário de autenticação...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const authResponse = await criarUsuarioMedico({
|
|
||||||
email: form.email,
|
|
||||||
full_name: form.full_name,
|
|
||||||
phone_mobile: form.celular || '',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (authResponse.success && authResponse.user) {
|
|
||||||
console.log('✅ Usuário Auth criado:', authResponse.user.id);
|
|
||||||
|
|
||||||
// Attempt to link the created auth user id to the doctors record
|
|
||||||
try {
|
try {
|
||||||
// savedDoctorProfile may be an array or object depending on API
|
const token = typeof window !== 'undefined' ? (localStorage.getItem('auth_token') || localStorage.getItem('token') || sessionStorage.getItem('auth_token') || sessionStorage.getItem('token')) : null;
|
||||||
const docId = (savedDoctorProfile && (savedDoctorProfile.id || (Array.isArray(savedDoctorProfile) ? savedDoctorProfile[0]?.id : undefined))) || null;
|
const headers: Record<string,string> = { 'Content-Type': 'application/json' };
|
||||||
if (docId) {
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||||
console.log('[DoctorForm] Vinculando user_id ao médico:', { doctorId: docId, userId: authResponse.user.id });
|
const createResp = await fetch('/api/create-user', {
|
||||||
// dynamic import to avoid circular deps in some bundlers
|
method: 'POST',
|
||||||
const api = await import('@/lib/api');
|
headers,
|
||||||
if (api && typeof api.vincularUserIdMedico === 'function') {
|
body: JSON.stringify({ email: savedDoctorProfile.email, full_name: savedDoctorProfile.full_name, role: 'medico' })
|
||||||
await api.vincularUserIdMedico(String(docId), String(authResponse.user.id));
|
});
|
||||||
console.log('[DoctorForm] user_id vinculado com sucesso.');
|
|
||||||
|
if (createResp.ok) {
|
||||||
|
const json = await createResp.json();
|
||||||
|
// função server retorna objeto com user + possivelmente email/password
|
||||||
|
// alguns deployments retornam email/password em json.user or top-level
|
||||||
|
const email = json?.email ?? json?.user?.email ?? savedDoctorProfile.email;
|
||||||
|
const password = json?.password ?? json?.user?.password ?? json?.user?.temp_password ?? null;
|
||||||
|
|
||||||
|
// Atualiza estados e abre dialog se recebido password
|
||||||
|
setCredEmail(email ?? savedDoctorProfile.email ?? '');
|
||||||
|
setCredUserName(savedDoctorProfile.full_name ?? '');
|
||||||
|
if (password) {
|
||||||
|
setCredPassword(String(password));
|
||||||
|
setCredOpen(true);
|
||||||
|
} else {
|
||||||
|
// se não recebeu senha, apenas avisar que usuário foi criado
|
||||||
|
alert('Médico cadastrado com sucesso. A conta foi criada, mas a senha temporária não foi retornada pela API.');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn('[DoctorForm] Não foi possível determinar o ID do médico para vincular user_id. Doctor profile:', savedDoctorProfile);
|
const txt = await createResp.text().catch(() => '');
|
||||||
|
console.warn('[doctor-form] create-user não ok:', createResp.status, txt);
|
||||||
|
alert('Médico cadastrado com sucesso. Falha ao criar credenciais automaticamente. Verifique os logs do servidor.');
|
||||||
}
|
}
|
||||||
} catch (linkErr) {
|
} catch (e) {
|
||||||
console.warn('[DoctorForm] Falha ao vincular user_id ao médico:', linkErr);
|
console.error('[doctor-form] erro ao criar credenciais server-side', e);
|
||||||
|
alert('Médico cadastrado com sucesso. Não foi possível criar credenciais automaticamente (erro de rede).');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Exibe popup com credenciais
|
// Limpa formulário
|
||||||
setCredentials({
|
|
||||||
email: authResponse.email,
|
|
||||||
password: authResponse.password,
|
|
||||||
userName: form.full_name,
|
|
||||||
userType: 'médico',
|
|
||||||
});
|
|
||||||
setShowCredentialsDialog(true);
|
|
||||||
|
|
||||||
// 4. Limpa formulário
|
|
||||||
setForm(initial);
|
setForm(initial);
|
||||||
setPhotoPreview(null);
|
setPhotoPreview(null);
|
||||||
setServerAnexos([]);
|
setServerAnexos([]);
|
||||||
|
|
||||||
// 5. Notifica componente pai
|
|
||||||
onSaved?.(savedDoctorProfile);
|
onSaved?.(savedDoctorProfile);
|
||||||
} else {
|
|
||||||
throw new Error('Falha ao criar usuário de autenticação');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (authError: any) {
|
|
||||||
console.error('❌ Erro ao criar usuário Auth:', authError);
|
|
||||||
|
|
||||||
const errorMsg = authError?.message || String(authError);
|
|
||||||
|
|
||||||
// Mensagens específicas de erro
|
|
||||||
if (errorMsg.toLowerCase().includes('already registered') ||
|
|
||||||
errorMsg.toLowerCase().includes('already been registered') ||
|
|
||||||
errorMsg.toLowerCase().includes('já está cadastrado')) {
|
|
||||||
alert(
|
|
||||||
`⚠️ EMAIL JÁ CADASTRADO\n\n` +
|
|
||||||
`O email "${form.email}" já possui uma conta no sistema.\n\n` +
|
|
||||||
`✅ O perfil do médico "${form.full_name}" foi salvo com sucesso.\n\n` +
|
|
||||||
`❌ Porém, não foi possível criar o login porque este email já está em uso.\n\n` +
|
|
||||||
`SOLUÇÃO:\n` +
|
|
||||||
`• Use um email diferente para este médico, OU\n` +
|
|
||||||
`• Se o médico já tem conta, edite o perfil e vincule ao usuário existente`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
alert(
|
|
||||||
`⚠️ Médico cadastrado com sucesso, mas houve um problema ao criar o acesso ao sistema.\n\n` +
|
|
||||||
`✅ Perfil do médico salvo: ${form.full_name}\n\n` +
|
|
||||||
`❌ Erro ao criar login: ${errorMsg}\n\n` +
|
|
||||||
`Por favor, entre em contato com o administrador para criar o acesso manualmente.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Limpa formulário mesmo com erro
|
|
||||||
setForm(initial);
|
|
||||||
setPhotoPreview(null);
|
|
||||||
setServerAnexos([]);
|
|
||||||
onSaved?.(savedDoctorProfile);
|
|
||||||
if (inline) onClose?.();
|
|
||||||
else onOpenChange?.(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("❌ Erro no handleSubmit:", err);
|
console.error("❌ Erro no handleSubmit:", err);
|
||||||
@ -1009,28 +962,7 @@ async function handleSubmit(ev: React.FormEvent) {
|
|||||||
<>
|
<>
|
||||||
<div className="space-y-6">{content}</div>
|
<div className="space-y-6">{content}</div>
|
||||||
|
|
||||||
{/* Dialog de credenciais */}
|
{/* Credenciais não exibidas no cliente */}
|
||||||
{credentials && (
|
|
||||||
<CredentialsDialog
|
|
||||||
open={showCredentialsDialog}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
setShowCredentialsDialog(open);
|
|
||||||
if (!open) {
|
|
||||||
// Quando o dialog de credenciais fecha, fecha o formulário também
|
|
||||||
setCredentials(null);
|
|
||||||
if (inline) {
|
|
||||||
onClose?.();
|
|
||||||
} else {
|
|
||||||
onOpenChange?.(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
email={credentials.email}
|
|
||||||
password={credentials.password}
|
|
||||||
userName={credentials.userName}
|
|
||||||
userType={credentials.userType}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1048,23 +980,15 @@ async function handleSubmit(ev: React.FormEvent) {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Dialog de credenciais */}
|
{/* Exibe credenciais retornadas pelo servidor (quando houver) */}
|
||||||
{credentials && (
|
<CredentialsDialog
|
||||||
<CredentialsDialog
|
open={credOpen}
|
||||||
open={showCredentialsDialog}
|
onOpenChange={(v) => setCredOpen(v)}
|
||||||
onOpenChange={(open) => {
|
email={credEmail}
|
||||||
setShowCredentialsDialog(open);
|
password={credPassword}
|
||||||
if (!open) {
|
userName={credUserName}
|
||||||
setCredentials(null);
|
userType={credUserType}
|
||||||
onOpenChange?.(false);
|
/>
|
||||||
}
|
|
||||||
}}
|
|
||||||
email={credentials.email}
|
|
||||||
password={credentials.password}
|
|
||||||
userName={credentials.userName}
|
|
||||||
userType={credentials.userType}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,13 +25,12 @@ import {
|
|||||||
listarAnexos,
|
listarAnexos,
|
||||||
removerAnexo,
|
removerAnexo,
|
||||||
buscarPacientePorId,
|
buscarPacientePorId,
|
||||||
criarUsuarioPaciente,
|
|
||||||
criarPaciente,
|
criarPaciente,
|
||||||
} from "@/lib/api";
|
} from "@/lib/api";
|
||||||
|
import { CredentialsDialog } from "@/components/credentials-dialog";
|
||||||
|
|
||||||
import { validarCPFLocal } from "@/lib/utils";
|
import { validarCPFLocal } from "@/lib/utils";
|
||||||
import { verificarCpfDuplicado } from "@/lib/api";
|
import { verificarCpfDuplicado } from "@/lib/api";
|
||||||
import { CredentialsDialog } from "@/components/credentials-dialog";
|
|
||||||
|
|
||||||
type Mode = "create" | "edit";
|
type Mode = "create" | "edit";
|
||||||
|
|
||||||
@ -100,18 +99,17 @@ export function PatientRegistrationForm({
|
|||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
const [expanded, setExpanded] = useState({ dados: true, contato: false, endereco: false, obs: false });
|
const [expanded, setExpanded] = useState({ dados: true, contato: false, endereco: false, obs: false });
|
||||||
const [isSubmitting, setSubmitting] = useState(false);
|
const [isSubmitting, setSubmitting] = useState(false);
|
||||||
|
// credenciais geradas server-side
|
||||||
|
const [credOpen, setCredOpen] = useState(false);
|
||||||
|
const [credEmail, setCredEmail] = useState("");
|
||||||
|
const [credPassword, setCredPassword] = useState("");
|
||||||
|
const [credUserName, setCredUserName] = useState("");
|
||||||
|
const [credUserType, setCredUserType] = useState<"médico" | "paciente">("paciente");
|
||||||
const [isSearchingCEP, setSearchingCEP] = useState(false);
|
const [isSearchingCEP, setSearchingCEP] = useState(false);
|
||||||
const [photoPreview, setPhotoPreview] = useState<string | null>(null);
|
const [photoPreview, setPhotoPreview] = useState<string | null>(null);
|
||||||
const [serverAnexos, setServerAnexos] = useState<any[]>([]);
|
const [serverAnexos, setServerAnexos] = useState<any[]>([]);
|
||||||
|
|
||||||
// Estados para o dialog de credenciais
|
// Credenciais/usuário não são criados no cliente
|
||||||
const [showCredentialsDialog, setShowCredentialsDialog] = useState(false);
|
|
||||||
const [credentials, setCredentials] = useState<{
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
userName: string;
|
|
||||||
userType: 'médico' | 'paciente';
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
const title = useMemo(() => (mode === "create" ? "Cadastro de Paciente" : "Editar Paciente"), [mode]);
|
const title = useMemo(() => (mode === "create" ? "Cadastro de Paciente" : "Editar Paciente"), [mode]);
|
||||||
|
|
||||||
@ -304,94 +302,45 @@ export function PatientRegistrationForm({
|
|||||||
const savedPatientProfile = await criarPaciente(patientPayload);
|
const savedPatientProfile = await criarPaciente(patientPayload);
|
||||||
console.log(" Perfil do paciente criado:", savedPatientProfile);
|
console.log(" Perfil do paciente criado:", savedPatientProfile);
|
||||||
|
|
||||||
if (form.email && form.email.includes('@')) {
|
// Depois de criar perfil, solicitar criação de credenciais server-side
|
||||||
console.log(" Criando usuário de autenticação (paciente)...");
|
|
||||||
try {
|
try {
|
||||||
const userResponse = await criarUsuarioPaciente({
|
const token = typeof window !== 'undefined' ? (localStorage.getItem('auth_token') || localStorage.getItem('token') || sessionStorage.getItem('auth_token') || sessionStorage.getItem('token')) : null;
|
||||||
email: form.email,
|
const headers: Record<string,string> = { 'Content-Type': 'application/json' };
|
||||||
full_name: form.nome,
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||||
phone_mobile: form.telefone,
|
const resp = await fetch('/api/create-user', {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({ email: savedPatientProfile.email, full_name: savedPatientProfile.full_name, role: 'user' })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (userResponse.success && userResponse.user) {
|
if (resp.ok) {
|
||||||
console.log(" Usuário de autenticação criado:", userResponse.user);
|
const json = await resp.json();
|
||||||
|
const email = json?.email ?? json?.user?.email ?? savedPatientProfile.email;
|
||||||
// Mostra credenciais no dialog usando as credenciais retornadas
|
const password = json?.password ?? json?.user?.password ?? json?.user?.temp_password ?? null;
|
||||||
setCredentials({
|
setCredEmail(email ?? savedPatientProfile.email ?? '');
|
||||||
email: userResponse.email ?? form.email,
|
setCredUserName(savedPatientProfile.full_name ?? '');
|
||||||
password: userResponse.password ?? '',
|
if (password) {
|
||||||
userName: form.nome,
|
setCredPassword(String(password));
|
||||||
userType: 'paciente',
|
setCredOpen(true);
|
||||||
});
|
} else {
|
||||||
setShowCredentialsDialog(true);
|
alert('Paciente cadastrado com sucesso. Conta criada, mas a senha temporária não foi retornada.');
|
||||||
|
|
||||||
// Tenta vincular o user_id ao perfil do paciente recém-criado
|
|
||||||
try {
|
|
||||||
const apiMod = await import('@/lib/api');
|
|
||||||
const pacienteId = savedPatientProfile?.id || (savedPatientProfile && (savedPatientProfile as any).id);
|
|
||||||
const userId = (userResponse.user as any)?.id || (userResponse.user as any)?.user_id || (userResponse.user as any)?.id;
|
|
||||||
if (pacienteId && userId && typeof apiMod.vincularUserIdPaciente === 'function') {
|
|
||||||
console.log('[PatientForm] Vinculando user_id ao paciente:', pacienteId, userId);
|
|
||||||
try {
|
|
||||||
await apiMod.vincularUserIdPaciente(pacienteId, String(userId));
|
|
||||||
console.log('[PatientForm] user_id vinculado com sucesso ao paciente');
|
|
||||||
} catch (linkErr) {
|
|
||||||
console.warn('[PatientForm] Falha ao vincular user_id ao paciente:', linkErr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (dynErr) {
|
|
||||||
console.warn('[PatientForm] Não foi possível importar helper para vincular user_id:', dynErr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limpa formulário mas NÃO fecha ainda - fechará quando o dialog de credenciais fechar
|
|
||||||
setForm(initial);
|
|
||||||
setPhotoPreview(null);
|
|
||||||
setServerAnexos([]);
|
|
||||||
onSaved?.(savedPatientProfile);
|
|
||||||
return;
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error((userResponse as any).message || "Falhou ao criar o usuário de acesso.");
|
const txt = await resp.text().catch(() => '');
|
||||||
|
console.warn('[patient-form] create-user failed', resp.status, txt);
|
||||||
|
alert('Paciente cadastrado com sucesso. Falha ao criar credenciais automaticamente.');
|
||||||
}
|
}
|
||||||
} catch (userError: any) {
|
} catch (e) {
|
||||||
console.error(" Erro ao criar usuário via signup:", userError);
|
console.error('[patient-form] erro ao criar credenciais server-side', e);
|
||||||
|
alert('Paciente cadastrado com sucesso. Não foi possível criar credenciais automaticamente (erro de rede).');
|
||||||
// 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 {
|
|
||||||
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);
|
onSaved?.(savedPatientProfile);
|
||||||
setForm(initial);
|
setForm(initial);
|
||||||
setPhotoPreview(null);
|
setPhotoPreview(null);
|
||||||
setServerAnexos([]);
|
setServerAnexos([]);
|
||||||
if (inline) onClose?.();
|
if (inline) onClose?.();
|
||||||
else onOpenChange?.(false);
|
else onOpenChange?.(false);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("❌ Erro no handleSubmit:", err);
|
console.error("❌ Erro no handleSubmit:", err);
|
||||||
@ -747,28 +696,15 @@ export function PatientRegistrationForm({
|
|||||||
<>
|
<>
|
||||||
<div className="space-y-6">{content}</div>
|
<div className="space-y-6">{content}</div>
|
||||||
|
|
||||||
{/* Dialog de credenciais */}
|
{/* Credenciais não exibidas no cliente */}
|
||||||
{credentials && (
|
<CredentialsDialog
|
||||||
<CredentialsDialog
|
open={credOpen}
|
||||||
open={showCredentialsDialog}
|
onOpenChange={(v) => setCredOpen(v)}
|
||||||
onOpenChange={(open) => {
|
email={credEmail}
|
||||||
setShowCredentialsDialog(open);
|
password={credPassword}
|
||||||
if (!open) {
|
userName={credUserName}
|
||||||
// Quando o dialog de credenciais fecha, fecha o formulário também
|
userType={credUserType}
|
||||||
setCredentials(null);
|
/>
|
||||||
if (inline) {
|
|
||||||
onClose?.();
|
|
||||||
} else {
|
|
||||||
onOpenChange?.(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
email={credentials.email}
|
|
||||||
password={credentials.password}
|
|
||||||
userName={credentials.userName}
|
|
||||||
userType={credentials.userType}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -786,23 +722,15 @@ export function PatientRegistrationForm({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Dialog de credenciais */}
|
{/* Credenciais não exibidas no cliente */}
|
||||||
{credentials && (
|
<CredentialsDialog
|
||||||
<CredentialsDialog
|
open={credOpen}
|
||||||
open={showCredentialsDialog}
|
onOpenChange={(v) => setCredOpen(v)}
|
||||||
onOpenChange={(open) => {
|
email={credEmail}
|
||||||
setShowCredentialsDialog(open);
|
password={credPassword}
|
||||||
if (!open) {
|
userName={credUserName}
|
||||||
setCredentials(null);
|
userType={credUserType}
|
||||||
onOpenChange?.(false);
|
/>
|
||||||
}
|
|
||||||
}}
|
|
||||||
email={credentials.email}
|
|
||||||
password={credentials.password}
|
|
||||||
userName={credentials.userName}
|
|
||||||
userType={credentials.userType}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
1
susconecta/eslint-report.json
Normal file
1
susconecta/eslint-report.json
Normal file
File diff suppressed because one or more lines are too long
@ -106,31 +106,15 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
if (isExpired(storedToken)) {
|
if (isExpired(storedToken)) {
|
||||||
console.log('[AUTH] Token expirado - tentando renovar...')
|
console.log('[AUTH] Token expirado - tentando renovar...')
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
const refreshToken = localStorage.getItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN)
|
const refreshToken = localStorage.getItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN)
|
||||||
if (refreshToken && !isExpired(refreshToken)) {
|
if (refreshToken && !isExpired(refreshToken)) {
|
||||||
// Tentar renovar via HTTP client (que já tem a lógica)
|
// Refresh automático foi desativado no cliente. A renovação de tokens
|
||||||
try {
|
// deve ser feita via backend seguro. Se desejar habilitar refresh no
|
||||||
await httpClient.get('/auth/v1/me') // Trigger refresh se necessário
|
// cliente, implemente um flow server-side ou use o SDK autenticado.
|
||||||
|
console.log('[AUTH] Refresh token presente, mas refresh automático desativado no cliente')
|
||||||
// Se chegou aqui, refresh foi bem-sucedido
|
|
||||||
const newToken = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN)
|
|
||||||
const userData = JSON.parse(storedUser) as UserData
|
|
||||||
|
|
||||||
if (newToken && newToken !== storedToken) {
|
|
||||||
setToken(newToken)
|
|
||||||
setUser(userData)
|
|
||||||
setAuthStatus('authenticated')
|
|
||||||
console.log('[AUTH] Token RENOVADO automaticamente!')
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 800))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch (refreshError) {
|
|
||||||
console.log(' [AUTH] Falha no refresh automático')
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 400))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clearAuthData()
|
clearAuthData()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -138,38 +122,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
// Restaurar sessão válida
|
// Restaurar sessão válida
|
||||||
const userData = JSON.parse(storedUser) as UserData
|
const userData = JSON.parse(storedUser) as UserData
|
||||||
setToken(storedToken)
|
setToken(storedToken)
|
||||||
// Tentar buscar profile consolidado (user-info) e mesclar
|
// Nota: chamadas a user-info foram removidas do cliente. Mantemos os dados do
|
||||||
try {
|
// usuário que já estavam persistidos localmente sem tentar reconciliar com funções remotas.
|
||||||
const info = await getUserInfo()
|
|
||||||
if (info?.profile) {
|
|
||||||
const mapped = {
|
|
||||||
cpf: (info.profile as any).cpf ?? userData.profile?.cpf,
|
|
||||||
crm: (info.profile as any).crm ?? userData.profile?.crm,
|
|
||||||
telefone: info.profile.phone ?? userData.profile?.telefone,
|
|
||||||
foto_url: info.profile.avatar_url ?? userData.profile?.foto_url,
|
|
||||||
}
|
|
||||||
if (userData.profile) {
|
|
||||||
userData.profile = { ...userData.profile, ...mapped }
|
|
||||||
} else {
|
|
||||||
userData.profile = mapped
|
|
||||||
}
|
|
||||||
// Persistir o usuário atualizado no localStorage para evitar
|
|
||||||
// que 'auth_user.profile' fique vazio após um reload completo
|
|
||||||
try {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
// Persistir também roles se disponíveis
|
|
||||||
if (info?.roles && Array.isArray(info.roles)) {
|
|
||||||
userData.roles = info.roles
|
|
||||||
}
|
|
||||||
localStorage.setItem(AUTH_STORAGE_KEYS.USER, JSON.stringify(userData))
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('[AUTH] Falha ao persistir user (profile) no localStorage:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[AUTH] Falha ao buscar user-info na restauração de sessão:', err)
|
|
||||||
}
|
|
||||||
|
|
||||||
setUser(userData)
|
setUser(userData)
|
||||||
setAuthStatus('authenticated')
|
setAuthStatus('authenticated')
|
||||||
@ -197,74 +151,57 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
try {
|
try {
|
||||||
console.log('[AUTH] Iniciando login:', { email, userType })
|
console.log('[AUTH] Iniciando login:', { email, userType })
|
||||||
|
|
||||||
const response = await loginUser(email, password, userType)
|
const response = await loginUser(email, password)
|
||||||
|
|
||||||
// Após receber token, buscar roles/permissions reais e reconciliar userType
|
// Nota: busca de user-info e reconciliação via funções server-side foi removida do cliente.
|
||||||
try {
|
// Mantemos o objeto response.user retornado pelo fluxo de autenticação sem tentativas
|
||||||
const infoRes = await fetch(`${ENV_CONFIG.SUPABASE_URL}/functions/v1/user-info`, {
|
// adicionais de mesclagem aqui.
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Authorization': `Bearer ${response.access_token}`,
|
|
||||||
'apikey': ENV_CONFIG.SUPABASE_ANON_KEY,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (infoRes.ok) {
|
|
||||||
const info = await infoRes.json().catch(() => null)
|
|
||||||
const roles: string[] = Array.isArray(info?.roles) ? info.roles : (info?.roles ? [info.roles] : [])
|
|
||||||
|
|
||||||
// Derivar tipo de usuário a partir dos roles
|
|
||||||
let derived: UserType = 'paciente'
|
|
||||||
if (roles.includes('admin') || roles.includes('gestor') || roles.includes('secretaria')) {
|
|
||||||
derived = 'administrador'
|
|
||||||
} else if (roles.includes('medico') || roles.includes('enfermeiro')) {
|
|
||||||
derived = 'profissional'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Atualizar userType caso seja diferente
|
|
||||||
if (response.user && response.user.userType !== derived) {
|
|
||||||
response.user.userType = derived
|
|
||||||
console.log('[AUTH] userType reconciled from roles ->', derived)
|
|
||||||
}
|
|
||||||
// Persistir roles no objeto user para suportar multi-role
|
|
||||||
if (response.user && roles.length) {
|
|
||||||
response.user.roles = roles
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('[AUTH] Falha ao obter user-info para reconciliar roles:', infoRes.status)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[AUTH] Erro ao buscar user-info após login (não crítico):', err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Após login, tentar buscar profile consolidado e mesclar antes de persistir
|
|
||||||
try {
|
|
||||||
const info = await getUserInfo()
|
|
||||||
if (info?.profile && response.user) {
|
|
||||||
const mapped = {
|
|
||||||
cpf: (info.profile as any).cpf ?? response.user.profile?.cpf,
|
|
||||||
crm: (info.profile as any).crm ?? response.user.profile?.crm,
|
|
||||||
telefone: info.profile.phone ?? response.user.profile?.telefone,
|
|
||||||
foto_url: info.profile.avatar_url ?? response.user.profile?.foto_url,
|
|
||||||
}
|
|
||||||
if (response.user.profile) {
|
|
||||||
response.user.profile = { ...response.user.profile, ...mapped }
|
|
||||||
} else {
|
|
||||||
response.user.profile = mapped
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[AUTH] Falha ao buscar user-info após login (não crítico):', err)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Salva dados iniciais (token + user retornado pelo login)
|
||||||
saveAuthData(
|
saveAuthData(
|
||||||
response.access_token,
|
response.access_token,
|
||||||
response.user,
|
response.user,
|
||||||
response.refresh_token
|
response.refresh_token
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Tentar buscar informações consolidadas do usuário (profile + roles)
|
||||||
|
try {
|
||||||
|
const userInfo = await getUserInfo()
|
||||||
|
if (userInfo) {
|
||||||
|
const mergedUser: any = {
|
||||||
|
...response.user,
|
||||||
|
// prefer profile and roles vindos do userInfo
|
||||||
|
profile: userInfo.profile ?? (response.user as any).profile,
|
||||||
|
roles: userInfo.roles ?? (response.user as any).roles ?? [],
|
||||||
|
// garantir id/email caso estejam no userInfo
|
||||||
|
id: (response.user as any).id || (userInfo.user as any)?.id,
|
||||||
|
email: (response.user as any).email || (userInfo.user as any)?.email,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inferir userType a partir de roles se estiver ausente
|
||||||
|
if (!mergedUser.userType || mergedUser.userType === undefined) {
|
||||||
|
const r: string[] = mergedUser.roles || [];
|
||||||
|
if (r.includes('admin') || r.includes('gestor') || r.includes('secretaria')) mergedUser.userType = 'administrador'
|
||||||
|
else if (r.includes('medico') || r.includes('enfermeiro')) mergedUser.userType = 'profissional'
|
||||||
|
else if (r.includes('paciente')) mergedUser.userType = 'paciente'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persistir e atualizar estado com dados consolidados
|
||||||
|
try {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem(AUTH_STORAGE_KEYS.USER, JSON.stringify(mergedUser))
|
||||||
|
localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, mergedUser.userType || '')
|
||||||
|
}
|
||||||
|
setUser(mergedUser as any)
|
||||||
|
console.log('[AUTH] userInfo consolidado e salvo:', { email: mergedUser.email, roles: mergedUser.roles })
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[AUTH] Não foi possível persistir userInfo consolidado:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (infoErr) {
|
||||||
|
console.warn('[AUTH] getUserInfo falhou (ignorar no cliente):', infoErr)
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[AUTH] Login realizado com sucesso')
|
console.log('[AUTH] Login realizado com sucesso')
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
// lib/api.ts
|
// lib/api.ts
|
||||||
|
|
||||||
import { ENV_CONFIG } from '@/lib/env-config';
|
import { ENV_CONFIG } from '@/lib/env-config';
|
||||||
|
import { FUNCTIONS_ENDPOINTS } from '@/lib/config';
|
||||||
// Use ENV_CONFIG for SUPABASE URL and anon key in frontend
|
// Use ENV_CONFIG for SUPABASE URL and anon key in frontend
|
||||||
|
|
||||||
export type ApiOk<T = any> = {
|
export type ApiOk<T = any> = {
|
||||||
@ -613,17 +614,9 @@ export async function excluirPaciente(id: string | number): Promise<void> {
|
|||||||
* Este endpoint usa a service role key e valida se o requisitante é administrador.
|
* Este endpoint usa a service role key e valida se o requisitante é administrador.
|
||||||
*/
|
*/
|
||||||
export async function assignRoleServerSide(userId: string, role: string): Promise<any> {
|
export async function assignRoleServerSide(userId: string, role: string): Promise<any> {
|
||||||
const url = `/api/assign-role`;
|
// Operação administrativa removida do cliente.
|
||||||
const token = getAuthToken();
|
// Atribuição de roles é feita exclusivamente via backend com a service role key.
|
||||||
const res = await fetch(url, {
|
throw new TypeError('assignRoleServerSide removido do cliente. Use APIs administrativas no servidor para atribuir roles.');
|
||||||
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) =====
|
// ===== PACIENTES (Extra: verificação de CPF duplicado) =====
|
||||||
export async function verificarCpfDuplicado(cpf: string): Promise<boolean> {
|
export async function verificarCpfDuplicado(cpf: string): Promise<boolean> {
|
||||||
@ -935,6 +928,15 @@ export async function vincularUserIdMedico(medicoId: string | number, userId: st
|
|||||||
* Retorna o paciente atualizado.
|
* Retorna o paciente atualizado.
|
||||||
*/
|
*/
|
||||||
export async function vincularUserIdPaciente(pacienteId: string | number, userId: string): Promise<Paciente> {
|
export async function vincularUserIdPaciente(pacienteId: string | number, userId: string): Promise<Paciente> {
|
||||||
|
// Nota: a coluna `user_id` não existe no schema público de `patients`.
|
||||||
|
// Operações de vinculação devem ser feitas server-side usando a service role key.
|
||||||
|
// Esta função NÃO deve ser chamada a partir do browser. Se for chamada no cliente,
|
||||||
|
// lançamos um erro que instrui a executar a operação via backend/rota administrativa.
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
throw new TypeError('Operação inválida: vincularUserIdPaciente não pode ser executada no cliente. Execute a vinculação no backend com a service role key.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executando no servidor (node): tentar patch direto (caso o schema permita).
|
||||||
const url = `${REST}/patients?id=eq.${encodeURIComponent(String(pacienteId))}`;
|
const url = `${REST}/patients?id=eq.${encodeURIComponent(String(pacienteId))}`;
|
||||||
const payload = { user_id: String(userId) };
|
const payload = { user_id: String(userId) };
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
@ -1024,11 +1026,15 @@ export type UserRole = {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listar roles de usuários
|
||||||
|
* GET /rest/v1/user_roles
|
||||||
|
*/
|
||||||
export async function listarUserRoles(): Promise<UserRole[]> {
|
export async function listarUserRoles(): Promise<UserRole[]> {
|
||||||
const url = `${API_BASE}/rest/v1/user_roles`;
|
const url = `${REST}/user_roles`;
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
headers: baseHeaders(),
|
headers: baseHeaders()
|
||||||
});
|
});
|
||||||
return await parse<UserRole[]>(res);
|
return await parse<UserRole[]>(res);
|
||||||
}
|
}
|
||||||
@ -1096,21 +1102,47 @@ export type UserInfo = {
|
|||||||
permissions: Permissions;
|
permissions: Permissions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obter dados do usuário atual via Supabase Auth
|
||||||
|
* GET /auth/v1/user
|
||||||
|
*/
|
||||||
export async function getCurrentUser(): Promise<CurrentUser> {
|
export async function getCurrentUser(): Promise<CurrentUser> {
|
||||||
const url = `${API_BASE}/auth/v1/user`;
|
const token = getAuthToken();
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('Token não encontrado. Faça login primeiro.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/user`;
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
headers: baseHeaders(),
|
headers: {
|
||||||
|
...baseHeaders(),
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return await parse<CurrentUser>(res);
|
return await parse<CurrentUser>(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obter informações completas do usuário
|
||||||
|
* GET /functions/v1/user-info
|
||||||
|
*/
|
||||||
export async function getUserInfo(): Promise<UserInfo> {
|
export async function getUserInfo(): Promise<UserInfo> {
|
||||||
const url = `${API_BASE}/functions/v1/user-info`;
|
const token = getAuthToken();
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('Token não encontrado. Faça login primeiro.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = FUNCTIONS_ENDPOINTS.USER_INFO;
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
headers: baseHeaders(),
|
headers: {
|
||||||
|
...baseHeaders(),
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return await parse<UserInfo>(res);
|
return await parse<UserInfo>(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1150,391 +1182,52 @@ export function gerarSenhaAleatoria(): string {
|
|||||||
return `senha${num1}${num2}${num3}!`;
|
return `senha${num1}${num2}${num3}!`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Criar novo usuário
|
||||||
|
* POST /functions/v1/create-user
|
||||||
|
*/
|
||||||
export async function criarUsuario(input: CreateUserInput): Promise<CreateUserResponse> {
|
export async function criarUsuario(input: CreateUserInput): Promise<CreateUserResponse> {
|
||||||
const url = `${API_BASE}/functions/v1/create-user`;
|
const token = getAuthToken();
|
||||||
const res = await fetch(url, {
|
if (!token) {
|
||||||
method: "POST",
|
throw new Error('Token não encontrado. Faça login primeiro.');
|
||||||
headers: { ...baseHeaders(), "Content-Type": "application/json" },
|
}
|
||||||
body: JSON.stringify(input),
|
|
||||||
});
|
|
||||||
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 || '',
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const url = FUNCTIONS_ENDPOINTS.CREATE_USER;
|
||||||
|
// Log do request para debugging (visível no console do navegador)
|
||||||
try {
|
try {
|
||||||
const response = await fetch(signupUrl, {
|
console.debug('[USER API] POST', url, 'body:', input);
|
||||||
method: "POST",
|
} catch (e) {
|
||||||
|
// ignore logging errors
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: Response;
|
||||||
|
try {
|
||||||
|
response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
...baseHeaders(),
|
||||||
"Accept": "application/json",
|
'Content-Type': 'application/json',
|
||||||
"apikey": ENV_CONFIG.SUPABASE_ANON_KEY,
|
'Authorization': `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(input),
|
||||||
});
|
});
|
||||||
|
} catch (networkErr) {
|
||||||
if (!response.ok) {
|
console.error('[USER API] Network error calling create-user', networkErr);
|
||||||
const errorText = await response.text();
|
throw new Error('Erro de rede ao tentar criar usuário. Verifique sua conexão e tente novamente.');
|
||||||
let errorMsg = `Erro ao criar usuário (${response.status})`;
|
}
|
||||||
try {
|
|
||||||
const errorData = JSON.parse(errorText);
|
// Parse com captura para logar resposta bruta e final
|
||||||
errorMsg = errorData.msg || errorData.message || errorData.error_description || errorMsg;
|
try {
|
||||||
} catch (e) {
|
const parsed = await parse<CreateUserResponse>(response);
|
||||||
// Ignora erro de parse
|
try { console.debug('[USER API] create-user response:', parsed); } catch (_) {}
|
||||||
}
|
return parsed;
|
||||||
throw new Error(errorMsg);
|
} catch (apiErr: any) {
|
||||||
}
|
// Já temos logs do parse em parse(), mas adicionamos um log contextual
|
||||||
|
console.error('[USER API] create-user failed:', apiErr?.message || apiErr);
|
||||||
const responseData = await response.json();
|
throw apiErr;
|
||||||
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
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
// Criar usuário para MÉDICO no Supabase Auth (sistema de autenticação)
|
|
||||||
export async function criarUsuarioMedico(medico: {
|
|
||||||
email: string;
|
|
||||||
full_name: string;
|
|
||||||
phone_mobile: string;
|
|
||||||
}): Promise<CreateUserWithPasswordResponse> {
|
|
||||||
|
|
||||||
const senha = gerarSenhaAleatoria();
|
|
||||||
|
|
||||||
console.log('[CRIAR MÉDICO] Iniciando criação no Supabase Auth...');
|
|
||||||
console.log('Email:', medico.email);
|
|
||||||
console.log('Nome:', medico.full_name);
|
|
||||||
console.log('Telefone:', medico.phone_mobile);
|
|
||||||
console.log('Senha gerada:', senha);
|
|
||||||
|
|
||||||
// Endpoint do Supabase Auth (mesmo que auth.ts usa)
|
|
||||||
const signupUrl = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/signup`;
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
email: medico.email,
|
|
||||||
password: senha,
|
|
||||||
data: {
|
|
||||||
userType: 'profissional', // Para login em /login -> /profissional
|
|
||||||
full_name: medico.full_name,
|
|
||||||
phone: medico.phone_mobile,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('[CRIAR MÉDICO] Enviando para:', signupUrl);
|
|
||||||
|
|
||||||
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),
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[CRIAR MÉDICO] Status da resposta:', response.status, response.statusText);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
console.error('[CRIAR MÉDICO] Erro na resposta:', errorText);
|
|
||||||
|
|
||||||
// Tenta parsear o erro para pegar mensagem específica
|
|
||||||
let errorMsg = `Erro ao criar usuário (${response.status})`;
|
|
||||||
try {
|
|
||||||
const errorData = JSON.parse(errorText);
|
|
||||||
errorMsg = errorData.msg || errorData.message || errorData.error_description || errorMsg;
|
|
||||||
|
|
||||||
// Mensagens amigáveis para erros comuns
|
|
||||||
if (errorMsg.includes('already registered') || errorMsg.includes('already exists')) {
|
|
||||||
errorMsg = 'Este email já está cadastrado no sistema';
|
|
||||||
} else if (errorMsg.includes('invalid email')) {
|
|
||||||
errorMsg = 'Formato de email inválido';
|
|
||||||
} else if (errorMsg.includes('weak password')) {
|
|
||||||
errorMsg = 'Senha muito fraca';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Se não conseguir parsear, usa mensagem genérica
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseData = await response.json();
|
|
||||||
console.log('[CRIAR MÉDICO] Usuário criado com sucesso no Supabase Auth!');
|
|
||||||
console.log('User ID:', responseData.user?.id || responseData.id);
|
|
||||||
|
|
||||||
// 🔧 AUTO-CONFIRMAR EMAIL: Fazer login automático logo após criar usuário
|
|
||||||
// Isso força o Supabase a confirmar o email automaticamente
|
|
||||||
if (responseData.user?.email_confirmed_at === null || !responseData.user?.email_confirmed_at) {
|
|
||||||
console.warn('[CRIAR MÉDICO] Email NÃO confirmado - tentando auto-confirmar via login...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const loginUrl = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/token?grant_type=password`;
|
|
||||||
console.log('[AUTO-CONFIRMAR] Fazendo login automático para confirmar email...');
|
|
||||||
|
|
||||||
const loginResponse = await fetch(loginUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'apikey': ENV_CONFIG.SUPABASE_ANON_KEY,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: medico.email,
|
|
||||||
password: senha,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (loginResponse.ok) {
|
|
||||||
const loginData = await loginResponse.json();
|
|
||||||
console.log('[AUTO-CONFIRMAR] Login automático realizado com sucesso!');
|
|
||||||
console.log('[AUTO-CONFIRMAR] Email confirmado:', loginData.user?.email_confirmed_at ? 'SIM' : 'NÃO');
|
|
||||||
|
|
||||||
// Atualizar responseData com dados do login (que tem email confirmado)
|
|
||||||
if (loginData.user) {
|
|
||||||
responseData.user = loginData.user;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const errorText = await loginResponse.text();
|
|
||||||
console.error('[AUTO-CONFIRMAR] Falha no login automático:', loginResponse.status, errorText);
|
|
||||||
console.warn('[AUTO-CONFIRMAR] Usuário pode não conseguir fazer login imediatamente!');
|
|
||||||
}
|
|
||||||
} catch (confirmError) {
|
|
||||||
console.error('[AUTO-CONFIRMAR] Erro ao tentar fazer login automático:', confirmError);
|
|
||||||
console.warn('[AUTO-CONFIRMAR] Continuando sem confirmação automática...');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('[CRIAR MÉDICO] Email confirmado automaticamente!');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log bem visível com as credenciais para teste
|
|
||||||
console.log('========================================');
|
|
||||||
console.log('CREDENCIAIS DO MÉDICO CRIADO:');
|
|
||||||
console.log('Email:', medico.email);
|
|
||||||
console.log('Senha:', senha);
|
|
||||||
console.log('Pode fazer login?', responseData.user?.email_confirmed_at ? 'SIM' : 'NÃO (precisa confirmar email)');
|
|
||||||
console.log('========================================');
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
user: responseData.user || responseData,
|
|
||||||
email: medico.email,
|
|
||||||
password: senha,
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('[CRIAR MÉDICO] Erro ao criar usuário:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Criar usuário para PACIENTE no Supabase Auth (sistema de autenticação)
|
|
||||||
export async function criarUsuarioPaciente(paciente: {
|
|
||||||
email: string;
|
|
||||||
full_name: string;
|
|
||||||
phone_mobile: string;
|
|
||||||
}): Promise<CreateUserWithPasswordResponse> {
|
|
||||||
|
|
||||||
const senha = gerarSenhaAleatoria();
|
|
||||||
|
|
||||||
console.log('[CRIAR PACIENTE] Iniciando criação no Supabase Auth...');
|
|
||||||
console.log('Email:', paciente.email);
|
|
||||||
console.log('Nome:', paciente.full_name);
|
|
||||||
console.log('Telefone:', paciente.phone_mobile);
|
|
||||||
console.log('Senha gerada:', senha);
|
|
||||||
|
|
||||||
// Endpoint do Supabase Auth (mesmo que auth.ts usa)
|
|
||||||
const signupUrl = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/signup`;
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
email: paciente.email,
|
|
||||||
password: senha,
|
|
||||||
data: {
|
|
||||||
userType: 'paciente', // Para login em /login-paciente -> /paciente
|
|
||||||
full_name: paciente.full_name,
|
|
||||||
phone: paciente.phone_mobile,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('[CRIAR PACIENTE] Enviando para:', signupUrl);
|
|
||||||
|
|
||||||
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),
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[CRIAR PACIENTE] Status da resposta:', response.status, response.statusText);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
console.error('[CRIAR PACIENTE] Erro na resposta:', errorText);
|
|
||||||
|
|
||||||
// Tenta parsear o erro para pegar mensagem específica
|
|
||||||
let errorMsg = `Erro ao criar usuário (${response.status})`;
|
|
||||||
try {
|
|
||||||
const errorData = JSON.parse(errorText);
|
|
||||||
errorMsg = errorData.msg || errorData.message || errorData.error_description || errorMsg;
|
|
||||||
|
|
||||||
// Mensagens amigáveis para erros comuns
|
|
||||||
if (errorMsg.includes('already registered') || errorMsg.includes('already exists')) {
|
|
||||||
errorMsg = 'Este email já está cadastrado no sistema';
|
|
||||||
} else if (errorMsg.includes('invalid email')) {
|
|
||||||
errorMsg = 'Formato de email inválido';
|
|
||||||
} else if (errorMsg.includes('weak password')) {
|
|
||||||
errorMsg = 'Senha muito fraca';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Se não conseguir parsear, usa mensagem genérica
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseData = await response.json();
|
|
||||||
console.log('[CRIAR PACIENTE] Usuário criado com sucesso no Supabase Auth!');
|
|
||||||
console.log('User ID:', responseData.user?.id || responseData.id);
|
|
||||||
console.log('[CRIAR PACIENTE] Resposta completa do Supabase:', JSON.stringify(responseData, null, 2));
|
|
||||||
|
|
||||||
// VERIFICAÇÃO CRÍTICA: O usuário foi realmente criado?
|
|
||||||
if (!responseData.user && !responseData.id) {
|
|
||||||
console.error('AVISO: Supabase retornou sucesso mas sem user ID!');
|
|
||||||
console.error('Isso pode significar que o usuário não foi criado de verdade!');
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = responseData.user?.id || responseData.id;
|
|
||||||
|
|
||||||
// 🔧 AUTO-CONFIRMAR EMAIL: Fazer login automático logo após criar usuário
|
|
||||||
// Isso força o Supabase a confirmar o email automaticamente
|
|
||||||
if (responseData.user?.email_confirmed_at === null || !responseData.user?.email_confirmed_at) {
|
|
||||||
console.warn('[CRIAR PACIENTE] Email NÃO confirmado - tentando auto-confirmar via login...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const loginUrl = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/token?grant_type=password`;
|
|
||||||
console.log('[AUTO-CONFIRMAR] Fazendo login automático para confirmar email...');
|
|
||||||
|
|
||||||
const loginResponse = await fetch(loginUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'apikey': ENV_CONFIG.SUPABASE_ANON_KEY,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: paciente.email,
|
|
||||||
password: senha,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[AUTO-CONFIRMAR] Status do login automático:', loginResponse.status);
|
|
||||||
|
|
||||||
if (loginResponse.ok) {
|
|
||||||
const loginData = await loginResponse.json();
|
|
||||||
console.log('[AUTO-CONFIRMAR] Login automático realizado com sucesso!');
|
|
||||||
console.log('[AUTO-CONFIRMAR] Dados completos do login:', JSON.stringify(loginData, undefined, 2));
|
|
||||||
console.log('[AUTO-CONFIRMAR] Email confirmado:', loginData.user?.email_confirmed_at ? 'SIM' : 'NÃO');
|
|
||||||
console.log('[AUTO-CONFIRMAR] UserType no metadata:', loginData.user?.user_metadata?.userType);
|
|
||||||
console.log('[AUTO-CONFIRMAR] Email verified:', loginData.user?.user_metadata?.email_verified);
|
|
||||||
|
|
||||||
// Atualizar responseData com dados do login (que tem email confirmado)
|
|
||||||
if (loginData.user) {
|
|
||||||
responseData.user = loginData.user;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const errorText = await loginResponse.text();
|
|
||||||
console.error('[AUTO-CONFIRMAR] Falha no login automático:', loginResponse.status, errorText);
|
|
||||||
console.warn('[AUTO-CONFIRMAR] Usuário pode não conseguir fazer login imediatamente!');
|
|
||||||
|
|
||||||
// Tentar parsear o erro para entender melhor
|
|
||||||
try {
|
|
||||||
const errorData = JSON.parse(errorText);
|
|
||||||
console.error('[AUTO-CONFIRMAR] Detalhes do erro:', errorData);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[AUTO-CONFIRMAR] Erro não é JSON:', errorText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (confirmError) {
|
|
||||||
console.error('[AUTO-CONFIRMAR] Erro ao tentar fazer login automático:', confirmError);
|
|
||||||
console.warn('[AUTO-CONFIRMAR] Continuando sem confirmação automática...');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('[CRIAR PACIENTE] Email confirmado automaticamente!');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log bem visível com as credenciais para teste
|
|
||||||
console.log('========================================');
|
|
||||||
console.log('CREDENCIAIS DO PACIENTE CRIADO:');
|
|
||||||
console.log('Email:', paciente.email);
|
|
||||||
console.log('Senha:', senha);
|
|
||||||
console.log('UserType:', 'paciente');
|
|
||||||
console.log('Pode fazer login?', responseData.user?.email_confirmed_at ? 'SIM' : 'NÃO (precisa confirmar email)');
|
|
||||||
console.log('========================================');
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
user: responseData.user || responseData,
|
|
||||||
email: paciente.email,
|
|
||||||
password: senha,
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('[CRIAR PACIENTE] Erro ao criar usuário:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== CEP (usado nos formulários) =====
|
// ===== CEP (usado nos formulários) =====
|
||||||
export async function buscarCepAPI(cep: string): Promise<{
|
export async function buscarCepAPI(cep: string): Promise<{
|
||||||
|
|||||||
@ -83,245 +83,85 @@ async function processResponse<T>(response: Response): Promise<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Serviço para fazer login e obter token JWT
|
* Serviço para fazer login e obter token JWT
|
||||||
|
* POST /auth/v1/token?grant_type=password
|
||||||
*/
|
*/
|
||||||
export async function loginUser(
|
export async function loginUser(
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string
|
||||||
userType: 'profissional' | 'paciente' | 'administrador'
|
|
||||||
): Promise<LoginResponse> {
|
): Promise<LoginResponse> {
|
||||||
let url = AUTH_ENDPOINTS.LOGIN;
|
const url = `${AUTH_ENDPOINTS.LOGIN}?grant_type=password`;
|
||||||
|
const headers = getLoginHeaders();
|
||||||
|
|
||||||
const payload = {
|
const body = JSON.stringify({ email, password });
|
||||||
email,
|
|
||||||
password,
|
console.log('[AUTH] Login request:', { url, email });
|
||||||
};
|
|
||||||
|
const response = await fetch(url, {
|
||||||
console.log('[AUTH-API] Iniciando login...', {
|
method: 'POST',
|
||||||
email,
|
headers,
|
||||||
userType,
|
body,
|
||||||
url,
|
|
||||||
payload,
|
|
||||||
timestamp: new Date().toLocaleTimeString()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('🔑 [AUTH-API] Credenciais sendo usadas no login:');
|
|
||||||
console.log('📧 Email:', email);
|
|
||||||
console.log('🔐 Senha:', password);
|
|
||||||
console.log('👤 UserType:', userType);
|
|
||||||
|
|
||||||
// Delay para visualizar na aba Network
|
return processResponse<LoginResponse>(response);
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('[AUTH-API] Enviando requisição de login...');
|
|
||||||
|
|
||||||
// Debug: Log request sem credenciais sensíveis
|
|
||||||
debugRequest('POST', url, getLoginHeaders(), payload);
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: getLoginHeaders(),
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`[AUTH-API] Login response: ${response.status} ${response.statusText}`, {
|
|
||||||
url: response.url,
|
|
||||||
status: response.status,
|
|
||||||
timestamp: new Date().toLocaleTimeString()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Se falhar, mostrar detalhes do erro
|
|
||||||
if (!response.ok) {
|
|
||||||
try {
|
|
||||||
const errorText = await response.text();
|
|
||||||
console.error('[AUTH-API] Erro detalhado:', {
|
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
body: errorText,
|
|
||||||
headers: Object.fromEntries(response.headers.entries())
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[AUTH-API] Não foi possível ler erro da resposta');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delay adicional para ver status code
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
|
||||||
|
|
||||||
const data = await processResponse<any>(response);
|
|
||||||
|
|
||||||
console.log('[AUTH] Dados recebidos da API:', data);
|
|
||||||
|
|
||||||
// Verificar se recebemos os dados necessários
|
|
||||||
if (!data || (!data.access_token && !data.token)) {
|
|
||||||
console.error('[AUTH] API não retornou token válido:', data);
|
|
||||||
throw new AuthenticationError(
|
|
||||||
'API não retornou token de acesso',
|
|
||||||
'NO_TOKEN_RECEIVED',
|
|
||||||
data
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adaptar resposta da sua API para o formato esperado
|
|
||||||
const adaptedResponse: LoginResponse = {
|
|
||||||
access_token: data.access_token || data.token,
|
|
||||||
token_type: data.token_type || "Bearer",
|
|
||||||
expires_in: data.expires_in || 3600,
|
|
||||||
user: {
|
|
||||||
id: data.user?.id || data.id || "1",
|
|
||||||
email: email,
|
|
||||||
name: data.user?.name || data.name || email.split('@')[0],
|
|
||||||
userType: userType,
|
|
||||||
profile: data.user?.profile || data.profile || {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('[AUTH-API] LOGIN REALIZADO COM SUCESSO!', {
|
|
||||||
token: adaptedResponse.access_token?.substring(0, 20) + '...',
|
|
||||||
user: {
|
|
||||||
email: adaptedResponse.user.email,
|
|
||||||
userType: adaptedResponse.user.userType
|
|
||||||
},
|
|
||||||
timestamp: new Date().toLocaleTimeString()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delay final para visualizar sucesso
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
|
||||||
|
|
||||||
return adaptedResponse;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[AUTH] Erro no login:', error);
|
|
||||||
|
|
||||||
if (error instanceof AuthenticationError) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new AuthenticationError(
|
|
||||||
'Email ou senha incorretos',
|
|
||||||
'INVALID_CREDENTIALS',
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serviço para fazer logout do usuário
|
* Serviço para fazer logout do usuário
|
||||||
|
* POST /auth/v1/logout
|
||||||
*/
|
*/
|
||||||
export async function logoutUser(token: string): Promise<void> {
|
export async function logoutUser(token: string): Promise<void> {
|
||||||
const url = AUTH_ENDPOINTS.LOGOUT;
|
const url = AUTH_ENDPOINTS.LOGOUT;
|
||||||
|
const headers = getAuthHeaders(token);
|
||||||
console.log('[AUTH-API] Fazendo logout na API...', {
|
|
||||||
url,
|
console.log('[AUTH] Logout request');
|
||||||
hasToken: !!token,
|
|
||||||
timestamp: new Date().toLocaleTimeString()
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delay para visualizar na aba Network
|
if (!response.ok && response.status !== 204) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 400));
|
await processResponse(response);
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('[AUTH-API] Enviando requisição de logout...');
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: getAuthHeaders(token),
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`[AUTH-API] Logout response: ${response.status} ${response.statusText}`, {
|
|
||||||
timestamp: new Date().toLocaleTimeString()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delay para ver status code
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 600));
|
|
||||||
|
|
||||||
// Logout pode retornar 200, 204 ou até 401 (se token já expirou)
|
|
||||||
// Todos são considerados "sucesso" para logout
|
|
||||||
if (response.ok || response.status === 401) {
|
|
||||||
console.log('[AUTH] Logout realizado com sucesso na API');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Se chegou aqui, algo deu errado mas não é crítico para logout
|
|
||||||
console.warn('[AUTH] API retornou status inesperado:', response.status);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[AUTH] Erro ao chamar API de logout:', error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Para logout, sempre continuamos mesmo com erro na API
|
|
||||||
// Isso evita que o usuário fique "preso" se a API estiver indisponível
|
|
||||||
console.log('[AUTH] Logout concluído (local sempre executado)');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serviço para renovar token JWT
|
* Serviço para renovar token JWT
|
||||||
|
* POST /auth/v1/token?grant_type=refresh_token
|
||||||
*/
|
*/
|
||||||
export async function refreshAuthToken(refreshToken: string): Promise<RefreshTokenResponse> {
|
export async function refreshAuthToken(refreshToken: string): Promise<RefreshTokenResponse> {
|
||||||
const url = AUTH_ENDPOINTS.REFRESH;
|
const url = `${AUTH_ENDPOINTS.LOGIN}?grant_type=refresh_token`;
|
||||||
|
const headers = getLoginHeaders();
|
||||||
|
|
||||||
|
const body = JSON.stringify({ refresh_token: refreshToken });
|
||||||
|
|
||||||
|
console.log('[AUTH] Refresh token request');
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
console.log('[AUTH] Renovando token');
|
return processResponse<RefreshTokenResponse>(response);
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json",
|
|
||||||
"apikey": ENV_CONFIG.SUPABASE_ANON_KEY,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await processResponse<RefreshTokenResponse>(response);
|
|
||||||
|
|
||||||
console.log('[AUTH] Token renovado com sucesso');
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[AUTH] Erro ao renovar token:', error);
|
|
||||||
|
|
||||||
if (error instanceof AuthenticationError) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new AuthenticationError(
|
|
||||||
'Não foi possível renovar a sessão',
|
|
||||||
'REFRESH_ERROR',
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serviço para obter dados do usuário atual
|
* Serviço para obter dados do usuário atual
|
||||||
|
* GET /auth/v1/user
|
||||||
*/
|
*/
|
||||||
export async function getCurrentUser(token: string): Promise<UserData> {
|
export async function getCurrentUser(token: string): Promise<UserData> {
|
||||||
const url = AUTH_ENDPOINTS.USER;
|
const url = AUTH_ENDPOINTS.USER;
|
||||||
|
const headers = getAuthHeaders(token);
|
||||||
|
|
||||||
|
console.log('[AUTH] Get current user request');
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
console.log('[AUTH] Obtendo dados do usuário atual');
|
return processResponse<UserData>(response);
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: getAuthHeaders(token),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await processResponse<UserData>(response);
|
|
||||||
|
|
||||||
console.log('[AUTH] Dados do usuário obtidos:', { id: data.id, email: data.email });
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[AUTH] Erro ao obter usuário atual:', error);
|
|
||||||
|
|
||||||
if (error instanceof AuthenticationError) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new AuthenticationError(
|
|
||||||
'Não foi possível obter dados do usuário',
|
|
||||||
'USER_DATA_ERROR',
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -6,7 +6,19 @@ export const API_CONFIG = {
|
|||||||
VERSION: "v1",
|
VERSION: "v1",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const AUTH_ENDPOINTS = ENV_CONFIG.AUTH_ENDPOINTS;
|
export const AUTH_ENDPOINTS = {
|
||||||
|
LOGIN: `${ENV_CONFIG.SUPABASE_URL}/auth/v1/token`,
|
||||||
|
LOGOUT: `${ENV_CONFIG.SUPABASE_URL}/auth/v1/logout`,
|
||||||
|
USER: `${ENV_CONFIG.SUPABASE_URL}/auth/v1/user`,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const FUNCTIONS_ENDPOINTS = {
|
||||||
|
USER_INFO: `${ENV_CONFIG.SUPABASE_URL}/functions/v1/user-info`,
|
||||||
|
// Use internal Next.js server route which performs privileged operations
|
||||||
|
// with the service role key. Client should call this route instead of
|
||||||
|
// calling the Supabase Edge Function directly.
|
||||||
|
CREATE_USER: `/api/create-user`,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const API_KEY = ENV_CONFIG.SUPABASE_ANON_KEY;
|
export const API_KEY = ENV_CONFIG.SUPABASE_ANON_KEY;
|
||||||
|
|
||||||
|
|||||||
@ -64,13 +64,8 @@ export const ENV_CONFIG = {
|
|||||||
SUPABASE_ANON_KEY,
|
SUPABASE_ANON_KEY,
|
||||||
PROJECT_REF: extractProjectRef(SUPABASE_URL),
|
PROJECT_REF: extractProjectRef(SUPABASE_URL),
|
||||||
|
|
||||||
// URLs dos endpoints de autenticação
|
// Observação: endpoints de autenticação (admin) foram removidos do bundle cliente.
|
||||||
AUTH_ENDPOINTS: {
|
// Operações administrativas e privilégios devem ser executados no servidor.
|
||||||
LOGIN: `${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
LOGOUT: `${SUPABASE_URL}/auth/v1/logout`,
|
|
||||||
REFRESH: `${SUPABASE_URL}/auth/v1/token?grant_type=refresh_token`,
|
|
||||||
USER: `${SUPABASE_URL}/auth/v1/user`,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Headers padrão
|
// Headers padrão
|
||||||
DEFAULT_HEADERS: {
|
DEFAULT_HEADERS: {
|
||||||
|
|||||||
172
susconecta/src/app/api/create-user/route.ts
Normal file
172
susconecta/src/app/api/create-user/route.ts
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { ENV_CONFIG } from '@/lib/env-config';
|
||||||
|
|
||||||
|
type CreateUserInput = {
|
||||||
|
email: string;
|
||||||
|
password?: string;
|
||||||
|
full_name: string;
|
||||||
|
phone?: string | null;
|
||||||
|
role?: string;
|
||||||
|
patient_id?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function gerarSenhaAleatoria(): string {
|
||||||
|
const num1 = Math.floor(Math.random() * 10);
|
||||||
|
const num2 = Math.floor(Math.random() * 10);
|
||||||
|
const num3 = Math.floor(Math.random() * 10);
|
||||||
|
return `senha${num1}${num2}${num3}!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeJwtPayload(token: string): any | null {
|
||||||
|
try {
|
||||||
|
const parts = token.split('.');
|
||||||
|
if (parts.length < 2) return null;
|
||||||
|
const payload = parts[1];
|
||||||
|
// Node environment: use Buffer
|
||||||
|
const json = Buffer.from(payload.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8');
|
||||||
|
return JSON.parse(json);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requesterHasAdminRole(serviceKey: string, requesterId: string | undefined): Promise<boolean> {
|
||||||
|
if (!requesterId) return false;
|
||||||
|
const url = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/user_roles?user_id=eq.${encodeURIComponent(String(requesterId))}`;
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { method: 'GET', headers: { apikey: serviceKey, Authorization: `Bearer ${serviceKey}`, Accept: 'application/json' } });
|
||||||
|
if (!res.ok) return false;
|
||||||
|
const arr = await res.json().catch(() => []);
|
||||||
|
if (!Array.isArray(arr)) return false;
|
||||||
|
for (const r of arr) {
|
||||||
|
const role = (r?.role || '').toString();
|
||||||
|
if (['admin', 'gestor', 'secretaria'].includes(role)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[create-user] failed to check requester roles', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
console.log('[create-user] POST handler invoked');
|
||||||
|
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||||
|
if (!serviceKey) {
|
||||||
|
return NextResponse.json({ error: 'server misconfigured: missing service role key' }, { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const authHeader = req.headers.get('authorization') || req.headers.get('Authorization');
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
return NextResponse.json({ error: 'Missing Authorization header' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientToken = authHeader.replace(/^Bearer\s+/i, '');
|
||||||
|
const payload = decodeJwtPayload(clientToken);
|
||||||
|
const requesterId = payload?.sub || payload?.user_id || payload?.id;
|
||||||
|
|
||||||
|
const allowed = await requesterHasAdminRole(serviceKey, requesterId);
|
||||||
|
if (!allowed) return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 });
|
||||||
|
|
||||||
|
let body: CreateUserInput;
|
||||||
|
try {
|
||||||
|
body = await req.json();
|
||||||
|
} catch (e) {
|
||||||
|
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body?.email || !body?.full_name) {
|
||||||
|
return NextResponse.json({ error: 'Missing required fields: email, full_name' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// call Edge Function create-user using service role key
|
||||||
|
const fnUrl = `${ENV_CONFIG.SUPABASE_URL}/functions/v1/create-user`;
|
||||||
|
try {
|
||||||
|
// ensure we have a password to show to the admin: generate one if caller didn't provide
|
||||||
|
const passwordToUse = body.password ?? gerarSenhaAleatoria();
|
||||||
|
|
||||||
|
const fnRes = await fetch(fnUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
apikey: serviceKey,
|
||||||
|
Authorization: `Bearer ${serviceKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: body.email,
|
||||||
|
password: passwordToUse,
|
||||||
|
full_name: body.full_name,
|
||||||
|
phone: body.phone ?? null,
|
||||||
|
role: body.role ?? 'user',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const fnText = await fnRes.text();
|
||||||
|
let fnJson: any = null;
|
||||||
|
try { fnJson = fnText ? JSON.parse(fnText) : null; } catch { fnJson = fnText; }
|
||||||
|
|
||||||
|
if (!fnRes.ok) {
|
||||||
|
return NextResponse.json({ error: 'create-user function failed', details: fnJson ?? fnText }, { status: fnRes.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdUser = fnJson?.user ?? fnJson ?? null;
|
||||||
|
const userId = createdUser?.id ?? createdUser?.user_id ?? null;
|
||||||
|
|
||||||
|
// include password in result when we generated/passed one so the client can display it
|
||||||
|
const result: any = { success: true, user: createdUser, password: passwordToUse ?? null, assignments: null, role_assignment: null };
|
||||||
|
|
||||||
|
const isProfessional = (body.role === 'medico' || body.role === 'enfermeiro');
|
||||||
|
|
||||||
|
// If request provided patient_id and user is professional, create assignment
|
||||||
|
if (isProfessional && body.patient_id && userId) {
|
||||||
|
try {
|
||||||
|
const assignUrl = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/patient_assignments`;
|
||||||
|
const assignRes = await fetch(assignUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json', apikey: serviceKey, Authorization: `Bearer ${serviceKey}` },
|
||||||
|
body: JSON.stringify({ patient_id: body.patient_id, user_id: userId, role: 'medico' }),
|
||||||
|
});
|
||||||
|
const assignText = await assignRes.text();
|
||||||
|
let assignJson: any = null; try { assignJson = assignText ? JSON.parse(assignText) : null; } catch { assignJson = assignText; }
|
||||||
|
result.assignments = assignRes.ok ? { ok: true, data: assignJson } : { ok: false, status: assignRes.status, details: assignJson ?? assignText };
|
||||||
|
} catch (e) {
|
||||||
|
result.assignments = { ok: false, error: String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: removed automatic assignment-by-email behavior.
|
||||||
|
// The server will no longer try to find a patient by email and create
|
||||||
|
// a patient_assignments row automatically when no `patient_id` is provided.
|
||||||
|
// This decision keeps assignment control in the UI: the admin should
|
||||||
|
// explicitly pick which patient (if any) to assign to the newly-created
|
||||||
|
// professional. If you want to re-enable automatic linking, restore this
|
||||||
|
// logic and ensure it's acceptable for your product policy.
|
||||||
|
if (isProfessional && !body.patient_id) {
|
||||||
|
console.log('[create-user] automatic patient-by-email assignment disabled; skip search');
|
||||||
|
result.assign_by_email = { ok: false, reason: 'automatic_assignment_disabled' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert into user_roles if requested
|
||||||
|
if (body.role && userId) {
|
||||||
|
try {
|
||||||
|
const rolesUrl = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/user_roles`;
|
||||||
|
const roleRes = await fetch(rolesUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', apikey: serviceKey, Authorization: `Bearer ${serviceKey}` }, body: JSON.stringify({ user_id: userId, role: body.role }) });
|
||||||
|
const roleText = await roleRes.text();
|
||||||
|
let roleJson: any = null; try { roleJson = roleText ? JSON.parse(roleText) : null; } catch { roleJson = roleText; }
|
||||||
|
result.role_assignment = roleRes.ok ? { ok: true, data: roleJson } : { ok: false, status: roleRes.status, details: roleJson ?? roleText };
|
||||||
|
} catch (e) {
|
||||||
|
result.role_assignment = { ok: false, error: String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(result);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[create-user] unexpected error', e);
|
||||||
|
return NextResponse.json({ error: 'internal error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({ ok: true, msg: 'create-user route alive' });
|
||||||
|
}
|
||||||
80
susconecta/src/app/api/link-paciente/route.ts
Normal file
80
susconecta/src/app/api/link-paciente/route.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import { ENV_CONFIG } from '@/lib/env-config'
|
||||||
|
|
||||||
|
type Body = {
|
||||||
|
patientId: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
try {
|
||||||
|
const body = (await req.json()) as Body
|
||||||
|
if (!body || !body.patientId || !body.email) {
|
||||||
|
return NextResponse.json({ error: 'patientId and email are required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const svcKey = process.env.SUPABASE_SERVICE_ROLE_KEY
|
||||||
|
if (!svcKey) {
|
||||||
|
console.error('[link-paciente] SUPABASE_SERVICE_ROLE_KEY is missing')
|
||||||
|
return NextResponse.json({ error: 'server misconfigured' }, { status: 500 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Buscar usuário no Auth por email usando endpoint admin
|
||||||
|
const usersUrl = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/admin/users?email=${encodeURIComponent(body.email)}`
|
||||||
|
const uRes = await fetch(usersUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
apikey: svcKey,
|
||||||
|
Authorization: `Bearer ${svcKey}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const uText = await uRes.text().catch(() => '')
|
||||||
|
let uJson: any = null
|
||||||
|
try { uJson = uText ? JSON.parse(uText) : null } catch { uJson = uText }
|
||||||
|
|
||||||
|
if (!uRes.ok) {
|
||||||
|
console.error('[link-paciente] failed to fetch user by email', uRes.status, uJson ?? uText)
|
||||||
|
return NextResponse.json({ error: 'failed to lookup user', details: uJson ?? uText }, { status: uRes.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
// The admin users endpoint can return an object or array depending on Supabase version; normalize
|
||||||
|
let foundUserId: string | null = null
|
||||||
|
if (Array.isArray(uJson) && uJson.length > 0) foundUserId = uJson[0].id
|
||||||
|
else if (uJson && uJson.id) foundUserId = uJson.id
|
||||||
|
|
||||||
|
if (!foundUserId) {
|
||||||
|
return NextResponse.json({ ok: false, found: false, message: 'No auth user found for this email' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Inserir role de aplicação (user/paciente) em user_roles
|
||||||
|
try {
|
||||||
|
const rolesUrl = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/user_roles`
|
||||||
|
const roleRes = await fetch(rolesUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
apikey: svcKey,
|
||||||
|
Authorization: `Bearer ${svcKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ user_id: foundUserId, role: 'user' }),
|
||||||
|
})
|
||||||
|
const roleText = await roleRes.text().catch(() => '')
|
||||||
|
let roleJson: any = null
|
||||||
|
try { roleJson = roleText ? JSON.parse(roleText) : null } catch { roleJson = roleText }
|
||||||
|
if (!roleRes.ok) {
|
||||||
|
console.error('[link-paciente] failed to insert user_roles', roleRes.status, roleJson ?? roleText)
|
||||||
|
return NextResponse.json({ ok: false, assigned: false, details: roleJson ?? roleText }, { status: roleRes.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ ok: true, userId: foundUserId, assigned: true, data: roleJson })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[link-paciente] error inserting user_roles', e)
|
||||||
|
return NextResponse.json({ ok: false, error: String(e) }, { status: 500 })
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[link-paciente] unexpected error', err)
|
||||||
|
return NextResponse.json({ error: 'internal error' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user