backup/remove-server-side #51

Merged
JoaoGustavo-dev merged 9 commits from backup/remove-server-side into develop 2025-10-18 17:07:50 +00:00
4 changed files with 189 additions and 259 deletions
Showing only changes of commit 37a87af28d - Show all commits

View File

@ -301,7 +301,24 @@ export function PatientRegistrationForm({
const apiMod = await import('@/lib/api'); const apiMod = await import('@/lib/api');
const pacienteId = savedPatientProfile?.id || (savedPatientProfile && (savedPatientProfile as any).id); 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; 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); console.log('[PatientForm] Vinculando user_id ao paciente:', pacienteId, userId);
try { try {
await apiMod.vincularUserIdPaciente(pacienteId, String(userId)); await apiMod.vincularUserIdPaciente(pacienteId, String(userId));

View File

@ -1081,17 +1081,17 @@ export async function excluirPaciente(id: string | number): Promise<void> {
* Este endpoint usa a service role key e valida se o requisitante é administrador. * Este endpoint usa a service role key e valida se o requisitante é administrador.
*/ */
export async function assignRoleServerSide(userId: string, role: string): Promise<any> { export async function assignRoleServerSide(userId: string, role: string): Promise<any> {
const url = `/api/assign-role`; // Atribuição de roles é uma operação privilegiada que requer a
const token = getAuthToken(); // service_role key do Supabase (ou equivalente) e validação de permissões
const res = await fetch(url, { // server-side. Não execute isso do cliente.
method: 'POST', //
headers: { // Antes este helper chamava `/api/assign-role` (um proxy server-side).
'Content-Type': 'application/json', // Agora que o projeto deve usar apenas o endpoint público seguro de
...(token ? { Authorization: `Bearer ${token}` } : {}), // criação de usuários (OpenAPI `/create-user`), a atribuição deve ocorrer
}, // dentro desse endpoint no backend. Portanto este helper foi descontinuado
body: JSON.stringify({ user_id: userId, role }), // no cliente para evitar qualquer tentativa de realizar operação
}); // privilegiada no navegador.
return await parse<any>(res); 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) ===== // ===== PACIENTES (Extra: verificação de CPF duplicado) =====
export async function verificarCpfDuplicado(cpf: string): Promise<boolean> { export async function verificarCpfDuplicado(cpf: string): Promise<boolean> {
@ -1618,26 +1618,34 @@ export function gerarSenhaAleatoria(): string {
} }
export async function criarUsuario(input: CreateUserInput): Promise<CreateUserResponse> { export async function criarUsuario(input: CreateUserInput): Promise<CreateUserResponse> {
// When running in the browser, call our Next.js proxy to avoid CORS/preflight // Call the Edge Function directly (no proxy). The backend function is
// issues that some Edge Functions may have. On server-side, call the function // responsible for role assignment and any service-role operations.
// directly. // The OpenAPI for the new endpoint exposes POST /create-user at the
if (typeof window !== 'undefined') { // API root (API_BASE). Call that endpoint directly from the client.
const proxyUrl = '/api/create-user' const url = `${API_BASE}/create-user`;
const res = await fetch(proxyUrl, {
// 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', method: 'POST',
headers: { ...baseHeaders(), 'Content-Type': 'application/json' }, headers: { ...baseHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify(input), body: JSON.stringify(input),
}) });
return await parse<CreateUserResponse>(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`; return await parse<CreateUserResponse>(res as Response);
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 ===== // ===== ALTERNATIVA: Criar usuário diretamente via Supabase Auth =====
@ -1689,14 +1697,22 @@ export async function criarUsuarioDirectAuth(input: {
} }
const responseData = await response.json(); const responseData = await response.json();
const userId = responseData.user?.id || responseData.id; // 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;
console.log('[DIRECT AUTH] Usuário criado:', userId);
// 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) // 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. // 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. // The backend should use the service role key to insert into user_roles table.
return { return {
success: true, success: true,
user: { user: {
@ -1723,93 +1739,57 @@ export async function criarUsuarioDirectAuth(input: {
// Criar usuário para MÉDICO no Supabase Auth (sistema de autenticação) // 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<any> { export async function criarUsuarioMedico(medico: { email: string; full_name: string; phone_mobile: string; }): Promise<any> {
// Prefer server-side creation (new OpenAPI create-user) so roles are assigned // Rely on the server-side create-user endpoint (POST /create-user). The
// correctly (and magic link is sent). Fallback to direct Supabase signup if // backend is responsible for role assignment and sending the magic link.
// the server function is unavailable. // Any error should be surfaced to the caller so it can be handled there.
try { return await criarUsuario({ email: medico.email, password: '', full_name: medico.full_name, phone: medico.phone_mobile, role: 'medico' as any });
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 };
} }
// Criar usuário para PACIENTE no Supabase Auth (sistema de autenticação) // 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<any> { export async function criarUsuarioPaciente(paciente: { email: string; full_name: string; phone_mobile: string; }): Promise<any> {
// 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 { try {
const res = await criarUsuario({ email: paciente.email, password: '', full_name: paciente.full_name, phone: paciente.phone_mobile, role: 'paciente' as any }); const res = await fetch(url, {
return res; method: 'POST',
} catch (err) { headers: {
console.warn('[CRIAR PACIENTE] Falha no endpoint server-side create-user, tentando fallback direto no Supabase Auth:', err); 'Content-Type': 'application/json',
} Accept: 'application/json',
apikey: ENV_CONFIG.SUPABASE_ANON_KEY,
},
body: JSON.stringify(payload),
});
// Fallback to previous direct signup behavior const text = await res.text();
const senha = gerarSenhaAleatoria(); let json: any = null;
const signupUrl = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/signup`; try { json = text ? JSON.parse(text) : null; } catch { json = null; }
const payload = {
email: paciente.email, if (!res.ok) {
password: senha, const msg = (json && (json.error || json.msg || json.message)) ?? text ?? res.statusText;
data: { throw new Error(String(msg));
userType: 'paciente',
full_name: paciente.full_name,
phone: paciente.phone_mobile,
} }
};
const response = await fetch(signupUrl, { return { success: true, message: (json && (json.message || json.msg)) ?? 'Magic link enviado. Verifique seu email.' };
method: "POST", } catch (err: any) {
headers: { console.error('[sendMagicLink] erro ao enviar magic link', err);
"Content-Type": "application/json", throw new Error(err?.message ?? 'Falha ao enviar magic link');
"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: paciente.email, password: senha };
} }
// ===== CEP (usado nos formulários) ===== // ===== CEP (usado nos formulários) =====

View File

@ -1,7 +1,6 @@
import type { import type {
LoginRequest, LoginRequest,
LoginResponse, LoginResponse,
RefreshTokenResponse,
AuthError, AuthError,
UserData UserData
} from '@/types/auth'; } from '@/types/auth';
@ -89,144 +88,48 @@ export async function loginUser(
password: string, password: string,
userType: 'profissional' | 'paciente' | 'administrador' userType: 'profissional' | 'paciente' | 'administrador'
): Promise<LoginResponse> { ): Promise<LoginResponse> {
// 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 { try {
console.log('[AUTH-API] Enviando requisição de login...'); // Use the canonical Supabase token endpoint for password grant as configured in ENV_CONFIG.
const url = AUTH_ENDPOINTS.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;
}
}
let response: Response; const payload = { email, password };
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[];
try { console.log('[AUTH-API] Iniciando login (using AUTH_ENDPOINTS.LOGIN)...', {
tried.push(fallback1); email,
response = await doLoginFetch(fallback1); userType,
} catch (e1) { url,
console.warn('[AUTH-API] Fallback1 failed', fallback1, e1); timestamp: new Date().toLocaleTimeString()
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] Login response: ${response.status} ${response.statusText}`, { // Do not log passwords. Log only non-sensitive info.
url: response.url, debugRequest('POST', url, getLoginHeaders(), { email });
status: response.status,
timestamp: new Date().toLocaleTimeString() // 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) console.log(`[AUTH-API] Login response: ${response.status} ${response.statusText}`, {
if (response.status === 404) { url: response.url,
console.warn('[AUTH-API] Proxy returned 404, attempting direct login fallbacks'); status: response.status,
const fallback1 = AUTH_ENDPOINTS.LOGIN; timestamp: new Date().toLocaleTimeString()
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);
}
if (!fallbackResponse || fallbackResponse.status === 404) { // If endpoint is missing, make the error explicit
try { if (response.status === 404) {
fallbackResponse = await doLoginFetch(fallback2); console.error('[AUTH-API] Final response was 404 (Not Found) for', url);
} catch (e) { throw new AuthenticationError('Signin endpoint not found (404) at configured AUTH_ENDPOINTS.LOGIN', 'SIGNIN_NOT_FOUND', { url });
console.warn('[AUTH-API] fallback2 failed', fallback2, e); }
}
}
if (fallbackResponse) { const data = await processResponse<any>(response);
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<any>(response);
console.log('[AUTH] Dados recebidos da API:', data); console.log('[AUTH] Dados recebidos da API:', data);
@ -334,10 +237,10 @@ export async function logoutUser(token: string): Promise<void> {
/** /**
* Serviço para renovar token JWT * Serviço para renovar token JWT
*/ */
export async function refreshAuthToken(refreshToken: string): Promise<RefreshTokenResponse> { export async function refreshAuthToken(refreshToken: string): Promise<LoginResponse> {
const url = AUTH_ENDPOINTS.REFRESH; const url = AUTH_ENDPOINTS.REFRESH;
console.log('[AUTH] Renovando token'); console.log('[AUTH] Renovando token via REFRESH endpoint');
try { try {
const response = await fetch(url, { const response = await fetch(url, {
@ -350,22 +253,43 @@ export async function refreshAuthToken(refreshToken: string): Promise<RefreshTok
body: JSON.stringify({ refresh_token: refreshToken }), body: JSON.stringify({ refresh_token: refreshToken }),
}); });
const data = await processResponse<RefreshTokenResponse>(response); const data = await processResponse<any>(response);
console.log('[AUTH] Token renovado com sucesso'); console.log('[AUTH] Dados recebidos no refresh:', data);
return 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) { } catch (error) {
console.error('[AUTH] Erro ao renovar token:', error); console.error('[AUTH] Erro ao renovar token:', error);
if (error instanceof AuthenticationError) { if (error instanceof AuthenticationError) {
throw error; throw error;
} }
throw new AuthenticationError( throw new AuthenticationError('Não foi possível renovar a sessão', 'REFRESH_ERROR', error);
'Não foi possível renovar a sessão',
'REFRESH_ERROR',
error
);
} }
} }

View File

@ -73,17 +73,26 @@ class HttpClient {
} }
const data = await response.json() 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 // Atualizar tokens de forma atômica
localStorage.setItem(AUTH_STORAGE_KEYS.TOKEN, data.access_token) localStorage.setItem(AUTH_STORAGE_KEYS.TOKEN, newAccessToken)
if (data.refresh_token) { if (newRefreshToken) {
localStorage.setItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN, data.refresh_token) localStorage.setItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN, newRefreshToken)
} }
console.log('[HTTP] Token renovado com sucesso!', { console.log('[HTTP] Token renovado com sucesso!', {
timestamp: new Date().toLocaleTimeString() timestamp: new Date().toLocaleTimeString()
}) })
return data.access_token return newAccessToken
} }
/** /**