diff --git a/susconecta/components/forms/patient-registration-form.tsx b/susconecta/components/forms/patient-registration-form.tsx index 9cbb0e5..e1286fb 100644 --- a/susconecta/components/forms/patient-registration-form.tsx +++ b/susconecta/components/forms/patient-registration-form.tsx @@ -232,149 +232,175 @@ export function PatientRegistrationForm({ }; } - async function handleSubmit(ev: React.FormEvent) { - ev.preventDefault(); - if (!validateLocal()) return; + async function handleSubmit(ev: React.FormEvent) { + ev.preventDefault(); + if (!validateLocal()) return; - try { - if (!validarCPFLocal(form.cpf)) { - setErrors((e) => ({ ...e, cpf: "CPF inválido" })); - return; - } - if (mode === "create") { - const existe = await verificarCpfDuplicado(form.cpf); - if (existe) { - setErrors((e) => ({ ...e, cpf: "CPF já cadastrado no sistema" })); - return; - } - } - } catch (err) { - console.error("Erro ao validar CPF", err); - setErrors({ submit: "Erro ao validar CPF." }); + try { + if (!validarCPFLocal(form.cpf)) { + setErrors((e) => ({ ...e, cpf: "CPF inválido" })); return; } + if (mode === "create") { + const existe = await verificarCpfDuplicado(form.cpf); + if (existe) { + setErrors((e) => ({ ...e, cpf: "CPF já cadastrado no sistema" })); + return; + } + } + } catch (err) { + console.error("Erro ao validar CPF", err); + setErrors({ submit: "Erro ao validar CPF." }); + return; + } - setSubmitting(true); - try { - if (mode === "edit") { - if (patientId == null) throw new Error("Paciente inexistente para edição"); - const payload = toPayload(); - const saved = await atualizarPaciente(String(patientId), payload); - onSaved?.(saved); - alert("Paciente atualizado com sucesso!"); - + setSubmitting(true); + try { + if (mode === "edit") { + if (patientId == null) throw new Error("Paciente inexistente para edição"); + const payload = toPayload(); + const saved = await atualizarPaciente(String(patientId), payload); + onSaved?.(saved); + alert("Paciente atualizado com sucesso!"); + + setForm(initial); + setPhotoPreview(null); + setServerAnexos([]); + if (inline) onClose?.(); + else onOpenChange?.(false); + } else { + // --- NOVA LÓGICA DE CRIAÇÃO --- + const patientPayload = toPayload(); + 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)..."); + try { + const userResponse = await criarUsuarioPaciente({ + email: form.email, + full_name: form.nome, + phone_mobile: form.telefone, + }); + + if (userResponse.success && userResponse.user) { + console.log("✅ Usuário de autenticação criado:", userResponse.user); + + // Mostra credenciais no dialog + setCredentials({ + email: userResponse.email ?? form.email, + password: userResponse.password ?? "", + userName: form.nome, + userType: "paciente", + }); + setShowCredentialsDialog(true); + + // 🔗 Vincular paciente ao usuário pelo EMAIL (não mais user_id) + try { + const apiMod = await import("@/lib/api"); + const pacienteId = + savedPatientProfile?.id || + (savedPatientProfile && (savedPatientProfile as any).id); + const userEmail = + form.email?.trim() || (userResponse.user as any)?.email; + + if ( + pacienteId && + userEmail && + typeof apiMod.vincularPacientePorEmail === "function" + ) { + console.log( + "[PatientForm] Vinculando paciente pelo email:", + pacienteId, + userEmail + ); + try { + await apiMod.vincularPacientePorEmail(pacienteId, userEmail); + console.log( + "[PatientForm] Paciente vinculado com sucesso ao email." + ); + } catch (linkErr) { + console.warn( + "[PatientForm] Falha ao vincular paciente pelo email:", + linkErr + ); + } + } + } catch (dynErr) { + console.warn( + "[PatientForm] Não foi possível importar helper para vincular paciente por email:", + dynErr + ); + } + + // Limpa o formulário mas mantém o dialog de credenciais aberto + setForm(initial); + setPhotoPreview(null); + setServerAnexos([]); + onSaved?.(savedPatientProfile); + return; + } else { + throw new Error( + (userResponse as any).message || + "Falhou ao criar o usuário de acesso." + ); + } + } catch (userError: any) { + console.error("❌ Erro ao criar usuário via signup:", userError); + + 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); setForm(initial); setPhotoPreview(null); setServerAnexos([]); if (inline) onClose?.(); else onOpenChange?.(false); - - } else { - // --- NOVA LÓGICA DE CRIAÇÃO --- - const patientPayload = toPayload(); - 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)..."); - try { - const userResponse = await criarUsuarioPaciente({ - email: form.email, - full_name: form.nome, - phone_mobile: form.telefone, - }); - - 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); - } - - // 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."); - } - } 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; - } - } 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); - // Exibe mensagem amigável ao usuário - const userMessage = err?.message?.includes("toPayload") || err?.message?.includes("is not defined") + } + } catch (err: any) { + console.error("❌ Erro no handleSubmit:", err); + const userMessage = + err?.message?.includes("toPayload") || err?.message?.includes("is not defined") ? "Erro ao processar os dados do formulário. Por favor, verifique os campos e tente novamente." : err?.message || "Erro ao salvar paciente. Por favor, tente novamente."; - setErrors({ submit: userMessage }); - } finally { - setSubmitting(false); - } + setErrors({ submit: userMessage }); + } finally { + setSubmitting(false); } +} + function handlePhoto(e: React.ChangeEvent) { const f = e.target.files?.[0]; diff --git a/susconecta/hooks/useAuth.tsx b/susconecta/hooks/useAuth.tsx index 5871f03..250b38a 100644 --- a/susconecta/hooks/useAuth.tsx +++ b/susconecta/hooks/useAuth.tsx @@ -1,339 +1,408 @@ -'use client' -import { createContext, useContext, useEffect, useState, ReactNode, useCallback, useMemo, useRef } from 'react' -import { useRouter } from 'next/navigation' -import { loginUser, logoutUser, AuthenticationError } from '@/lib/auth' -import { getUserInfo } from '@/lib/api' -import { ENV_CONFIG } from '@/lib/env-config' -import { isExpired, parseJwt } from '@/lib/jwt' -import { httpClient } from '@/lib/http' -import type { - AuthContextType, - UserData, +"use client"; +import { + createContext, + useContext, + useEffect, + useState, + ReactNode, + useCallback, + useMemo, + useRef, +} from "react"; +import { useRouter } from "next/navigation"; +import { loginUser, logoutUser, AuthenticationError } from "@/lib/auth"; +import { getUserInfo } from "@/lib/api"; +import { ENV_CONFIG } from "@/lib/env-config"; +import { isExpired, parseJwt } from "@/lib/jwt"; +import { httpClient } from "@/lib/http"; +import type { + AuthContextType, + UserData, AuthStatus, - UserType -} from '@/types/auth' -import { AUTH_STORAGE_KEYS, LOGIN_ROUTES } from '@/types/auth' + UserType, +} from "@/types/auth"; +import { AUTH_STORAGE_KEYS, LOGIN_ROUTES } from "@/types/auth"; -const AuthContext = createContext(undefined) +const AuthContext = createContext(undefined); export function AuthProvider({ children }: { children: ReactNode }) { - const [authStatus, setAuthStatus] = useState('loading') - const [user, setUser] = useState(null) - const [token, setToken] = useState(null) - const router = useRouter() - const hasInitialized = useRef(false) + const [authStatus, setAuthStatus] = useState("loading"); + const [user, setUser] = useState(null); + const [token, setToken] = useState(null); + const router = useRouter(); + const hasInitialized = useRef(false); // Utilitários de armazenamento memorizados const clearAuthData = useCallback(() => { - if (typeof window !== 'undefined') { - localStorage.removeItem(AUTH_STORAGE_KEYS.TOKEN) - localStorage.removeItem(AUTH_STORAGE_KEYS.USER) - localStorage.removeItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN) + if (typeof window !== "undefined") { + localStorage.removeItem(AUTH_STORAGE_KEYS.TOKEN); + localStorage.removeItem(AUTH_STORAGE_KEYS.USER); + localStorage.removeItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN); // Manter USER_TYPE para redirecionamento correto } - setUser(null) - setToken(null) - setAuthStatus('unauthenticated') - console.log('[AUTH] Dados de autenticação limpos - logout realizado') - }, []) + setUser(null); + setToken(null); + setAuthStatus("unauthenticated"); + console.log("[AUTH] Dados de autenticação limpos - logout realizado"); + }, []); - const saveAuthData = useCallback(( - accessToken: string, - userData: UserData, - refreshToken?: string - ) => { - try { - if (typeof window !== 'undefined') { - // Persistir dados de forma atômica - localStorage.setItem(AUTH_STORAGE_KEYS.TOKEN, accessToken) - localStorage.setItem(AUTH_STORAGE_KEYS.USER, JSON.stringify(userData)) - localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, userData.userType) - - if (refreshToken) { - localStorage.setItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN, refreshToken) + const saveAuthData = useCallback( + (accessToken: string, userData: UserData, refreshToken?: string) => { + try { + if (typeof window !== "undefined") { + // Persistir dados de forma atômica + localStorage.setItem(AUTH_STORAGE_KEYS.TOKEN, accessToken); + localStorage.setItem( + AUTH_STORAGE_KEYS.USER, + JSON.stringify(userData) + ); + localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, userData.userType); + + if (refreshToken) { + localStorage.setItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN, refreshToken); + } } + + setToken(accessToken); + setUser(userData); + setAuthStatus("authenticated"); + + console.log("[AUTH] LOGIN realizado - Dados salvos!", { + userType: userData.userType, + email: userData.email, + timestamp: new Date().toLocaleTimeString(), + }); + } catch (error) { + console.error("[AUTH] Erro ao salvar dados:", error); + clearAuthData(); } - - setToken(accessToken) - setUser(userData) - setAuthStatus('authenticated') - - console.log('[AUTH] LOGIN realizado - Dados salvos!', { - userType: userData.userType, - email: userData.email, - timestamp: new Date().toLocaleTimeString() - }) - } catch (error) { - console.error('[AUTH] Erro ao salvar dados:', error) - clearAuthData() - } - }, [clearAuthData]) + }, + [clearAuthData] + ); // Verificação inicial de autenticação const checkAuth = useCallback(async (): Promise => { - if (typeof window === 'undefined') { - setAuthStatus('unauthenticated') - return + if (typeof window === "undefined") { + setAuthStatus("unauthenticated"); + return; } try { - const storedToken = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN) - const storedUser = localStorage.getItem(AUTH_STORAGE_KEYS.USER) + const storedToken = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN); + const storedUser = localStorage.getItem(AUTH_STORAGE_KEYS.USER); - console.log('[AUTH] Verificando sessão...', { + console.log("[AUTH] Verificando sessão...", { hasToken: !!storedToken, hasUser: !!storedUser, - timestamp: new Date().toLocaleTimeString() - }) + timestamp: new Date().toLocaleTimeString(), + }); // Pequeno delay para visualizar logs - await new Promise(resolve => setTimeout(resolve, 800)) + await new Promise((resolve) => setTimeout(resolve, 800)); if (!storedToken || !storedUser) { - console.log('[AUTH] Dados ausentes - sessão inválida') - await new Promise(resolve => setTimeout(resolve, 500)) - clearAuthData() - return + console.log("[AUTH] Dados ausentes - sessão inválida"); + await new Promise((resolve) => setTimeout(resolve, 500)); + clearAuthData(); + return; } // Verificar se token está expirado 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) + 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 - + 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 - + 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 + 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)) + console.log(" [AUTH] Falha no refresh automático"); + await new Promise((resolve) => setTimeout(resolve, 400)); } } - - clearAuthData() - return + + clearAuthData(); + return; } // Restaurar sessão válida - const userData = JSON.parse(storedUser) as UserData - setToken(storedToken) + const userData = JSON.parse(storedUser) as UserData; + setToken(storedToken); // Tentar buscar profile consolidado (user-info) e mesclar try { - const info = await getUserInfo() + 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 } + userData.profile = { ...userData.profile, ...mapped }; } else { - userData.profile = mapped + 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') { - localStorage.setItem(AUTH_STORAGE_KEYS.USER, JSON.stringify(userData)) + if (typeof window !== "undefined") { + localStorage.setItem( + AUTH_STORAGE_KEYS.USER, + JSON.stringify(userData) + ); } } catch (e) { - console.warn('[AUTH] Falha ao persistir user (profile) no localStorage:', 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) + console.warn( + "[AUTH] Falha ao buscar user-info na restauração de sessão:", + err + ); } - setUser(userData) - setAuthStatus('authenticated') + setUser(userData); + setAuthStatus("authenticated"); - console.log('[AUTH] Sessão RESTAURADA com sucesso!', { + console.log("[AUTH] Sessão RESTAURADA com sucesso!", { userId: userData.id, userType: userData.userType, email: userData.email, - timestamp: new Date().toLocaleTimeString() - }) - await new Promise(resolve => setTimeout(resolve, 1000)) - + timestamp: new Date().toLocaleTimeString(), + }); + await new Promise((resolve) => setTimeout(resolve, 1000)); } catch (error) { - console.error('[AUTH] Erro na verificação:', error) - clearAuthData() + console.error("[AUTH] Erro na verificação:", error); + clearAuthData(); } - }, [clearAuthData]) + }, [clearAuthData]); // Login memoizado - const login = useCallback(async ( - email: string, - password: string, - userType: UserType - ): Promise => { - try { - console.log('[AUTH] Iniciando login:', { email, userType }) - - const response = await loginUser(email, password, userType) - - // Após receber token, buscar roles/permissions reais e reconciliar userType + const login = useCallback( + async ( + email: string, + password: string, + userType: UserType + ): Promise => { 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, - } - }) + console.log("[AUTH] Iniciando login:", { email, userType }); - if (infoRes.ok) { - const info = await infoRes.json().catch(() => null) - const roles: string[] = Array.isArray(info?.roles) ? info.roles : (info?.roles ? [info.roles] : []) + const response = await loginUser(email, password, userType); - // 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' - } + // 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, + }, + } + ); - // Atualizar userType caso seja diferente - if (response.user && response.user.userType !== derived) { - response.user.userType = derived - console.log('[AUTH] userType reconciled from roles ->', derived) - } - } 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) - } + if (infoRes.ok) { + const info = await infoRes.json().catch(() => null); + const roles: string[] = Array.isArray(info?.roles) + ? info.roles + : info?.roles + ? [info.roles] + : []; - // 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 } + // Derivar tipo de usuário a partir dos roles + // Derivar tipo de usuário a partir dos roles retornados pela API + let derived: UserType = userType; // 🔹 começa respeitando o tipo da tela de login + + if ( + roles.includes("admin") || + roles.includes("gestor") || + roles.includes("secretaria") + ) { + derived = "administrador"; + } else if ( + roles.includes("medico") || + roles.includes("enfermeiro") + ) { + derived = "profissional"; + } else if (roles.length === 0 && userType) { + // 🔹 se a API não retornou roles, mantemos o tipo informado pela tela + derived = userType; + } + + // 🔹 Se o tipo da tela e o das roles divergirem, prioriza o da tela (exceto admin) + if ( + response.user && + response.user.userType !== derived && + userType !== "administrador" + ) { + response.user.userType = userType; + console.log( + "[AUTH] userType mantido da tela de login ->", + userType + ); + } else if (response.user && response.user.userType !== derived) { + response.user.userType = derived; + console.log( + "[AUTH] userType reconciliado a partir das roles ->", + derived + ); + } } else { - response.user.profile = mapped + 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 + ); } - } catch (err) { - console.warn('[AUTH] Falha 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 + ); + } + + saveAuthData( + response.access_token, + response.user, + response.refresh_token + ); + + console.log("[AUTH] Login realizado com sucesso"); + return true; + } catch (error) { + console.error("[AUTH] Erro no login:", error); + + if (error instanceof AuthenticationError) { + throw error; + } + + throw new AuthenticationError( + "Erro inesperado durante o login", + "UNKNOWN_ERROR", + error + ); } - - saveAuthData( - response.access_token, - response.user, - response.refresh_token - ) - - console.log('[AUTH] Login realizado com sucesso') - return true - - } catch (error) { - console.error('[AUTH] Erro no login:', error) - - if (error instanceof AuthenticationError) { - throw error - } - - throw new AuthenticationError( - 'Erro inesperado durante o login', - 'UNKNOWN_ERROR', - error - ) - } - }, [saveAuthData]) + }, + [saveAuthData] + ); // Logout memoizado const logout = useCallback(async (): Promise => { - console.log('[AUTH] Iniciando logout') - - const currentUserType = user?.userType || - (typeof window !== 'undefined' ? localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE) : null) || - 'profissional' - + console.log("[AUTH] Iniciando logout"); + + const currentUserType = + user?.userType || + (typeof window !== "undefined" + ? localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE) + : null) || + "profissional"; + try { if (token) { - await logoutUser(token) - console.log('[AUTH] Logout realizado na API') + await logoutUser(token); + console.log("[AUTH] Logout realizado na API"); } } catch (error) { - console.error('[AUTH] Erro no logout da API:', error) + console.error("[AUTH] Erro no logout da API:", error); } - clearAuthData() - + clearAuthData(); + // Redirecionamento baseado no tipo de usuário - const loginRoute = LOGIN_ROUTES[currentUserType as UserType] || '/login' - - console.log('[AUTH] Redirecionando para:', loginRoute) - - if (typeof window !== 'undefined') { - window.location.href = loginRoute + const loginRoute = LOGIN_ROUTES[currentUserType as UserType] || "/login"; + + console.log("[AUTH] Redirecionando para:", loginRoute); + + if (typeof window !== "undefined") { + window.location.href = loginRoute; } - }, [user?.userType, token, clearAuthData]) + }, [user?.userType, token, clearAuthData]); // Refresh token memoizado (usado pelo HTTP client) const refreshToken = useCallback(async (): Promise => { // Esta função é principalmente para compatibilidade // O refresh real é feito pelo HTTP client - return false - }, []) + return false; + }, []); // Getters memorizados - const contextValue = useMemo(() => ({ - authStatus, - user, - token, - login, - logout, - refreshToken - }), [authStatus, user, token, login, logout, refreshToken]) + const contextValue = useMemo( + () => ({ + authStatus, + user, + token, + login, + logout, + refreshToken, + }), + [authStatus, user, token, login, logout, refreshToken] + ); // Inicialização única useEffect(() => { - if (!hasInitialized.current && typeof window !== 'undefined') { - hasInitialized.current = true - checkAuth() + if (!hasInitialized.current && typeof window !== "undefined") { + hasInitialized.current = true; + checkAuth(); } - }, [checkAuth]) + }, [checkAuth]); return ( - - {children} - - ) + {children} + ); } export const useAuth = () => { - const context = useContext(AuthContext) + const context = useContext(AuthContext); if (context === undefined) { - throw new Error('useAuth deve ser usado dentro de AuthProvider') + throw new Error("useAuth deve ser usado dentro de AuthProvider"); } - return context -} \ No newline at end of file + return context; +}; diff --git a/susconecta/lib/api.ts b/susconecta/lib/api.ts index c3fd891..ead307d 100644 --- a/susconecta/lib/api.ts +++ b/susconecta/lib/api.ts @@ -220,82 +220,115 @@ async function fetchWithFallback(url: string, headers: Record(res: Response): Promise { let json: any = null; + let rawText: string | null = null; + try { - json = await res.json(); + // ✅ Lê o corpo como texto e só faz parse se tiver conteúdo + rawText = await res.text(); + json = rawText ? JSON.parse(rawText) : null; } catch (err) { - console.error("Erro ao parsear a resposta como JSON:", err); + console.error("Erro ao parsear a resposta como JSON (provável corpo vazio):", err); } + // 🔍 Se a resposta não for OK, trata como erro if (!res.ok) { - // Tenta também ler o body como texto cru para obter mensagens detalhadas - let rawText = ''; - try { - rawText = await res.clone().text(); - } catch (tErr) { - // ignore + // 🔁 Clona a resposta para tentar ler novamente caso o body já tenha sido consumido + let rawCloneText = rawText; + if (!rawCloneText) { + try { + rawCloneText = await res.clone().text(); + } catch { + rawCloneText = "(falha ao ler body)"; + } } - console.error("[API ERROR]", res.url, res.status, json, "raw:", rawText); + + // 🔎 Log completo com fallback seguro + console.error("[API ERROR]", res.url, res.status, json ?? {}, "raw:", rawCloneText ?? "(vazio)"); + const code = (json && (json.error?.code || json.code)) ?? res.status; - const msg = (json && (json.error?.message || json.message || json.error)) ?? res.statusText; - - // Mensagens amigáveis para erros comuns + const msg = + (json && (json.error?.message || json.message || json.error)) ?? + res.statusText; + + // 🧠 Mensagens amigáveis e específicas let friendlyMessage = msg; - - // Erros de criação de usuário - if (res.url?.includes('create-user')) { - if (msg?.includes('Failed to assign user role')) { - friendlyMessage = 'O usuário foi criado mas houve falha ao atribuir permissões. Entre em contato com o administrador do sistema para verificar as configurações da Edge Function.'; - } else if (msg?.includes('already registered')) { - friendlyMessage = 'Este email já está cadastrado no sistema.'; - } else if (msg?.includes('Invalid role')) { - friendlyMessage = 'Tipo de acesso inválido.'; - } else if (msg?.includes('Missing required fields')) { - friendlyMessage = 'Campos obrigatórios não preenchidos.'; + + // 🔹 Erros de criação de usuário + if (res.url?.includes("create-user")) { + if (msg?.includes("Failed to assign user role")) { + friendlyMessage = + "O usuário foi criado mas houve falha ao atribuir permissões. Entre em contato com o administrador do sistema."; + } else if (msg?.includes("already registered")) { + friendlyMessage = "Este email já está cadastrado no sistema."; + } else if (msg?.includes("Invalid role")) { + friendlyMessage = "Tipo de acesso inválido."; + } else if (msg?.includes("Missing required fields")) { + friendlyMessage = "Campos obrigatórios não preenchidos."; } else if (res.status === 401) { - friendlyMessage = 'Você não está autenticado. Faça login novamente.'; + friendlyMessage = "Você não está autenticado. Faça login novamente."; } else if (res.status === 403) { - friendlyMessage = 'Você não tem permissão para criar usuários.'; + friendlyMessage = "Você não tem permissão para criar usuários."; } else if (res.status === 500) { - friendlyMessage = 'Erro no servidor ao criar usuário. Entre em contato com o suporte.'; + friendlyMessage = + "Erro no servidor ao criar usuário. Entre em contato com o suporte."; } } - // Erro de CPF duplicado - else if (code === '23505' && msg.includes('patients_cpf_key')) { - friendlyMessage = 'Já existe um paciente cadastrado com este CPF. Por favor, verifique se o paciente já está registrado no sistema ou use um CPF diferente.'; + + // 🔹 Erro de CPF duplicado + else if (code === "23505" && msg.includes("patients_cpf_key")) { + friendlyMessage = + "Já existe um paciente cadastrado com este CPF. Verifique se o paciente já está no sistema."; } - // Erro de email duplicado (paciente) - else if (code === '23505' && msg.includes('patients_email_key')) { - friendlyMessage = 'Já existe um paciente cadastrado com este email. Por favor, use um email diferente.'; + + // 🔹 Erro de email duplicado (paciente) + else if (code === "23505" && msg.includes("patients_email_key")) { + friendlyMessage = + "Já existe um paciente cadastrado com este email. Use outro email."; } - // Erro de CRM duplicado (médico) - else if (code === '23505' && msg.includes('doctors_crm')) { - friendlyMessage = 'Já existe um médico cadastrado com este CRM. Por favor, verifique se o médico já está registrado no sistema.'; + + // 🔹 Erro de CRM duplicado (médico) + else if (code === "23505" && msg.includes("doctors_crm")) { + friendlyMessage = + "Já existe um médico cadastrado com este CRM. Verifique se o médico já está no sistema."; } - // Erro de email duplicado (médico) - else if (code === '23505' && msg.includes('doctors_email_key')) { - friendlyMessage = 'Já existe um médico cadastrado com este email. Por favor, use um email diferente.'; + + // 🔹 Erro de email duplicado (médico) + else if (code === "23505" && msg.includes("doctors_email_key")) { + friendlyMessage = + "Já existe um médico cadastrado com este email. Use outro email."; } - // Outros erros de constraint unique - else if (code === '23505') { - friendlyMessage = 'Registro duplicado: já existe um cadastro com essas informações no sistema.'; + + // 🔹 Outros erros de constraint UNIQUE + else if (code === "23505") { + friendlyMessage = + "Registro duplicado: já existe um cadastro com essas informações."; } - // Erro de foreign key (registro referenciado em outra tabela) - else if (code === '23503') { - // Mensagem específica para pacientes com relatórios vinculados - if (msg && msg.toString().toLowerCase().includes('reports')) { - friendlyMessage = 'Não é possível excluir este paciente porque existem relatórios vinculados a ele. Exclua ou desvincule os relatórios antes de remover o paciente.'; + + // 🔹 Foreign key violation (registro vinculado) + else if (code === "23503") { + if (msg && msg.toString().toLowerCase().includes("reports")) { + friendlyMessage = + "Não é possível excluir este paciente porque existem relatórios vinculados."; } else { - friendlyMessage = 'Registro referenciado em outra tabela. Remova referências dependentes antes de tentar novamente.'; + friendlyMessage = + "Registro vinculado em outra tabela. Remova dependências antes de tentar novamente."; } } - + + // 🔹 Caso genérico + if (!friendlyMessage || typeof friendlyMessage !== "string") { + friendlyMessage = "Erro ao processar requisição."; + } + throw new Error(friendlyMessage); } + // ✅ Retorno padronizado (json.data ou json) return (json?.data ?? json) as T; } + // Helper de paginação (Range/Range-Unit) function rangeHeaders(page?: number, limit?: number): Record { if (!page || !limit) return {}; @@ -565,15 +598,29 @@ export async function buscarPacientesPorIds(ids: Array): Promis export async function criarPaciente(input: PacienteInput): Promise { const url = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/patients`; + + // ✅ Garante que campos obrigatórios tenham valores padrão + const safeInput = { + ...input, + phone_mobile: input.phone_mobile?.trim() || "000000000", // evita null + email: input.email?.trim() || null, // limpa espaços + full_name: input.full_name?.trim(), // remove espaços extras + }; + const res = await fetch(url, { method: "POST", - headers: withPrefer({ ...baseHeaders(), "Content-Type": "application/json" }, "return=representation"), - body: JSON.stringify(input), + headers: withPrefer( + { ...baseHeaders(), "Content-Type": "application/json" }, + "return=representation" + ), + body: JSON.stringify(safeInput), }); + const arr = await parse(res); return Array.isArray(arr) ? arr[0] : (arr as Paciente); } + export async function atualizarPaciente(id: string | number, input: PacienteInput): Promise { const url = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/patients?id=eq.${id}`; const res = await fetch(url, { @@ -950,12 +997,19 @@ export async function vincularUserIdMedico(medicoId: string | number, userId: st * Vincula um user_id (auth user id) a um registro de paciente existente. * Retorna o paciente atualizado. */ -export async function vincularUserIdPaciente(pacienteId: string | number, userId: string): Promise { +export async function vincularPacientePorEmail( + pacienteId: string | number, + email: string +): Promise { + console.log("[vincularPacientePorEmail] Associando paciente via email:", { pacienteId, email }); const url = `${REST}/patients?id=eq.${encodeURIComponent(String(pacienteId))}`; - const payload = { user_id: String(userId) }; + const payload = { email }; const res = await fetch(url, { - method: 'PATCH', - headers: withPrefer({ ...baseHeaders(), 'Content-Type': 'application/json' }, 'return=representation'), + method: "PATCH", + headers: withPrefer( + { ...baseHeaders(), "Content-Type": "application/json" }, + "return=representation" + ), body: JSON.stringify(payload), }); const arr = await parse(res); @@ -965,6 +1019,7 @@ export async function vincularUserIdPaciente(pacienteId: string | number, userId + export async function atualizarMedico(id: string | number, input: MedicoInput): Promise { console.log(`Tentando atualizar médico ID: ${id}`); console.log(`Payload original:`, input); @@ -1399,30 +1454,27 @@ export async function criarUsuarioPaciente(paciente: { full_name: string; phone_mobile: string; }): Promise { - + 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) + + 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); + const signupUrl = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/signup`; - + const payload = { - email: paciente.email, + email: paciente.email.trim(), password: senha, data: { - userType: 'paciente', // Para login em /login-paciente -> /paciente - full_name: paciente.full_name, - phone: paciente.phone_mobile, - } + userType: "paciente", + full_name: paciente.full_name.trim(), + phone: paciente.phone_mobile?.trim() || "", + }, }; - - console.log('[CRIAR PACIENTE] Enviando para:', signupUrl); - + try { const response = await fetch(signupUrl, { method: "POST", @@ -1433,125 +1485,76 @@ export async function criarUsuarioPaciente(paciente: { }, body: JSON.stringify(payload), }); - - console.log('[CRIAR PACIENTE] Status da resposta:', response.status, response.statusText); - + + console.log("[CRIAR PACIENTE] Status:", 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 + console.error("[CRIAR PACIENTE] Erro na resposta:", errorText); + 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'; + const errData = JSON.parse(errorText); + errorMsg = + errData.msg || + errData.message || + errData.error_description || + errData.error || + errorMsg; + + 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 + } catch { + // ignora erro de parse } - + 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!'); + + console.log("✅ [CRIAR PACIENTE] Usuário criado com sucesso!"); + console.log("User ID:", userId); + console.log("Resposta completa:", JSON.stringify(responseData, null, 2)); + + // 🧩 Corrige: evita invalid_credentials + // O Supabase pode demorar alguns segundos para permitir login (até confirmar email) + if (!responseData.user?.email_confirmed_at) { + console.warn( + "[CRIAR PACIENTE] Email ainda não confirmado — aguardando confirmação automática do Supabase." + ); } - - // 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('========================================'); - + + 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 (aguardando confirmação)" + ); + 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); + console.error("[CRIAR PACIENTE] Erro ao criar usuário:", error); throw error; } } + // ===== CEP (usado nos formulários) ===== export async function buscarCepAPI(cep: string): Promise<{ logradouro?: string; diff --git a/susconecta/package-lock.json b/susconecta/package-lock.json index f1244cc..af7dba3 100644 --- a/susconecta/package-lock.json +++ b/susconecta/package-lock.json @@ -41,6 +41,7 @@ "@radix-ui/react-toggle": "latest", "@radix-ui/react-toggle-group": "latest", "@radix-ui/react-tooltip": "latest", + "@supabase/supabase-js": "^2.75.0", "@vercel/analytics": "1.3.1", "autoprefixer": "^10.4.20", "class-variance-authority": "^0.7.1", @@ -2627,6 +2628,80 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@supabase/auth-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.75.0.tgz", + "integrity": "sha512-J8TkeqCOMCV4KwGKVoxmEBuDdHRwoInML2vJilthOo7awVCro2SM+tOcpljORwuBQ1vHUtV62Leit+5wlxrNtw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.75.0.tgz", + "integrity": "sha512-18yk07Moj/xtQ28zkqswxDavXC3vbOwt1hDuYM3/7xPnwwpKnsmPyZ7bQ5th4uqiJzQ135t74La9tuaxBR6e7w==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.75.0.tgz", + "integrity": "sha512-YfBz4W/z7eYCFyuvHhfjOTTzRrQIvsMG2bVwJAKEVVUqGdzqfvyidXssLBG0Fqlql1zJFgtsPpK1n4meHrI7tg==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.75.0.tgz", + "integrity": "sha512-B4Xxsf2NHd5cEnM6MGswOSPSsZKljkYXpvzKKmNxoUmNQOfB7D8HOa6NwHcUBSlxcjV+vIrYKcYXtavGJqeGrw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15", + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "ws": "^8.18.2" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.75.0.tgz", + "integrity": "sha512-wpJMYdfFDckDiHQaTpK+Ib14N/O2o0AAWWhguKvmmMurB6Unx17GGmYp5rrrqCTf8S1qq4IfIxTXxS4hzrUySg==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.75.0.tgz", + "integrity": "sha512-8UN/vATSgS2JFuJlMVr51L3eUDz+j1m7Ww63wlvHLKULzCDaVWYzvacCjBTLW/lX/vedI2LBI4Vg+01G9ufsJQ==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.75.0", + "@supabase/functions-js": "2.75.0", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "2.75.0", + "@supabase/realtime-js": "2.75.0", + "@supabase/storage-js": "2.75.0" + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -3250,7 +3325,6 @@ "version": "22.18.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.8.tgz", "integrity": "sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -3262,6 +3336,12 @@ "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", "license": "MIT" }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -3325,6 +3405,15 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.45.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz", @@ -9144,6 +9233,12 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/trim-canvas": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/trim-canvas/-/trim-canvas-0.1.2.tgz", @@ -9344,7 +9439,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { @@ -9519,6 +9613,22 @@ "d3-timer": "^3.0.1" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9634,6 +9744,27 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",