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:
M-Gabrielly 2025-10-14 17:02:26 -03:00
parent 05123e6c8f
commit fb578b2a7a
12 changed files with 604 additions and 1004 deletions

View File

@ -693,8 +693,13 @@ export default function PacientePage() {
}
// 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 (
<ProtectedRoute requiredUserType={["paciente"]}>
<ProtectedRoute>
<div className="min-h-screen bg-background text-foreground flex flex-col">
{/* 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">
@ -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>
</nav>
{/* 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">
{/* Toasts de feedback */}
{toast && (

View File

@ -12,7 +12,7 @@ export interface CredentialsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
email: string;
password: string;
password?: string | null;
userName: string;
userType: "médico" | "paciente";
}
@ -36,13 +36,14 @@ export function CredentialsDialog({
}
function handleCopyPassword() {
if (!password) return;
navigator.clipboard.writeText(password);
setCopiedPassword(true);
setTimeout(() => setCopiedPassword(false), 2000);
}
function handleCopyBoth() {
const text = `Email: ${email}\nSenha: ${password}`;
const text = password ? `Email: ${email}\nSenha: ${password}` : `Email: ${email}`;
navigator.clipboard.writeText(text);
}
@ -89,36 +90,40 @@ export function CredentialsDialog({
<div className="space-y-2">
<Label htmlFor="password">Senha Temporária</Label>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
id="password"
type={showPassword ? "text" : "password"}
value={password}
readOnly
className="bg-muted pr-10"
/>
{password ? (
<div className="flex gap-2">
<div className="relative flex-1">
<Input
id="password"
type={showPassword ? "text" : "password"}
value={password}
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
type="button"
variant="ghost"
variant="outline"
size="icon"
className="absolute right-0 top-0 h-full"
onClick={() => setShowPassword(!showPassword)}
title={showPassword ? "Ocultar senha" : "Mostrar senha"}
onClick={handleCopyPassword}
title="Copiar 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>
</div>
<Button
type="button"
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 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>
)}
</div>
</div>

View File

@ -24,13 +24,11 @@ import {
removerAnexoMedico,
MedicoInput,
Medico,
criarUsuarioMedico,
gerarSenhaAleatoria,
} from "@/lib/api";
;
import { CredentialsDialog } from "@/components/credentials-dialog";
import { buscarCepAPI } from "@/lib/api";
import { CredentialsDialog } from "@/components/credentials-dialog";
type FormacaoAcademica = {
instituicao: string;
@ -150,18 +148,17 @@ export function DoctorRegistrationForm({
const [errors, setErrors] = useState<Record<string, string>>({});
const [expanded, setExpanded] = useState({ dados: true, contato: false, endereco: false, obs: false, formacao: false, admin: 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 [photoPreview, setPhotoPreview] = useState<string | null>(null);
const [serverAnexos, setServerAnexos] = useState<any[]>([]);
// Estados para o dialog de credenciais
const [showCredentialsDialog, setShowCredentialsDialog] = useState(false);
const [credentials, setCredentials] = useState<{
email: string;
password: string;
userName: string;
userType: 'médico' | 'paciente';
} | null>(null);
// Não exibimos credenciais no cliente
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);
console.log("✅ Perfil do médico criado:", savedDoctorProfile);
// 2. Cria usuário no Supabase Auth (direto via /auth/v1/signup)
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
// Nota: criação de credenciais é feita server-side. Chamamos a rota administrativa
try {
// savedDoctorProfile may be an array or object depending on API
const docId = (savedDoctorProfile && (savedDoctorProfile.id || (Array.isArray(savedDoctorProfile) ? savedDoctorProfile[0]?.id : undefined))) || null;
if (docId) {
console.log('[DoctorForm] Vinculando user_id ao médico:', { doctorId: docId, userId: authResponse.user.id });
// dynamic import to avoid circular deps in some bundlers
const api = await import('@/lib/api');
if (api && typeof api.vincularUserIdMedico === 'function') {
await api.vincularUserIdMedico(String(docId), String(authResponse.user.id));
console.log('[DoctorForm] user_id vinculado com sucesso.');
const token = typeof window !== 'undefined' ? (localStorage.getItem('auth_token') || localStorage.getItem('token') || sessionStorage.getItem('auth_token') || sessionStorage.getItem('token')) : null;
const headers: Record<string,string> = { 'Content-Type': 'application/json' };
if (token) headers['Authorization'] = `Bearer ${token}`;
const createResp = await fetch('/api/create-user', {
method: 'POST',
headers,
body: JSON.stringify({ email: savedDoctorProfile.email, full_name: savedDoctorProfile.full_name, role: 'medico' })
});
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 {
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) {
console.warn('[DoctorForm] Falha ao vincular user_id ao médico:', linkErr);
} catch (e) {
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
setCredentials({
email: authResponse.email,
password: authResponse.password,
userName: form.full_name,
userType: 'médico',
});
setShowCredentialsDialog(true);
// 4. Limpa formulário
// Limpa formulário
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
// 5. Notifica componente pai
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) {
console.error("❌ Erro no handleSubmit:", err);
@ -1009,28 +962,7 @@ async function handleSubmit(ev: React.FormEvent) {
<>
<div className="space-y-6">{content}</div>
{/* Dialog de credenciais */}
{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}
/>
)}
{/* Credenciais não exibidas no cliente */}
</>
);
}
@ -1048,23 +980,15 @@ async function handleSubmit(ev: React.FormEvent) {
</DialogContent>
</Dialog>
{/* Dialog de credenciais */}
{credentials && (
<CredentialsDialog
open={showCredentialsDialog}
onOpenChange={(open) => {
setShowCredentialsDialog(open);
if (!open) {
setCredentials(null);
onOpenChange?.(false);
}
}}
email={credentials.email}
password={credentials.password}
userName={credentials.userName}
userType={credentials.userType}
/>
)}
{/* Exibe credenciais retornadas pelo servidor (quando houver) */}
<CredentialsDialog
open={credOpen}
onOpenChange={(v) => setCredOpen(v)}
email={credEmail}
password={credPassword}
userName={credUserName}
userType={credUserType}
/>
</>
);
}

View File

@ -25,13 +25,12 @@ import {
listarAnexos,
removerAnexo,
buscarPacientePorId,
criarUsuarioPaciente,
criarPaciente,
} from "@/lib/api";
import { CredentialsDialog } from "@/components/credentials-dialog";
import { validarCPFLocal } from "@/lib/utils";
import { verificarCpfDuplicado } from "@/lib/api";
import { CredentialsDialog } from "@/components/credentials-dialog";
type Mode = "create" | "edit";
@ -100,18 +99,17 @@ export function PatientRegistrationForm({
const [errors, setErrors] = useState<Record<string, string>>({});
const [expanded, setExpanded] = useState({ dados: true, contato: false, endereco: false, obs: 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 [photoPreview, setPhotoPreview] = useState<string | null>(null);
const [serverAnexos, setServerAnexos] = useState<any[]>([]);
// Estados para o dialog de credenciais
const [showCredentialsDialog, setShowCredentialsDialog] = useState(false);
const [credentials, setCredentials] = useState<{
email: string;
password: string;
userName: string;
userType: 'médico' | 'paciente';
} | null>(null);
// Credenciais/usuário não são criados no cliente
const title = useMemo(() => (mode === "create" ? "Cadastro de Paciente" : "Editar Paciente"), [mode]);
@ -304,94 +302,45 @@ export function PatientRegistrationForm({
const savedPatientProfile = await criarPaciente(patientPayload);
console.log(" Perfil do paciente criado:", savedPatientProfile);
if (form.email && form.email.includes('@')) {
console.log(" Criando usuário de autenticação (paciente)...");
// Depois de criar perfil, solicitar criação de credenciais server-side
try {
const userResponse = await criarUsuarioPaciente({
email: form.email,
full_name: form.nome,
phone_mobile: form.telefone,
const token = typeof window !== 'undefined' ? (localStorage.getItem('auth_token') || localStorage.getItem('token') || sessionStorage.getItem('auth_token') || sessionStorage.getItem('token')) : null;
const headers: Record<string,string> = { 'Content-Type': 'application/json' };
if (token) headers['Authorization'] = `Bearer ${token}`;
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) {
console.log(" Usuário de autenticação criado:", userResponse.user);
// Mostra credenciais no dialog usando as credenciais retornadas
setCredentials({
email: userResponse.email ?? form.email,
password: userResponse.password ?? '',
userName: form.nome,
userType: 'paciente',
});
setShowCredentialsDialog(true);
// 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);
if (resp.ok) {
const json = await resp.json();
const email = json?.email ?? json?.user?.email ?? savedPatientProfile.email;
const password = json?.password ?? json?.user?.password ?? json?.user?.temp_password ?? null;
setCredEmail(email ?? savedPatientProfile.email ?? '');
setCredUserName(savedPatientProfile.full_name ?? '');
if (password) {
setCredPassword(String(password));
setCredOpen(true);
} else {
alert('Paciente cadastrado com sucesso. Conta criada, mas a senha temporária não foi retornada.');
}
// Limpa formulário mas NÃO fecha ainda - fechará quando o dialog de credenciais fechar
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
onSaved?.(savedPatientProfile);
return;
} 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) {
console.error(" Erro ao criar usuário via signup:", 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 {
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;
} catch (e) {
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).');
}
} 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) {
console.error("❌ Erro no handleSubmit:", err);
@ -747,28 +696,15 @@ export function PatientRegistrationForm({
<>
<div className="space-y-6">{content}</div>
{/* Dialog de credenciais */}
{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}
/>
)}
{/* Credenciais não exibidas no cliente */}
<CredentialsDialog
open={credOpen}
onOpenChange={(v) => setCredOpen(v)}
email={credEmail}
password={credPassword}
userName={credUserName}
userType={credUserType}
/>
</>
);
}
@ -786,23 +722,15 @@ export function PatientRegistrationForm({
</DialogContent>
</Dialog>
{/* Dialog de credenciais */}
{credentials && (
<CredentialsDialog
open={showCredentialsDialog}
onOpenChange={(open) => {
setShowCredentialsDialog(open);
if (!open) {
setCredentials(null);
onOpenChange?.(false);
}
}}
email={credentials.email}
password={credentials.password}
userName={credentials.userName}
userType={credentials.userType}
/>
)}
{/* Credenciais não exibidas no cliente */}
<CredentialsDialog
open={credOpen}
onOpenChange={(v) => setCredOpen(v)}
email={credEmail}
password={credPassword}
userName={credUserName}
userType={credUserType}
/>
</>
);
}

File diff suppressed because one or more lines are too long

View File

@ -106,31 +106,15 @@ export function AuthProvider({ children }: { children: ReactNode }) {
if (isExpired(storedToken)) {
console.log('[AUTH] Token expirado - tentando renovar...')
await new Promise(resolve => setTimeout(resolve, 1000))
const refreshToken = localStorage.getItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN)
if (refreshToken && !isExpired(refreshToken)) {
// Tentar renovar via HTTP client (que já tem a lógica)
try {
await httpClient.get('/auth/v1/me') // Trigger refresh se necessário
// 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))
}
// Refresh automático foi desativado no cliente. A renovação de tokens
// deve ser feita via backend seguro. Se desejar habilitar refresh no
// cliente, implemente um flow server-side ou use o SDK autenticado.
console.log('[AUTH] Refresh token presente, mas refresh automático desativado no cliente')
}
clearAuthData()
return
}
@ -138,38 +122,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
// Restaurar sessão válida
const userData = JSON.parse(storedUser) as UserData
setToken(storedToken)
// Tentar buscar profile consolidado (user-info) e mesclar
try {
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)
}
// Nota: chamadas a user-info foram removidas do cliente. Mantemos os dados do
// usuário que já estavam persistidos localmente sem tentar reconciliar com funções remotas.
setUser(userData)
setAuthStatus('authenticated')
@ -197,74 +151,57 @@ export function AuthProvider({ children }: { children: ReactNode }) {
try {
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
try {
const infoRes = await fetch(`${ENV_CONFIG.SUPABASE_URL}/functions/v1/user-info`, {
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)
}
// Nota: busca de user-info e reconciliação via funções server-side foi removida do cliente.
// Mantemos o objeto response.user retornado pelo fluxo de autenticação sem tentativas
// adicionais de mesclagem aqui.
// Salva dados iniciais (token + user retornado pelo login)
saveAuthData(
response.access_token,
response.user,
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')
return true

View File

@ -1,6 +1,7 @@
// lib/api.ts
import { ENV_CONFIG } from '@/lib/env-config';
import { FUNCTIONS_ENDPOINTS } from '@/lib/config';
// Use ENV_CONFIG for SUPABASE URL and anon key in frontend
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.
*/
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);
// Operação administrativa removida do cliente.
// Atribuição de roles é feita exclusivamente via backend com a service role key.
throw new TypeError('assignRoleServerSide removido do cliente. Use APIs administrativas no servidor para atribuir roles.');
}
// ===== PACIENTES (Extra: verificação de CPF duplicado) =====
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.
*/
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 payload = { user_id: String(userId) };
const res = await fetch(url, {
@ -1024,11 +1026,15 @@ export type UserRole = {
created_at: string;
};
/**
* Listar roles de usuários
* GET /rest/v1/user_roles
*/
export async function listarUserRoles(): Promise<UserRole[]> {
const url = `${API_BASE}/rest/v1/user_roles`;
const res = await fetch(url, {
method: "GET",
headers: baseHeaders(),
const url = `${REST}/user_roles`;
const res = await fetch(url, {
method: 'GET',
headers: baseHeaders()
});
return await parse<UserRole[]>(res);
}
@ -1096,21 +1102,47 @@ export type UserInfo = {
permissions: Permissions;
};
/**
* Obter dados do usuário atual via Supabase Auth
* GET /auth/v1/user
*/
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, {
method: "GET",
headers: baseHeaders(),
method: 'GET',
headers: {
...baseHeaders(),
'Authorization': `Bearer ${token}`,
},
});
return await parse<CurrentUser>(res);
}
/**
* Obter informações completas do usuário
* GET /functions/v1/user-info
*/
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, {
method: "GET",
headers: baseHeaders(),
method: 'GET',
headers: {
...baseHeaders(),
'Authorization': `Bearer ${token}`,
},
});
return await parse<UserInfo>(res);
}
@ -1150,391 +1182,52 @@ export function gerarSenhaAleatoria(): string {
return `senha${num1}${num2}${num3}!`;
}
/**
* Criar novo usuário
* POST /functions/v1/create-user
*/
export async function criarUsuario(input: CreateUserInput): Promise<CreateUserResponse> {
const url = `${API_BASE}/functions/v1/create-user`;
const res = await fetch(url, {
method: "POST",
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 token = getAuthToken();
if (!token) {
throw new Error('Token não encontrado. Faça login primeiro.');
}
const url = FUNCTIONS_ENDPOINTS.CREATE_USER;
// Log do request para debugging (visível no console do navegador)
try {
const response = await fetch(signupUrl, {
method: "POST",
console.debug('[USER API] POST', url, 'body:', input);
} catch (e) {
// ignore logging errors
}
let response: Response;
try {
response = await fetch(url, {
method: 'POST',
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"apikey": ENV_CONFIG.SUPABASE_ANON_KEY,
...baseHeaders(),
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify(payload),
body: JSON.stringify(input),
});
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;
} catch (networkErr) {
console.error('[USER API] Network error calling create-user', networkErr);
throw new Error('Erro de rede ao tentar criar usuário. Verifique sua conexão e tente novamente.');
}
// Parse com captura para logar resposta bruta e final
try {
const parsed = await parse<CreateUserResponse>(response);
try { console.debug('[USER API] create-user response:', parsed); } catch (_) {}
return parsed;
} 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);
throw apiErr;
}
}
// ============================================
// 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) =====
export async function buscarCepAPI(cep: string): Promise<{

View File

@ -83,245 +83,85 @@ async function processResponse<T>(response: Response): Promise<T> {
/**
* Serviço para fazer login e obter token JWT
* POST /auth/v1/token?grant_type=password
*/
export async function loginUser(
email: string,
password: string,
userType: 'profissional' | 'paciente' | 'administrador'
password: string
): Promise<LoginResponse> {
let url = AUTH_ENDPOINTS.LOGIN;
const url = `${AUTH_ENDPOINTS.LOGIN}?grant_type=password`;
const headers = getLoginHeaders();
const payload = {
email,
password,
};
console.log('[AUTH-API] Iniciando login...', {
email,
userType,
url,
payload,
timestamp: new Date().toLocaleTimeString()
const body = JSON.stringify({ email, password });
console.log('[AUTH] Login request:', { url, email });
const response = await fetch(url, {
method: 'POST',
headers,
body,
});
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
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
);
}
return processResponse<LoginResponse>(response);
}
/**
* Serviço para fazer logout do usuário
* POST /auth/v1/logout
*/
export async function logoutUser(token: string): Promise<void> {
const url = AUTH_ENDPOINTS.LOGOUT;
console.log('[AUTH-API] Fazendo logout na API...', {
url,
hasToken: !!token,
timestamp: new Date().toLocaleTimeString()
const headers = getAuthHeaders(token);
console.log('[AUTH] Logout request');
const response = await fetch(url, {
method: 'POST',
headers,
});
// Delay para visualizar na aba Network
await new Promise(resolve => setTimeout(resolve, 400));
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);
if (!response.ok && response.status !== 204) {
await processResponse(response);
}
// 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
* POST /auth/v1/token?grant_type=refresh_token
*/
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');
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
);
}
return processResponse<RefreshTokenResponse>(response);
}
/**
* Serviço para obter dados do usuário atual
* GET /auth/v1/user
*/
export async function getCurrentUser(token: string): Promise<UserData> {
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');
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
);
}
return processResponse<UserData>(response);
}
/**

View File

@ -6,7 +6,19 @@ export const API_CONFIG = {
VERSION: "v1",
} 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;

View File

@ -64,13 +64,8 @@ export const ENV_CONFIG = {
SUPABASE_ANON_KEY,
PROJECT_REF: extractProjectRef(SUPABASE_URL),
// URLs dos endpoints de autenticação
AUTH_ENDPOINTS: {
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`,
},
// Observação: endpoints de autenticação (admin) foram removidos do bundle cliente.
// Operações administrativas e privilégios devem ser executados no servidor.
// Headers padrão
DEFAULT_HEADERS: {

View 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' });
}

View 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 })
}
}