From 37a87af28d73ea68887a9ccd330dec106ebfa90e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Gustavo?= <166467972+JoaoGustavo-dev@users.noreply.github.com> Date: Thu, 16 Oct 2025 22:03:40 -0300 Subject: [PATCH] add-new-authentication-endpoints --- .../forms/patient-registration-form.tsx | 19 +- susconecta/lib/api.ts | 198 ++++++++-------- susconecta/lib/auth.ts | 212 ++++++------------ susconecta/lib/http.ts | 19 +- 4 files changed, 189 insertions(+), 259 deletions(-) diff --git a/susconecta/components/forms/patient-registration-form.tsx b/susconecta/components/forms/patient-registration-form.tsx index 9cbb0e5..84d0c4c 100644 --- a/susconecta/components/forms/patient-registration-form.tsx +++ b/susconecta/components/forms/patient-registration-form.tsx @@ -301,7 +301,24 @@ export function PatientRegistrationForm({ 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') { + + // Guard: verify userId is present and looks plausible before attempting to PATCH + const isPlausibleUserId = (id: any) => { + if (!id) return false; + const s = String(id).trim(); + if (!s) return false; + // quick UUID v4-ish check (xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx) or numeric id fallback + const uuidV4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + const numeric = /^\d+$/; + return uuidV4.test(s) || numeric.test(s) || s.length >= 8; + }; + + if (!pacienteId) { + console.warn('[PatientForm] pacienteId ausente; pulando vinculação de user_id'); + } else if (!isPlausibleUserId(userId)) { + // Do not attempt to PATCH when userId is missing/invalid to avoid 400s + console.warn('[PatientForm] userId inválido ou ausente; não será feita a vinculação. userResponse:', userResponse); + } else if (typeof apiMod.vincularUserIdPaciente === 'function') { console.log('[PatientForm] Vinculando user_id ao paciente:', pacienteId, userId); try { await apiMod.vincularUserIdPaciente(pacienteId, String(userId)); diff --git a/susconecta/lib/api.ts b/susconecta/lib/api.ts index ff40e79..5dea530 100644 --- a/susconecta/lib/api.ts +++ b/susconecta/lib/api.ts @@ -1081,17 +1081,17 @@ export async function excluirPaciente(id: string | number): Promise { * Este endpoint usa a service role key e valida se o requisitante é administrador. */ export async function assignRoleServerSide(userId: string, role: string): Promise { - 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(res); + // Atribuição de roles é uma operação privilegiada que requer a + // service_role key do Supabase (ou equivalente) e validação de permissões + // server-side. Não execute isso do cliente. + // + // Antes este helper chamava `/api/assign-role` (um proxy server-side). + // Agora que o projeto deve usar apenas o endpoint público seguro de + // criação de usuários (OpenAPI `/create-user`), a atribuição deve ocorrer + // dentro desse endpoint no backend. Portanto este helper foi descontinuado + // no cliente para evitar qualquer tentativa de realizar operação + // privilegiada no navegador. + throw new Error('assignRoleServerSide is not available in the client. Use the backend /create-user endpoint which performs role assignment server-side.'); } // ===== PACIENTES (Extra: verificação de CPF duplicado) ===== export async function verificarCpfDuplicado(cpf: string): Promise { @@ -1618,26 +1618,34 @@ export function gerarSenhaAleatoria(): string { } export async function criarUsuario(input: CreateUserInput): Promise { - // When running in the browser, call our Next.js proxy to avoid CORS/preflight - // issues that some Edge Functions may have. On server-side, call the function - // directly. - if (typeof window !== 'undefined') { - const proxyUrl = '/api/create-user' - const res = await fetch(proxyUrl, { + // Call the Edge Function directly (no proxy). The backend function is + // responsible for role assignment and any service-role operations. + // The OpenAPI for the new endpoint exposes POST /create-user at the + // API root (API_BASE). Call that endpoint directly from the client. + const url = `${API_BASE}/create-user`; + + // Network/fetch errors (including CORS preflight failures) throw before we get a Response. + // Catch them and provide a clearer, actionable error message for developers/operators. + let res: Response; + try { + res = await fetch(url, { method: 'POST', headers: { ...baseHeaders(), 'Content-Type': 'application/json' }, body: JSON.stringify(input), - }) - return await parse(res as Response) + }); + } catch (err: any) { + console.error('[criarUsuario] fetch error for', url, err); + // Do not attempt client-side signup fallback. Role assignment and user creation + // must be performed by the backend endpoint `/create-user` which has the + // necessary privileges. Surface a clear error so operators can fix the + // backend (CORS / route availability) instead of silently creating an + // auth user without roles. + throw new Error( + 'Falha ao contatar o endpoint /create-user. Não será feito fallback via /auth/v1/signup. Verifique se o endpoint /create-user existe, está acessível e se o CORS/OPTIONS está configurado corretamente. Detalhes: ' + (err?.message ?? String(err)) + ); } - 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(res); + return await parse(res as Response); } // ===== ALTERNATIVA: Criar usuário diretamente via Supabase Auth ===== @@ -1689,14 +1697,22 @@ export async function criarUsuarioDirectAuth(input: { } const responseData = await response.json(); - const userId = responseData.user?.id || responseData.id; - - console.log('[DIRECT AUTH] Usuário criado:', userId); - + // Try several common locations for the returned user id depending on Supabase configuration + const userId = responseData?.user?.id || responseData?.id || responseData?.data?.user?.id || responseData?.data?.id; + + // If no user id was returned, treat this as a failure. Some Supabase setups (e.g. magic link / invite) + // may not return the user id immediately. In that case we cannot safely link the profile to a user. + if (!userId) { + console.warn('[DIRECT AUTH] signup response did not include a user id; response:', responseData); + throw new Error('Signup did not return a user id (provider may be configured for magic links or pending confirmation). Fallback cannot determine created user 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: { @@ -1723,93 +1739,57 @@ export async function criarUsuarioDirectAuth(input: { // 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 { - // Prefer server-side creation (new OpenAPI create-user) so roles are assigned - // correctly (and magic link is sent). Fallback to direct Supabase signup if - // the server function is unavailable. - try { - const res = await criarUsuario({ email: medico.email, password: '', full_name: medico.full_name, phone: medico.phone_mobile, role: 'medico' as any }); - return res; - } catch (err) { - console.warn('[CRIAR MÉDICO] Falha no endpoint server-side create-user, tentando fallback direto no Supabase Auth:', err); - // Fallback: create directly in Supabase Auth (old behavior) - } - - // --- Fallback to previous direct signup --- - const senha = gerarSenhaAleatoria(); - const signupUrl = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/signup`; - const payload = { - email: medico.email, - password: senha, - data: { - userType: 'profissional', - full_name: medico.full_name, - phone: medico.phone_mobile, - } - }; - - const response = await fetch(signupUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - "apikey": ENV_CONFIG.SUPABASE_ANON_KEY, - }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - const errorText = await response.text(); - let errorMsg = `Erro ao criar usuário (${response.status})`; - try { const errorData = JSON.parse(errorText); errorMsg = errorData.msg || errorData.message || errorData.error_description || errorMsg; } catch {} - throw new Error(errorMsg); - } - - const responseData = await response.json(); - return { success: true, user: responseData.user || responseData, email: medico.email, password: senha }; + // Rely on the server-side create-user endpoint (POST /create-user). The + // backend is responsible for role assignment and sending the magic link. + // Any error should be surfaced to the caller so it can be handled there. + return await criarUsuario({ email: medico.email, password: '', full_name: medico.full_name, phone: medico.phone_mobile, role: 'medico' as any }); } // 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 { - // Prefer server-side creation (OpenAPI create-user) to assign role 'paciente'. + // Rely on the server-side create-user endpoint (POST /create-user). + return await criarUsuario({ email: paciente.email, password: '', full_name: paciente.full_name, phone: paciente.phone_mobile, role: 'paciente' as any }); +} + +/** + * Envia um magic link (OTP) diretamente via Supabase Auth (cliente) + * Sem componente server-side adicional. Use quando quiser autenticar + * o usuário por email (senha não necessária). + * + * Observação: isto apenas envia o link de login. A atribuição de roles + * continua sendo operação server-side e deve ser feita pelo backend. + */ +export async function sendMagicLink(email: string, options?: { emailRedirectTo?: string }): Promise<{ success: boolean; message?: string }> { + if (!email) throw new Error('Email obrigatório para enviar magic link'); + const url = `${API_BASE}/auth/v1/otp`; + const payload: any = { email }; + if (options && options.emailRedirectTo) payload.options = { emailRedirectTo: options.emailRedirectTo }; + try { - const res = await criarUsuario({ email: paciente.email, password: '', full_name: paciente.full_name, phone: paciente.phone_mobile, role: 'paciente' as any }); - return res; - } catch (err) { - console.warn('[CRIAR PACIENTE] Falha no endpoint server-side create-user, tentando fallback direto no Supabase Auth:', err); - } + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + apikey: ENV_CONFIG.SUPABASE_ANON_KEY, + }, + body: JSON.stringify(payload), + }); - // Fallback to previous direct signup behavior - const senha = gerarSenhaAleatoria(); - const signupUrl = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/signup`; - const payload = { - email: paciente.email, - password: senha, - data: { - userType: 'paciente', - full_name: paciente.full_name, - phone: paciente.phone_mobile, + const text = await res.text(); + let json: any = null; + try { json = text ? JSON.parse(text) : null; } catch { json = null; } + + if (!res.ok) { + const msg = (json && (json.error || json.msg || json.message)) ?? text ?? res.statusText; + throw new Error(String(msg)); } - }; - const response = await fetch(signupUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - "apikey": ENV_CONFIG.SUPABASE_ANON_KEY, - }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - const errorText = await response.text(); - let errorMsg = `Erro ao criar usuário (${response.status})`; - try { const errorData = JSON.parse(errorText); errorMsg = errorData.msg || errorData.message || errorData.error_description || errorMsg; } catch {} - throw new Error(errorMsg); + return { success: true, message: (json && (json.message || json.msg)) ?? 'Magic link enviado. Verifique seu email.' }; + } catch (err: any) { + console.error('[sendMagicLink] erro ao enviar magic link', err); + throw new Error(err?.message ?? 'Falha ao enviar magic link'); } - - const responseData = await response.json(); - return { success: true, user: responseData.user || responseData, email: paciente.email, password: senha }; } // ===== CEP (usado nos formulários) ===== diff --git a/susconecta/lib/auth.ts b/susconecta/lib/auth.ts index 88c5186..a492312 100644 --- a/susconecta/lib/auth.ts +++ b/susconecta/lib/auth.ts @@ -1,7 +1,6 @@ import type { LoginRequest, LoginResponse, - RefreshTokenResponse, AuthError, UserData } from '@/types/auth'; @@ -89,144 +88,48 @@ export async function loginUser( password: string, userType: 'profissional' | 'paciente' | 'administrador' ): Promise { - // Use server-side AUTH_ENDPOINTS.LOGIN by default. When running in the browser - // prefer the local proxy that forwards to the OpenAPI signin: `/api/signin-user`. - const isBrowser = typeof window !== 'undefined'; - const url = isBrowser ? '/api/signin-user' : AUTH_ENDPOINTS.LOGIN; - - const payload = { - email, - password, - }; - - console.log('[AUTH-API] Iniciando login...', { - email, - userType, - url, - payload, - timestamp: new Date().toLocaleTimeString() - }); - - // Log only non-sensitive info; never log passwords - console.log('🔑 [AUTH-API] Credenciais sendo usadas no login (redacted):'); - console.log('📧 Email:', email); - 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); - - // Helper to perform a login fetch and return response (no processing here) - async function doLoginFetch(targetUrl: string) { - try { - return await fetch(targetUrl, { - method: 'POST', - headers: getLoginHeaders(), - body: JSON.stringify(payload), - }); - } catch (err) { - // bubble up the error to the caller - throw err; - } - } + // Use the canonical Supabase token endpoint for password grant as configured in ENV_CONFIG. + const url = AUTH_ENDPOINTS.LOGIN; - let response: Response; - try { - response = await doLoginFetch(url); - } catch (networkError) { - console.warn('[AUTH-API] Network error when calling', url, networkError); - // Try fallback to server endpoints if available - const fallback1 = AUTH_ENDPOINTS.LOGIN; - const fallback2 = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/signin`; - let tried = [] as string[]; + const payload = { email, password }; - try { - tried.push(fallback1); - response = await doLoginFetch(fallback1); - } catch (e1) { - console.warn('[AUTH-API] Fallback1 failed', fallback1, e1); - try { - tried.push(fallback2); - response = await doLoginFetch(fallback2); - } catch (e2) { - console.error('[AUTH-API] All fallbacks failed', { tried, e1, e2 }); - throw new AuthenticationError( - 'Não foi possível contatar o serviço de autenticação (todos os caminhos falharam)', - 'AUTH_NETWORK_ERROR', - { tried } - ); - } - } - } + console.log('[AUTH-API] Iniciando login (using AUTH_ENDPOINTS.LOGIN)...', { + email, + userType, + url, + timestamp: new Date().toLocaleTimeString() + }); - console.log(`[AUTH-API] Login response: ${response.status} ${response.statusText}`, { - url: response.url, - status: response.status, - timestamp: new Date().toLocaleTimeString() + // Do not log passwords. Log only non-sensitive info. + debugRequest('POST', url, getLoginHeaders(), { email }); + + // Perform single, explicit request to the configured token endpoint. + let response: Response; + try { + response = await fetch(url, { + method: 'POST', + headers: getLoginHeaders(), + body: JSON.stringify(payload), }); + } catch (networkError) { + console.error('[AUTH-API] Network error when calling', url, networkError); + throw new AuthenticationError('Não foi possível contatar o serviço de autenticação', 'AUTH_NETWORK_ERROR', networkError); + } - // If proxy returned 404, try direct fallbacks (in case the proxy route is missing) - if (response.status === 404) { - console.warn('[AUTH-API] Proxy returned 404, attempting direct login fallbacks'); - const fallback1 = AUTH_ENDPOINTS.LOGIN; - const fallback2 = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/signin`; - let fallbackResponse: Response | null = null; - try { - fallbackResponse = await doLoginFetch(fallback1); - } catch (e) { - console.warn('[AUTH-API] fallback1 failed', fallback1, e); - } + console.log(`[AUTH-API] Login response: ${response.status} ${response.statusText}`, { + url: response.url, + status: response.status, + timestamp: new Date().toLocaleTimeString() + }); - if (!fallbackResponse || fallbackResponse.status === 404) { - try { - fallbackResponse = await doLoginFetch(fallback2); - } catch (e) { - console.warn('[AUTH-API] fallback2 failed', fallback2, e); - } - } + // If endpoint is missing, make the error explicit + if (response.status === 404) { + console.error('[AUTH-API] Final response was 404 (Not Found) for', url); + throw new AuthenticationError('Signin endpoint not found (404) at configured AUTH_ENDPOINTS.LOGIN', 'SIGNIN_NOT_FOUND', { url }); + } - if (fallbackResponse) { - response = fallbackResponse; - console.log('[AUTH-API] Used fallback response', { url: response.url, status: response.status }); - } else { - console.error('[AUTH-API] No fallback produced a valid response'); - } - } - - // 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)); - - // If after trying fallbacks we still have a 404, make the error explicit and actionable - if (response.status === 404) { - console.error('[AUTH-API] Final response was 404 (Not Found). Likely the local proxy route is missing or Next dev server is not running.', { url: response.url }); - throw new AuthenticationError( - 'Signin endpoint not found (404). Ensure Next.js dev server is running and the route `/api/signin-user` exists.', - 'SIGNIN_NOT_FOUND', - { url: response.url } - ); - } - - const data = await processResponse(response); + const data = await processResponse(response); console.log('[AUTH] Dados recebidos da API:', data); @@ -334,10 +237,10 @@ export async function logoutUser(token: string): Promise { /** * Serviço para renovar token JWT */ -export async function refreshAuthToken(refreshToken: string): Promise { +export async function refreshAuthToken(refreshToken: string): Promise { const url = AUTH_ENDPOINTS.REFRESH; - console.log('[AUTH] Renovando token'); + console.log('[AUTH] Renovando token via REFRESH endpoint'); try { const response = await fetch(url, { @@ -350,22 +253,43 @@ export async function refreshAuthToken(refreshToken: string): Promise(response); - - console.log('[AUTH] Token renovado com sucesso'); - return data; + const data = await processResponse(response); + + console.log('[AUTH] Dados recebidos no refresh:', data); + + if (!data || !data.access_token) { + console.error('[AUTH] Refresh não retornou access_token:', data); + throw new AuthenticationError('Refresh não retornou access_token', 'NO_TOKEN_RECEIVED', data); + } + + // Adaptar para o mesmo formato usado no login + const adapted: LoginResponse = { + access_token: data.access_token || data.token, + refresh_token: data.refresh_token || null, + token_type: data.token_type || 'Bearer', + expires_in: data.expires_in || 3600, + user: { + id: data.user?.id || data.id || '', + email: data.user?.email || data.email || '', + name: data.user?.name || data.name || '', + userType: (data.user?.userType as any) || 'paciente', + profile: data.user?.profile || data.profile || {} + } + }; + + console.log('[AUTH] Token renovado com sucesso (adapted)', { + tokenSnippet: adapted.access_token?.substring(0, 20) + '...' + }); + + return adapted; } 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 - ); + + throw new AuthenticationError('Não foi possível renovar a sessão', 'REFRESH_ERROR', error); } } diff --git a/susconecta/lib/http.ts b/susconecta/lib/http.ts index 6172af2..cd55f8f 100644 --- a/susconecta/lib/http.ts +++ b/susconecta/lib/http.ts @@ -73,17 +73,26 @@ class HttpClient { } const data = await response.json() - + + // Data pode ser um LoginResponse completo ou apenas { access_token } + const newAccessToken = data.access_token || data.token || null + const newRefreshToken = data.refresh_token || null + + if (!newAccessToken) { + console.error('[HTTP] Refresh não retornou access_token', data) + throw new Error('Refresh did not return access_token') + } + // Atualizar tokens de forma atômica - localStorage.setItem(AUTH_STORAGE_KEYS.TOKEN, data.access_token) - if (data.refresh_token) { - localStorage.setItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN, data.refresh_token) + localStorage.setItem(AUTH_STORAGE_KEYS.TOKEN, newAccessToken) + if (newRefreshToken) { + localStorage.setItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN, newRefreshToken) } console.log('[HTTP] Token renovado com sucesso!', { timestamp: new Date().toLocaleTimeString() }) - return data.access_token + return newAccessToken } /**