diff --git a/MEDICONNECT 2/API-CONFIG.md b/MEDICONNECT 2/API-CONFIG.md new file mode 100644 index 000000000..50ca5d56c --- /dev/null +++ b/MEDICONNECT 2/API-CONFIG.md @@ -0,0 +1,315 @@ +# Configuração das APIs - MediConnect + +## ✅ APIs Testadas e Funcionando + +### 1. Autenticação (Auth API) + +**Base URL:** `https://yuanqfswhberkoevtmfr.supabase.co/auth/v1` + +#### Endpoints Funcionais: + +- **Login** ✅ + - `POST /token?grant_type=password` + - Body: `{ email, password }` + - Retorna: `{ access_token, refresh_token, user }` + +- **Recuperação de Senha** ✅ + - `POST /recover` + - Body: `{ email, options: { redirectTo: url } }` + - Envia email com link de recuperação + +- **Atualizar Senha** ✅ + - `PUT /user` + - Headers: `Authorization: Bearer ` + - Body: `{ password: "nova_senha" }` + - **IMPORTANTE:** Nova senha deve ser diferente da anterior (erro 422 se for igual) + +### 2. REST API + +**Base URL:** `https://yuanqfswhberkoevtmfr.supabase.co/rest/v1` + +#### Tabelas e Campos Corretos: + +##### **appointments** ✅ +```typescript +{ + id: string (UUID) + order_number: string (auto-gerado: APT-YYYY-NNNN) + patient_id: string (UUID) + doctor_id: string (UUID) + scheduled_at: string (ISO 8601 DateTime) + duration_minutes: number + appointment_type: "presencial" | "telemedicina" + status: "requested" | "confirmed" | "checked_in" | "in_progress" | "completed" | "cancelled" | "no_show" + chief_complaint: string | null + patient_notes: string | null + notes: string | null + insurance_provider: string | null + checked_in_at: string | null + completed_at: string | null + cancelled_at: string | null + cancellation_reason: string | null + created_at: string + updated_at: string + created_by: string (UUID) + updated_by: string | null +} +``` + +**Criar Consulta:** +```bash +POST /rest/v1/appointments +Headers: + - apikey: + - Authorization: Bearer + - Content-Type: application/json + - Prefer: return=representation + +Body: +{ + "patient_id": "uuid", + "doctor_id": "uuid", + "scheduled_at": "2025-11-03T10:00:00.000Z", + "duration_minutes": 30, + "appointment_type": "presencial", + "chief_complaint": "Motivo da consulta" +} +``` + +##### **doctor_availability** ✅ +```typescript +{ + id: string (UUID) + doctor_id: string (UUID) + weekday: "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" + start_time: string (HH:MM:SS, ex: "07:00:00") + end_time: string (HH:MM:SS, ex: "19:00:00") + slot_duration_minutes: number (ex: 30) + appointment_type: "presencial" | "telemedicina" + is_active: boolean + created_at: string + updated_at: string + created_by: string (UUID) + updated_by: string | null +} +``` + +**Criar Disponibilidade:** +```bash +POST /rest/v1/doctor_availability +Headers: + - apikey: + - Authorization: Bearer + - Content-Type: application/json + - Prefer: return=representation + +Body: +{ + "doctor_id": "uuid", + "weekday": "monday", // ⚠️ Texto, não número! + "start_time": "07:00:00", + "end_time": "19:00:00", + "slot_duration_minutes": 30, + "appointment_type": "presencial", + "is_active": true, + "created_by": "admin_user_id" +} +``` + +##### **patients** ✅ +```typescript +{ + id: string (UUID) + user_id: string (UUID) // ⚠️ Deve estar vinculado ao auth.users + full_name: string + email: string + cpf: string + phone_mobile: string + // ... outros campos +} +``` + +**Atualizar Patient:** +```bash +PATCH /rest/v1/patients?id=eq. +Headers: + - apikey: + - Authorization: Bearer + - Content-Type: application/json + +Body: +{ + "user_id": "auth_user_id" +} +``` + +##### **doctors** ✅ +```typescript +{ + id: string (UUID) + user_id: string (UUID) + full_name: string + email: string + crm: string + crm_uf: string + specialty: string + // ... outros campos +} +``` + +### 3. Edge Functions + +**Base URL:** `https://yuanqfswhberkoevtmfr.supabase.co/functions/v1` + +#### Funcionais: + +- **create-user-with-password** ✅ + - `POST /functions/v1/create-user-with-password` + - Cria usuário com senha e perfil completo + - Body: + ```json + { + "email": "email@example.com", + "password": "senha123", + "full_name": "Nome Completo", + "phone_mobile": "(11) 99999-9999", + "cpf": "12345678900", + "create_patient_record": true, + "role": "paciente" + } + ``` + +#### Com Problemas: + +- **request-password-reset** ❌ + - CORS blocking - não usar + - Usar diretamente `/auth/v1/recover` em vez disso + +## 🔑 Chaves de API + +```typescript +SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co" +SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ" +``` + +## 👥 Usuários de Teste + +### Admin +- Email: `riseup@popcode.com.br` +- Senha: `riseup` + +### Dr. Fernando Pirichowski +- Email: `fernando.pirichowski@souunit.com.br` +- Senha: `fernando123` +- User ID: `38aca60d-7418-4c35-95b6-cb206bb18a0a` +- Doctor ID: `6dad001d-229b-40b5-80f3-310243c4599c` +- CRM: `24245` +- Disponibilidade: Segunda a Domingo, 07:00-19:00 + +### Aurora Sabrina Clara Nascimento (Paciente) +- Email: `aurora-nascimento94@gmx.com` +- Senha: `auroranasc94` +- User ID: `6dc15cc5-7dae-4b30-924a-a4b4fa142f24` +- Patient ID: `b85486f7-9135-4b67-9aa7-b884d9603d12` +- CPF: `66864784231` +- Telefone: `(21) 99856-3014` + +## ⚠️ Pontos de Atenção + +### 1. Weekday no doctor_availability +- ❌ **NÃO** usar números (0-6) +- ✅ **USAR** strings em inglês: `"sunday"`, `"monday"`, `"tuesday"`, `"wednesday"`, `"thursday"`, `"friday"`, `"saturday"` + +### 2. scheduled_at em appointments +- ❌ **NÃO** usar campos separados `appointment_date` e `appointment_time` +- ✅ **USAR** campo único `scheduled_at` com ISO 8601 DateTime +- Exemplo: `"2025-11-03T10:00:00.000Z"` + +### 3. user_id nas tabelas patients e doctors +- ⚠️ Sempre vincular ao `auth.users.id` +- Sem esse vínculo, queries por `user_id` não funcionam + +### 4. Senha na recuperação +- ⚠️ Nova senha DEVE ser diferente da anterior +- Erro 422 com `error_code: "same_password"` se tentar usar a mesma + +### 5. redirectTo no password recovery +- ⚠️ Supabase pode ignorar o parâmetro `redirectTo` +- ✅ Implementar detecção de token no lado do cliente +- Verificar tanto query string `?token=` quanto hash `#access_token=` + +## 📦 Estrutura de Serviços no Frontend + +```typescript +// Tudo configurado em: +src/services/api/config.ts // URLs e chaves +src/services/api/client.ts // Cliente axios +src/services/appointments/ // Serviço de consultas +src/services/availability/ // Disponibilidade médicos +src/services/auth/ // Autenticação +src/services/doctors/ // Médicos +src/services/patients/ // Pacientes +src/services/index.ts // Exportações centralizadas +``` + +## ✅ Status Atual + +- [x] Autenticação funcionando +- [x] Recuperação de senha funcionando +- [x] Criação de usuários funcionando +- [x] Criação de pacientes funcionando +- [x] Criação de disponibilidade médica funcionando +- [x] Criação de consultas funcionando +- [x] Vinculação user_id ↔ patient_id corrigida +- [x] Todos os serviços usando campos corretos + +## 🚀 Próximos Passos + +1. Testar agendamento completo no frontend +2. Verificar listagem de consultas +3. Testar cancelamento e atualização de consultas +4. Verificar notificações SMS +5. Testar fluxo completo de check-in e prontuário + +## 📝 Exemplos de Uso + +### Criar Consulta +```typescript +import { appointmentService } from '@/services'; + +const appointment = await appointmentService.create({ + patient_id: 'patient-uuid', + doctor_id: 'doctor-uuid', + scheduled_at: '2025-11-03T10:00:00.000Z', + duration_minutes: 30, + appointment_type: 'presencial', + chief_complaint: 'Consulta de rotina' +}); +``` + +### Criar Disponibilidade +```typescript +import { availabilityService } from '@/services'; + +const availability = await availabilityService.create({ + doctor_id: 'doctor-uuid', + weekday: 'monday', + start_time: '07:00:00', + end_time: '19:00:00', + slot_duration_minutes: 30, + appointment_type: 'presencial' +}); +``` + +### Login +```typescript +import { authService } from '@/services'; + +const response = await authService.login({ + email: 'user@example.com', + password: 'senha123' +}); + +// response.access_token - JWT token +// response.user - dados do usuário +``` diff --git a/MEDICONNECT 2/README.md b/MEDICONNECT 2/README.md index de2ce5683..a0395d4a1 100644 --- a/MEDICONNECT 2/README.md +++ b/MEDICONNECT 2/README.md @@ -58,6 +58,7 @@ npx wrangler pages deploy dist --project-name=mediconnect --branch=production ### Autenticação JWT com Supabase O sistema usa **Supabase Auth** com tokens JWT. Todo login retorna: + - `access_token` (JWT, expira em 1 hora) - `refresh_token` (para renovação automática) @@ -65,47 +66,51 @@ O sistema usa **Supabase Auth** com tokens JWT. Todo login retorna: ```typescript // Adiciona token automaticamente em todas as requisições -axios.interceptors.request.use(config => { - const token = localStorage.getItem('access_token') +axios.interceptors.request.use((config) => { + const token = localStorage.getItem("access_token"); if (token) { - config.headers.Authorization = `Bearer ${token}` + config.headers.Authorization = `Bearer ${token}`; } - return config -}) + return config; +}); // Refresh automático quando token expira axios.interceptors.response.use( - response => response, - async error => { + (response) => response, + async (error) => { if (error.response?.status === 401) { - const refreshToken = localStorage.getItem('refresh_token') - const newTokens = await authService.refreshToken(refreshToken) + const refreshToken = localStorage.getItem("refresh_token"); + const newTokens = await authService.refreshToken(refreshToken); // Retry request original } } -) +); ``` ### Roles e Permissões (RLS) #### 👑 Admin/Gestor: + - ✅ **Acesso completo a todos os recursos** - ✅ Criar/editar/deletar usuários, médicos, pacientes - ✅ Visualizar todos os agendamentos e prontuários #### 👨‍⚕️ Médicos: + - ✅ Veem **todos os pacientes** - ✅ Veem apenas **seus próprios laudos** (filtro: `created_by = médico`) - ✅ Veem apenas **seus próprios agendamentos** (filtro: `doctor_id = médico`) - ✅ Editam apenas **seus próprios laudos e agendamentos** #### 👤 Pacientes: + - ✅ Veem apenas **seus próprios dados** - ✅ Veem apenas **seus próprios laudos** (filtro: `patient_id = paciente`) - ✅ Veem apenas **seus próprios agendamentos** - ✅ Podem agendar consultas #### 👩‍💼 Secretárias: + - ✅ Veem **todos os pacientes** - ✅ Veem **todos os agendamentos** - ✅ Veem **todos os laudos** @@ -145,96 +150,96 @@ src/services/ ```typescript // Login -await authService.login({ email, password }) +await authService.login({ email, password }); // Retorna: { access_token, refresh_token, user } // Recuperação de senha -await authService.requestPasswordReset(email) +await authService.requestPasswordReset(email); // Envia email com link de reset // Atualizar senha -await authService.updatePassword(accessToken, newPassword) +await authService.updatePassword(accessToken, newPassword); // Usado na página /reset-password // Refresh token -await authService.refreshToken(refreshToken) +await authService.refreshToken(refreshToken); ``` #### 👤 Usuários (userService) ```typescript // Buscar informações do usuário autenticado (com roles) -const userInfo = await userService.getUserInfo() +const userInfo = await userService.getUserInfo(); // Retorna: { id, email, full_name, roles: ['medico', 'admin'] } // Criar usuário com role await userService.createUser({ - email: 'user@example.com', - full_name: 'Nome Completo', - role: 'medico' // ou 'admin', 'paciente', 'secretaria' -}) + email: "user@example.com", + full_name: "Nome Completo", + role: "medico", // ou 'admin', 'paciente', 'secretaria' +}); // Deletar usuário -await userService.deleteUser(userId) +await userService.deleteUser(userId); ``` #### 🏥 Pacientes (patientService) ```typescript // Listar pacientes -const patients = await patientService.list() +const patients = await patientService.list(); // Buscar por ID -const patient = await patientService.getById(id) +const patient = await patientService.getById(id); // Criar paciente await patientService.create({ - email: 'paciente@example.com', - full_name: 'Nome Paciente', - cpf: '12345678900', - phone_mobile: '11999999999' -}) + email: "paciente@example.com", + full_name: "Nome Paciente", + cpf: "12345678900", + phone_mobile: "11999999999", +}); // Atualizar paciente -await patientService.update(id, { phone_mobile: '11888888888' }) +await patientService.update(id, { phone_mobile: "11888888888" }); // Deletar paciente -await patientService.delete(id) +await patientService.delete(id); ``` #### 👨‍⚕️ Médicos (doctorService) ```typescript // Listar médicos -const doctors = await doctorService.list() +const doctors = await doctorService.list(); // Buscar por ID -const doctor = await doctorService.getById(id) +const doctor = await doctorService.getById(id); // Buscar disponibilidade -const slots = await doctorService.getAvailableSlots(doctorId, date) +const slots = await doctorService.getAvailableSlots(doctorId, date); ``` #### 📅 Agendamentos (appointmentService) ```typescript // Listar agendamentos (filtrado por role automaticamente) -const appointments = await appointmentService.list() +const appointments = await appointmentService.list(); // Criar agendamento await appointmentService.create({ - patient_id: 'uuid-paciente', - doctor_id: 'uuid-medico', - scheduled_at: '2025-10-25T10:00:00', - reason: 'Consulta de rotina' -}) + patient_id: "uuid-paciente", + doctor_id: "uuid-medico", + scheduled_at: "2025-10-25T10:00:00", + reason: "Consulta de rotina", +}); // Atualizar status -await appointmentService.updateStatus(id, 'confirmed') +await appointmentService.updateStatus(id, "confirmed"); // Status: requested, confirmed, completed, cancelled // Cancelar -await appointmentService.cancel(id, 'Motivo do cancelamento') +await appointmentService.cancel(id, "Motivo do cancelamento"); ``` --- @@ -253,9 +258,9 @@ export const API_CONFIG = { STORAGE_KEYS: { ACCESS_TOKEN: "mediconnect_access_token", REFRESH_TOKEN: "mediconnect_refresh_token", - USER: "mediconnect_user" - } -} + USER: "mediconnect_user", + }, +}; ``` --- @@ -292,11 +297,11 @@ npx wrangler pages deploy dist --project-name=mediconnect --commit-dirty=true ## 1. Variáveis de Ambiente (`.env` / `.env.local`) -| Variável | Obrigatória | Descrição | -| ------------------------ | ---------------- | --------------------------------------------------------------- | -| `VITE_SUPABASE_URL` | Sim | URL base do projeto Supabase (`https://.supabase.co`) | -| `VITE_SUPABASE_ANON_KEY` | Sim | Chave pública (anon) usada para Auth password grant e PostgREST | -| `VITE_APP_ENV` | Não | Identifica ambiente (ex: `dev`, `staging`, `prod`) | +| Variável | Obrigatória | Descrição | +| ------------------------ | ----------- | --------------------------------------------------------------- | +| `VITE_SUPABASE_URL` | Sim | URL base do projeto Supabase (`https://.supabase.co`) | +| `VITE_SUPABASE_ANON_KEY` | Sim | Chave pública (anon) usada para Auth password grant e PostgREST | +| `VITE_APP_ENV` | Não | Identifica ambiente (ex: `dev`, `staging`, `prod`) | **Nota:** As variáveis já estão configuradas no código em `src/services/api/config.ts`. Não é necessário criar arquivo `.env` para desenvolvimento local. @@ -314,38 +319,37 @@ Boas práticas: ```typescript // pages/LoginMedico.tsx const handleLogin = async (e: FormEvent) => { - e.preventDefault() - + e.preventDefault(); + try { // 1. Autenticar com Supabase - const loginResponse = await authService.login({ email, password }) - + const loginResponse = await authService.login({ email, password }); + // 2. Salvar tokens - localStorage.setItem('access_token', loginResponse.access_token) - localStorage.setItem('refresh_token', loginResponse.refresh_token) - + localStorage.setItem("access_token", loginResponse.access_token); + localStorage.setItem("refresh_token", loginResponse.refresh_token); + // 3. Buscar informações do usuário com roles - const userInfo = await userService.getUserInfo() - const roles = userInfo.roles || [] - + const userInfo = await userService.getUserInfo(); + const roles = userInfo.roles || []; + // 4. Validar permissões - const isAdmin = roles.includes('admin') - const isGestor = roles.includes('gestor') - const isMedico = roles.includes('medico') - + const isAdmin = roles.includes("admin"); + const isGestor = roles.includes("gestor"); + const isMedico = roles.includes("medico"); + if (!isAdmin && !isGestor && !isMedico) { - toast.error("Você não tem permissão para acessar esta área") - await authService.logout() - return + toast.error("Você não tem permissão para acessar esta área"); + await authService.logout(); + return; } - + // 5. Redirecionar para o painel - navigate('/painel-medico') - + navigate("/painel-medico"); } catch (error) { - toast.error("Email ou senha incorretos") + toast.error("Email ou senha incorretos"); } -} +}; ``` --- @@ -367,35 +371,35 @@ const handleLogin = async (e: FormEvent) => { // Solicitar recuperação (páginas de login) const handlePasswordReset = async () => { try { - await authService.requestPasswordReset(email) - toast.success("Link de recuperação enviado para seu email") + await authService.requestPasswordReset(email); + toast.success("Link de recuperação enviado para seu email"); } catch (error) { - toast.error("Erro ao enviar email de recuperação") + toast.error("Erro ao enviar email de recuperação"); } -} +}; // Página de reset (ResetPassword.tsx) useEffect(() => { // Extrair token do URL - const hash = window.location.hash - const params = new URLSearchParams(hash.substring(1)) - const token = params.get('access_token') - + const hash = window.location.hash; + const params = new URLSearchParams(hash.substring(1)); + const token = params.get("access_token"); + if (token) { - setAccessToken(token) - setIsLoading(false) + setAccessToken(token); + setIsLoading(false); } -}, []) +}, []); const handleSubmit = async (e: FormEvent) => { try { - await authService.updatePassword(accessToken, newPassword) - toast.success("Senha atualizada com sucesso!") - navigate('/login-paciente') + await authService.updatePassword(accessToken, newPassword); + toast.success("Senha atualizada com sucesso!"); + navigate("/login-paciente"); } catch (error) { - toast.error("Erro ao atualizar senha") + toast.error("Erro ao atualizar senha"); } -} +}; ``` --- @@ -405,6 +409,7 @@ const handleSubmit = async (e: FormEvent) => { ### Tabelas Principais #### `profiles` + ```sql - id (uuid, PK) - email (text, unique) @@ -415,6 +420,7 @@ const handleSubmit = async (e: FormEvent) => { ``` #### `patients` + ```sql - id (uuid, PK, FK -> profiles) - email (text, unique) @@ -427,6 +433,7 @@ const handleSubmit = async (e: FormEvent) => { ``` #### `doctors` + ```sql - id (uuid, PK, FK -> profiles) - email (text, unique) @@ -438,6 +445,7 @@ const handleSubmit = async (e: FormEvent) => { ``` #### `appointments` + ```sql - id (uuid, PK) - patient_id (uuid, FK -> patients) @@ -450,6 +458,7 @@ const handleSubmit = async (e: FormEvent) => { ``` #### `user_roles` + ```sql - user_id (uuid, FK -> profiles) - role (text: admin, gestor, medico, secretaria, paciente) @@ -462,13 +471,14 @@ const handleSubmit = async (e: FormEvent) => { O sistema usa uma estratégia híbrida para máxima segurança: -| Tipo | Local | Expiração Natural | -| -------------- | ----------------- | ------------------------------------ | -| Access Token | localStorage | 1 hora (renovado automaticamente) | -| Refresh Token | localStorage | 30 dias (ou revogação backend) | -| User Snapshot | localStorage | Limpo em logout | +| Tipo | Local | Expiração Natural | +| ------------- | ------------ | --------------------------------- | +| Access Token | localStorage | 1 hora (renovado automaticamente) | +| Refresh Token | localStorage | 30 dias (ou revogação backend) | +| User Snapshot | localStorage | Limpo em logout | **Segurança:** + - Tokens são limpos automaticamente no logout - Refresh automático quando access_token expira (401) - Interceptors garantem tokens válidos em todas as requisições @@ -497,6 +507,7 @@ Fluxo de Refresh: 1. Requisição falha com 401. 2. Wrapper (`http.ts`) obtém refresh do `tokenStore`. + --- ## 7. Scripts Utilitários @@ -545,9 +556,9 @@ node search-fernando.cjs ```typescript // Imports -import React, { useState, useEffect } from "react" -import { useNavigate } from "react-router-dom" -import { serviceImport } from "../services" +import React, { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { serviceImport } from "../services"; // Types interface Props { @@ -557,28 +568,24 @@ interface Props { // Component const ComponentName: React.FC = ({ ...props }) => { // Hooks - const navigate = useNavigate() - const [state, setState] = useState() - + const navigate = useNavigate(); + const [state, setState] = useState(); + // Effects useEffect(() => { // ... - }, []) - + }, []); + // Handlers const handleAction = async () => { // ... - } - - // Render - return ( -
- {/* JSX */} -
- ) -} + }; -export default ComponentName + // Render + return
{/* JSX */}
; +}; + +export default ComponentName; ``` --- @@ -586,6 +593,7 @@ export default ComponentName ## 9. Tecnologias Utilizadas ### Frontend + - **React** 18.3.1 - Biblioteca UI - **TypeScript** 5.9.3 - Tipagem estática - **Vite** 7.1.10 - Build tool @@ -596,11 +604,13 @@ export default ComponentName - **date-fns** 4.1.0 - Manipulação de datas ### Backend + - **Supabase** - Backend as a Service - **PostgreSQL** - Banco de dados relacional - **Supabase Auth** - Autenticação JWT ### Deploy + - **Cloudflare Pages** - Hospedagem frontend - **Wrangler** 4.44.0 - CLI Cloudflare @@ -615,4 +625,3 @@ export default ComponentName --- **Desenvolvido com ❤️ pela Squad 18** - diff --git a/MEDICONNECT 2/create-aurora-appointment.cjs b/MEDICONNECT 2/create-aurora-appointment.cjs new file mode 100644 index 000000000..f0d93a4e7 --- /dev/null +++ b/MEDICONNECT 2/create-aurora-appointment.cjs @@ -0,0 +1,121 @@ +const axios = require('axios'); + +const SUPABASE_URL = 'https://yuanqfswhberkoevtmfr.supabase.co'; +const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ'; + +async function createAppointment() { + try { + console.log('🔐 Fazendo login como Aurora...'); + + // Login como Aurora + const loginResponse = await axios.post( + `${SUPABASE_URL}/auth/v1/token?grant_type=password`, + { + email: 'aurora-nascimento94@gmx.com', + password: 'auroranasc94' + }, + { + headers: { + 'apikey': SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + } + ); + + const auroraToken = loginResponse.data.access_token; + console.log('✅ Login realizado como Aurora'); + + // Buscar o patient_id da Aurora + console.log('\n👤 Buscando dados da paciente...'); + const patientResponse = await axios.get( + `${SUPABASE_URL}/rest/v1/patients?user_id=eq.${loginResponse.data.user.id}`, + { + headers: { + 'apikey': SUPABASE_ANON_KEY, + 'Authorization': `Bearer ${auroraToken}` + } + } + ); + + if (!patientResponse.data || patientResponse.data.length === 0) { + throw new Error('Paciente não encontrada'); + } + + const patientId = patientResponse.data[0].id; + console.log(`✅ Patient ID: ${patientId}`); + + // Buscar disponibilidade do Dr. Fernando para segunda-feira + console.log('\n📅 Buscando disponibilidade do Dr. Fernando...'); + const availabilityResponse = await axios.get( + `${SUPABASE_URL}/rest/v1/doctor_availability?doctor_id=eq.6dad001d-229b-40b5-80f3-310243c4599c&weekday=eq.monday`, + { + headers: { + 'apikey': SUPABASE_ANON_KEY, + 'Authorization': `Bearer ${auroraToken}` + } + } + ); + + if (!availabilityResponse.data || availabilityResponse.data.length === 0) { + throw new Error('Disponibilidade não encontrada'); + } + + const availability = availabilityResponse.data[0]; + console.log(`✅ Disponibilidade encontrada: ${availability.start_time} - ${availability.end_time}`); + + // Criar consulta para próxima segunda-feira às 10:00 + const today = new Date(); + const daysUntilMonday = (1 - today.getDay() + 7) % 7 || 7; // Próxima segunda + const appointmentDate = new Date(today); + appointmentDate.setDate(today.getDate() + daysUntilMonday); + appointmentDate.setHours(10, 0, 0, 0); + + const scheduledAt = appointmentDate.toISOString(); + + console.log(`\n📝 Criando consulta para ${scheduledAt}...`); + + const appointmentData = { + patient_id: patientId, + doctor_id: '6dad001d-229b-40b5-80f3-310243c4599c', + scheduled_at: scheduledAt, + duration_minutes: 30, + appointment_type: 'presencial', + chief_complaint: 'Consulta de rotina' + }; + + const appointmentResponse = await axios.post( + `${SUPABASE_URL}/rest/v1/appointments`, + appointmentData, + { + headers: { + 'apikey': SUPABASE_ANON_KEY, + 'Authorization': `Bearer ${auroraToken}`, + 'Content-Type': 'application/json', + 'Prefer': 'return=representation' + } + } + ); + + console.log('\n✅ Consulta criada com sucesso!'); + console.log('\n📋 Detalhes da consulta:'); + console.log(` - Paciente: Aurora Sabrina Clara Nascimento`); + console.log(` - Médico: Dr. Fernando Pirichowski`); + console.log(` - Data/Hora: ${scheduledAt}`); + console.log(` - Duração: 30 minutos`); + console.log(` - Tipo: presencial`); + + if (appointmentResponse.data && appointmentResponse.data.length > 0) { + console.log(` - ID da consulta: ${appointmentResponse.data[0].id}`); + console.log(` - Order Number: ${appointmentResponse.data[0].order_number}`); + console.log(` - Status: ${appointmentResponse.data[0].status}`); + } + + } catch (error) { + console.error('❌ Erro ao criar consulta:', error.response?.data || error.message); + if (error.response?.data) { + console.error('Detalhes:', JSON.stringify(error.response.data, null, 2)); + } + } +} + +createAppointment(); diff --git a/MEDICONNECT 2/create-fernando-availability.cjs b/MEDICONNECT 2/create-fernando-availability.cjs new file mode 100644 index 000000000..e47e5ef72 --- /dev/null +++ b/MEDICONNECT 2/create-fernando-availability.cjs @@ -0,0 +1,91 @@ +const axios = require('axios'); + +const SUPABASE_URL = 'https://yuanqfswhberkoevtmfr.supabase.co'; +const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ'; + +// IDs +const DOCTOR_ID = '6dad001d-229b-40b5-80f3-310243c4599c'; // Fernando (CRM 24245) +const ADMIN_ID = 'c7fcd702-9a6e-4b7c-abd3-956b25af407d'; // Admin (riseup) + +async function main() { + try { + console.log('🔐 Fazendo login como admin...'); + + const loginResponse = await axios.post( + `${SUPABASE_URL}/auth/v1/token?grant_type=password`, + { + email: 'riseup@popcode.com.br', + password: 'riseup' + }, + { + headers: { + 'Content-Type': 'application/json', + 'apikey': SUPABASE_ANON_KEY + } + } + ); + + const adminToken = loginResponse.data.access_token; + console.log('✅ Login realizado\n'); + + console.log('📅 Criando disponibilidade para Dr. Fernando...'); + console.log('⏰ Horário: 07:00 às 19:00'); + console.log('📆 Dias: Segunda a Domingo\n'); + + const weekdays = [ + { num: 'sunday', name: 'Domingo' }, + { num: 'monday', name: 'Segunda-feira' }, + { num: 'tuesday', name: 'Terça-feira' }, + { num: 'wednesday', name: 'Quarta-feira' }, + { num: 'thursday', name: 'Quinta-feira' }, + { num: 'friday', name: 'Sexta-feira' }, + { num: 'saturday', name: 'Sábado' } + ]; + + for (const day of weekdays) { + try { + const availabilityData = { + doctor_id: DOCTOR_ID, + weekday: day.num, + start_time: '07:00:00', + end_time: '19:00:00', + slot_minutes: 30, + appointment_type: 'presencial', + active: true, + created_by: ADMIN_ID + }; + + const response = await axios.post( + `${SUPABASE_URL}/rest/v1/doctor_availability`, + availabilityData, + { + headers: { + 'Content-Type': 'application/json', + 'apikey': SUPABASE_ANON_KEY, + 'Authorization': `Bearer ${adminToken}`, + 'Prefer': 'return=representation' + } + } + ); + + console.log(`✅ ${day.name}: Disponibilidade criada`); + + } catch (error) { + console.error(`❌ ${day.name}: Erro -`, error.response?.data?.message || error.message); + } + } + + console.log('\n🎉 Disponibilidade criada com sucesso!'); + console.log('\n📋 Resumo:'); + console.log('- Médico: Dr. Fernando Pirichowski'); + console.log('- Dias: Todos os dias da semana (Domingo a Sábado)'); + console.log('- Horário: 07:00 às 19:00'); + console.log('- Duração consulta: 30 minutos'); + console.log('- Tipo: Presencial'); + + } catch (error) { + console.error('❌ Erro geral:', error.response?.data || error.message); + } +} + +main(); diff --git a/MEDICONNECT 2/create-patient-with-password.cjs b/MEDICONNECT 2/create-patient-with-password.cjs new file mode 100644 index 000000000..68b003924 --- /dev/null +++ b/MEDICONNECT 2/create-patient-with-password.cjs @@ -0,0 +1,78 @@ +const axios = require('axios'); + +// Configuração +const SUPABASE_URL = 'https://yuanqfswhberkoevtmfr.supabase.co'; +const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ'; + +// Credenciais do admin +const ADMIN_EMAIL = 'riseup@popcode.com.br'; +const ADMIN_PASSWORD = 'riseup'; + +// Dados do paciente (Aurora Sabrina Clara Nascimento) +const PATIENT_DATA = { + email: 'aurora-nascimento94@gmx.com', + password: 'auroranasc94', + full_name: 'Aurora Sabrina Clara Nascimento', + phone_mobile: '(21) 99856-3014', + cpf: '66864784231', // CPF sem pontuação + create_patient_record: true, + role: 'paciente' +}; + +async function main() { + try { + console.log('🔐 1. Fazendo login como admin...'); + + // 1. Login do admin + const loginResponse = await axios.post( + `${SUPABASE_URL}/auth/v1/token?grant_type=password`, + { + email: ADMIN_EMAIL, + password: ADMIN_PASSWORD + }, + { + headers: { + 'Content-Type': 'application/json', + 'apikey': SUPABASE_ANON_KEY + } + } + ); + + const adminToken = loginResponse.data.access_token; + console.log('✅ Login realizado com sucesso!'); + console.log('🔑 Token:', adminToken.substring(0, 30) + '...'); + + console.log('\n👤 2. Criando paciente...'); + console.log('Dados:', JSON.stringify(PATIENT_DATA, null, 2)); + + // 2. Criar paciente + const createResponse = await axios.post( + `${SUPABASE_URL}/functions/v1/create-user-with-password`, + PATIENT_DATA, + { + headers: { + 'Content-Type': 'application/json', + 'apikey': SUPABASE_ANON_KEY, + 'Authorization': `Bearer ${adminToken}` + } + } + ); + + console.log('\n✅ Paciente criado com sucesso!'); + console.log('Resposta:', JSON.stringify(createResponse.data, null, 2)); + + if (createResponse.data.patient_id) { + console.log('\n📋 ID do paciente:', createResponse.data.patient_id); + console.log('✉️ Email de confirmação enviado para:', PATIENT_DATA.email); + console.log('🔐 Senha temporária:', PATIENT_DATA.password); + console.log('\n⚠️ O usuário precisa confirmar o email antes de fazer login!'); + } + + } catch (error) { + console.error('❌ Erro:', error.response?.data || error.message); + console.error('Status:', error.response?.status); + console.error('URL:', error.config?.url); + } +} + +main(); diff --git a/MEDICONNECT 2/fix-aurora-user-id.cjs b/MEDICONNECT 2/fix-aurora-user-id.cjs new file mode 100644 index 000000000..b27e40ae8 --- /dev/null +++ b/MEDICONNECT 2/fix-aurora-user-id.cjs @@ -0,0 +1,72 @@ +const axios = require('axios'); + +const SUPABASE_URL = 'https://yuanqfswhberkoevtmfr.supabase.co'; +const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ'; + +async function fixAuroraUserId() { + try { + console.log('🔐 Fazendo login como admin...'); + + const loginResponse = await axios.post( + `${SUPABASE_URL}/auth/v1/token?grant_type=password`, + { + email: 'riseup@popcode.com.br', + password: 'riseup' + }, + { + headers: { + 'apikey': SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + } + ); + + const adminToken = loginResponse.data.access_token; + console.log('✅ Login realizado'); + + // Fazer login como Aurora para pegar o user_id + console.log('\n👤 Fazendo login como Aurora...'); + const auroraLoginResponse = await axios.post( + `${SUPABASE_URL}/auth/v1/token?grant_type=password`, + { + email: 'aurora-nascimento94@gmx.com', + password: 'auroranasc94' + }, + { + headers: { + 'apikey': SUPABASE_ANON_KEY, + 'Content-Type': 'application/json' + } + } + ); + + const auroraUserId = auroraLoginResponse.data.user.id; + console.log(`✅ User ID da Aurora: ${auroraUserId}`); + + // Atualizar patient com user_id + console.log('\n📝 Atualizando registro da paciente...'); + const updateResponse = await axios.patch( + `${SUPABASE_URL}/rest/v1/patients?id=eq.b85486f7-9135-4b67-9aa7-b884d9603d12`, + { + user_id: auroraUserId + }, + { + headers: { + 'apikey': SUPABASE_ANON_KEY, + 'Authorization': `Bearer ${adminToken}`, + 'Content-Type': 'application/json', + 'Prefer': 'return=representation' + } + } + ); + + console.log('✅ Registro atualizado com sucesso!'); + console.log(` - Patient ID: b85486f7-9135-4b67-9aa7-b884d9603d12`); + console.log(` - User ID: ${auroraUserId}`); + + } catch (error) { + console.error('❌ Erro:', error.response?.data || error.message); + } +} + +fixAuroraUserId(); diff --git a/MEDICONNECT 2/get-aurora-info.cjs b/MEDICONNECT 2/get-aurora-info.cjs new file mode 100644 index 000000000..026052609 --- /dev/null +++ b/MEDICONNECT 2/get-aurora-info.cjs @@ -0,0 +1,59 @@ +const axios = require('axios'); + +const SUPABASE_URL = 'https://yuanqfswhberkoevtmfr.supabase.co'; +const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ'; + +async function main() { + try { + console.log('🔐 Fazendo login como admin...'); + + const loginResponse = await axios.post( + `${SUPABASE_URL}/auth/v1/token?grant_type=password`, + { + email: 'riseup@popcode.com.br', + password: 'riseup' + }, + { + headers: { + 'Content-Type': 'application/json', + 'apikey': SUPABASE_ANON_KEY + } + } + ); + + const adminToken = loginResponse.data.access_token; + console.log('✅ Login realizado\n'); + + console.log('👤 Buscando dados de Aurora na tabela patients...'); + + const patientsResponse = await axios.get( + `${SUPABASE_URL}/rest/v1/patients?email=eq.aurora-nascimento94@gmx.com`, + { + headers: { + 'apikey': SUPABASE_ANON_KEY, + 'Authorization': `Bearer ${adminToken}` + } + } + ); + + if (patientsResponse.data.length > 0) { + const patient = patientsResponse.data[0]; + console.log('✅ Paciente encontrada!\n'); + console.log('📋 DADOS DA AURORA:\n'); + console.log('User ID (auth):', patient.id); + console.log('Patient ID:', patient.id); // Em patients, o id é o mesmo do auth + console.log('Nome:', patient.full_name); + console.log('Email:', patient.email); + console.log('CPF:', patient.cpf); + console.log('Telefone:', patient.phone_mobile); + console.log('\n📄 Dados completos:', JSON.stringify(patient, null, 2)); + } else { + console.log('❌ Paciente não encontrada na tabela patients'); + } + + } catch (error) { + console.error('❌ Erro:', error.response?.data || error.message); + } +} + +main(); diff --git a/MEDICONNECT 2/get-fernando-complete.cjs b/MEDICONNECT 2/get-fernando-complete.cjs new file mode 100644 index 000000000..e4fd5b393 --- /dev/null +++ b/MEDICONNECT 2/get-fernando-complete.cjs @@ -0,0 +1,76 @@ +const axios = require('axios'); + +const SUPABASE_URL = 'https://yuanqfswhberkoevtmfr.supabase.co'; +const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ'; + +async function main() { + try { + console.log('🔐 Fazendo login como admin...'); + + const loginResponse = await axios.post( + `${SUPABASE_URL}/auth/v1/token?grant_type=password`, + { + email: 'riseup@popcode.com.br', + password: 'riseup' + }, + { + headers: { + 'Content-Type': 'application/json', + 'apikey': SUPABASE_ANON_KEY + } + } + ); + + const adminToken = loginResponse.data.access_token; + console.log('✅ Login realizado\n'); + + console.log('🔍 Buscando Fernando em profiles...'); + + const profilesResponse = await axios.get( + `${SUPABASE_URL}/rest/v1/profiles?email=eq.fernando.pirichowski@souunit.com.br`, + { + headers: { + 'apikey': SUPABASE_ANON_KEY, + 'Authorization': `Bearer ${adminToken}` + } + } + ); + + if (profilesResponse.data.length > 0) { + console.log(`✅ ${profilesResponse.data.length} perfil(is) encontrado(s)!\n`); + + profilesResponse.data.forEach((profile, index) => { + console.log(`📋 PERFIL ${index + 1}:\n`); + console.log('User ID:', profile.id); + console.log('Email:', profile.email); + console.log('Nome:', profile.full_name); + console.log('Telefone:', profile.phone || 'Não informado'); + console.log('\n' + '='.repeat(60) + '\n'); + }); + + // Pegar roles do primeiro perfil + const userId = profilesResponse.data[0].id; + console.log('🔍 Buscando roles...'); + + const rolesResponse = await axios.get( + `${SUPABASE_URL}/rest/v1/user_roles?user_id=eq.${userId}`, + { + headers: { + 'apikey': SUPABASE_ANON_KEY, + 'Authorization': `Bearer ${adminToken}` + } + } + ); + + console.log('📌 Roles:', rolesResponse.data.map(r => r.role).join(', ')); + + } else { + console.log('❌ Nenhum perfil encontrado'); + } + + } catch (error) { + console.error('❌ Erro:', error.response?.data || error.message); + } +} + +main(); diff --git a/MEDICONNECT 2/get-fernando-doctor-id.cjs b/MEDICONNECT 2/get-fernando-doctor-id.cjs new file mode 100644 index 000000000..c6544e4c0 --- /dev/null +++ b/MEDICONNECT 2/get-fernando-doctor-id.cjs @@ -0,0 +1,78 @@ +const axios = require('axios'); + +const SUPABASE_URL = 'https://yuanqfswhberkoevtmfr.supabase.co'; +const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ'; + +async function main() { + try { + console.log('🔐 Fazendo login como admin...'); + + const loginResponse = await axios.post( + `${SUPABASE_URL}/auth/v1/token?grant_type=password`, + { + email: 'riseup@popcode.com.br', + password: 'riseup' + }, + { + headers: { + 'Content-Type': 'application/json', + 'apikey': SUPABASE_ANON_KEY + } + } + ); + + const adminToken = loginResponse.data.access_token; + console.log('✅ Login realizado\n'); + + console.log('👨‍⚕️ Buscando dados de Fernando na tabela doctors...'); + + const doctorsResponse = await axios.get( + `${SUPABASE_URL}/rest/v1/doctors?email=eq.fernando.pirichowski@souunit.com.br`, + { + headers: { + 'apikey': SUPABASE_ANON_KEY, + 'Authorization': `Bearer ${adminToken}`, + 'Prefer': 'return=representation' + } + } + ); + + if (doctorsResponse.data.length > 0) { + console.log(`✅ ${doctorsResponse.data.length} médico(s) encontrado(s)!\n`); + + doctorsResponse.data.forEach((doctor, index) => { + console.log(`📋 MÉDICO ${index + 1}:\n`); + console.log('Doctor ID:', doctor.id); + console.log('Nome:', doctor.full_name); + console.log('Email:', doctor.email); + console.log('CRM:', doctor.crm); + console.log('Especialidade:', doctor.specialty || 'Não informada'); + console.log('Telefone:', doctor.phone || 'Não informado'); + console.log('\n' + '='.repeat(60) + '\n'); + }); + } else { + console.log('❌ Nenhum médico chamado Fernando encontrado'); + console.log('\n🔍 Buscando todos os médicos...'); + + const allDoctorsResponse = await axios.get( + `${SUPABASE_URL}/rest/v1/doctors`, + { + headers: { + 'apikey': SUPABASE_ANON_KEY, + 'Authorization': `Bearer ${adminToken}` + } + } + ); + + console.log(`\n📊 Total de médicos cadastrados: ${allDoctorsResponse.data.length}`); + allDoctorsResponse.data.forEach((doctor, index) => { + console.log(`${index + 1}. ${doctor.full_name} - ${doctor.email}`); + }); + } + + } catch (error) { + console.error('❌ Erro:', error.response?.data || error.message); + } +} + +main(); diff --git a/MEDICONNECT 2/get-fernando-user-id.cjs b/MEDICONNECT 2/get-fernando-user-id.cjs new file mode 100644 index 000000000..6d181c64d --- /dev/null +++ b/MEDICONNECT 2/get-fernando-user-id.cjs @@ -0,0 +1,38 @@ +const axios = require('axios'); + +const SUPABASE_URL = 'https://yuanqfswhberkoevtmfr.supabase.co'; +const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ'; + +async function main() { + try { + console.log('🔐 Fazendo login como Fernando...'); + + const loginResponse = await axios.post( + `${SUPABASE_URL}/auth/v1/token?grant_type=password`, + { + email: 'fernando.pirichowski@souunit.com.br', + password: 'fernando123' + }, + { + headers: { + 'Content-Type': 'application/json', + 'apikey': SUPABASE_ANON_KEY + } + } + ); + + const userData = loginResponse.data; + console.log('✅ Login realizado\n'); + + console.log('📋 DADOS DO FERNANDO (AUTH):\n'); + console.log('User ID:', userData.user.id); + console.log('Email:', userData.user.email); + console.log('Role:', userData.user.role); + console.log('\n' + '='.repeat(60)); + + } catch (error) { + console.error('❌ Erro:', error.response?.data || error.message); + } +} + +main(); diff --git a/MEDICONNECT 2/src/components/auth/RecoveryRedirect.tsx b/MEDICONNECT 2/src/components/auth/RecoveryRedirect.tsx new file mode 100644 index 000000000..8e676a90f --- /dev/null +++ b/MEDICONNECT 2/src/components/auth/RecoveryRedirect.tsx @@ -0,0 +1,54 @@ +import { useEffect } from "react"; +import { useNavigate, useLocation } from "react-router-dom"; + +/** + * Componente que detecta tokens de recuperação na URL e redireciona para /reset-password + * Funciona tanto com query string (?token=xxx) quanto com hash (#access_token=xxx) + */ +const RecoveryRedirect: React.FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + + useEffect(() => { + console.log("[RecoveryRedirect] Verificando URL..."); + console.log("[RecoveryRedirect] Pathname:", location.pathname); + console.log("[RecoveryRedirect] Search:", location.search); + console.log("[RecoveryRedirect] Hash:", location.hash); + + let shouldRedirect = false; + + // Verificar query string: ?token=xxx&type=recovery + const searchParams = new URLSearchParams(location.search); + const queryToken = searchParams.get("token"); + const queryType = searchParams.get("type"); + + if (queryToken && queryType === "recovery") { + console.log("[RecoveryRedirect] ✅ Token de recovery no query string detectado"); + shouldRedirect = true; + } + + // Verificar hash: #access_token=xxx&type=recovery + if (location.hash) { + const hashParams = new URLSearchParams(location.hash.substring(1)); + const hashToken = hashParams.get("access_token"); + const hashType = hashParams.get("type"); + + if (hashToken && hashType === "recovery") { + console.log("[RecoveryRedirect] ✅ Token de recovery no hash detectado"); + shouldRedirect = true; + } + } + + if (shouldRedirect) { + console.log("[RecoveryRedirect] 🔄 Redirecionando para /reset-password"); + // Preservar os parâmetros e redirecionar + navigate(`/reset-password${location.search}${location.hash}`, { replace: true }); + } else { + console.log("[RecoveryRedirect] ℹ️ Nenhum token de recovery detectado"); + } + }, [location, navigate]); + + return null; // Componente invisível +}; + +export default RecoveryRedirect; diff --git a/MEDICONNECT 2/src/pages/AuthCallback.tsx b/MEDICONNECT 2/src/pages/AuthCallback.tsx index 75e42ae82..6ecd3c591 100644 --- a/MEDICONNECT 2/src/pages/AuthCallback.tsx +++ b/MEDICONNECT 2/src/pages/AuthCallback.tsx @@ -21,7 +21,32 @@ export default function AuthCallback() { useEffect(() => { const handleCallback = async () => { try { - console.log("[AuthCallback] Iniciando processamento do magic link"); + console.log("[AuthCallback] Iniciando processamento"); + + // Verificar se é um token de recovery + const hash = window.location.hash; + const hashParams = new URLSearchParams(hash.substring(1)); + const accessToken = hashParams.get("access_token"); + const type = hashParams.get("type"); + + console.log("[AuthCallback] Hash:", hash); + console.log("[AuthCallback] Type:", type); + console.log("[AuthCallback] Access Token presente:", !!accessToken); + + // Se for recovery, redirecionar para página de reset + if (type === "recovery" && accessToken) { + console.log("[AuthCallback] ✅ Token de recovery detectado, redirecionando para /reset-password"); + setStatus("success"); + setMessage("Redirecionando para página de redefinição de senha..."); + + // Redirecionar preservando o hash com o token + setTimeout(() => { + navigate(`/reset-password${hash}`, { replace: true }); + }, 1000); + return; + } + + console.log("[AuthCallback] Processando magic link normal"); // Supabase automaticamente processa os query params const { diff --git a/MEDICONNECT 2/src/pages/Home.tsx b/MEDICONNECT 2/src/pages/Home.tsx index be09d7730..a3377e723 100644 --- a/MEDICONNECT 2/src/pages/Home.tsx +++ b/MEDICONNECT 2/src/pages/Home.tsx @@ -6,6 +6,7 @@ import { MetricCard } from "../components/MetricCard"; import { HeroBanner } from "../components/HeroBanner"; import { i18n } from "../i18n"; import { useAuth } from "../hooks/useAuth"; +import RecoveryRedirect from "../components/auth/RecoveryRedirect"; const Home: React.FC = () => { const [stats, setStats] = useState({ @@ -97,6 +98,9 @@ const Home: React.FC = () => { return (
+ {/* Componente invisível que detecta tokens de recuperação e redireciona */} + + {/* Hero Section com Background Rotativo */} diff --git a/MEDICONNECT 2/src/pages/ResetPassword.tsx b/MEDICONNECT 2/src/pages/ResetPassword.tsx index f022dca41..09a1d643e 100644 --- a/MEDICONNECT 2/src/pages/ResetPassword.tsx +++ b/MEDICONNECT 2/src/pages/ResetPassword.tsx @@ -14,35 +14,48 @@ const ResetPassword: React.FC = () => { const navigate = useNavigate(); useEffect(() => { - // Extrair access_token do hash da URL + // Extrair access_token do hash da URL (#access_token=...) + // OU do query string (?token=...&type=recovery) + + let token: string | null = null; + let type: string | null = null; + + // Primeiro tenta no hash (formato padrão do Supabase após redirect) const hash = window.location.hash; console.log("[ResetPassword] Hash completo:", hash); if (hash) { - const params = new URLSearchParams(hash.substring(1)); - const token = params.get("access_token"); - const type = params.get("type"); + const hashParams = new URLSearchParams(hash.substring(1)); + token = hashParams.get("access_token"); + type = hashParams.get("type"); + + console.log("[ResetPassword] Token do hash:", token ? token.substring(0, 20) + "..." : "null"); + console.log("[ResetPassword] Type do hash:", type); + } - console.log( - "[ResetPassword] Token extraído:", - token ? token.substring(0, 20) + "..." : "null" - ); - console.log("[ResetPassword] Type:", type); - - if (token) { - setAccessToken(token); - console.log( - "[ResetPassword] ✅ Token de recuperação detectado e armazenado" - ); - } else { - console.error("[ResetPassword] ❌ Token não encontrado no hash"); - toast.error("Link de recuperação inválido ou expirado"); - setTimeout(() => navigate("/"), 3000); + // Se não encontrou no hash, tenta no query string + if (!token) { + const search = window.location.search; + console.log("[ResetPassword] Query string completo:", search); + + if (search) { + const queryParams = new URLSearchParams(search); + token = queryParams.get("token"); + type = queryParams.get("type"); + + console.log("[ResetPassword] Token do query:", token ? token.substring(0, 20) + "..." : "null"); + console.log("[ResetPassword] Type do query:", type); } + } + + if (token) { + setAccessToken(token); + console.log("[ResetPassword] ✅ Token de recuperação detectado e armazenado"); + console.log("[ResetPassword] Type:", type); } else { - console.error("[ResetPassword] ❌ Nenhum hash encontrado na URL"); + console.error("[ResetPassword] ❌ Token não encontrado no hash nem no query string"); console.log("[ResetPassword] URL completa:", window.location.href); - toast.error("Link de recuperação inválido"); + toast.error("Link de recuperação inválido ou expirado"); setTimeout(() => navigate("/"), 3000); } }, [navigate]); @@ -93,10 +106,26 @@ const ResetPassword: React.FC = () => { } catch (error: unknown) { console.error("[ResetPassword] Erro ao atualizar senha:", error); const err = error as { - response?: { data?: { error_description?: string; message?: string } }; + response?: { + data?: { + error_description?: string; + message?: string; + msg?: string; + error_code?: string; + } + }; message?: string; }; + + // Mensagem específica para senha igual + if (err?.response?.data?.error_code === "same_password") { + toast.error("A nova senha deve ser diferente da senha atual"); + setLoading(false); + return; + } + const errorMessage = + err?.response?.data?.msg || err?.response?.data?.error_description || err?.response?.data?.message || err?.message || diff --git a/MEDICONNECT 2/src/services/auth/authService.ts b/MEDICONNECT 2/src/services/auth/authService.ts index 3b6a31301..5b7de7e4a 100644 --- a/MEDICONNECT 2/src/services/auth/authService.ts +++ b/MEDICONNECT 2/src/services/auth/authService.ts @@ -154,20 +154,31 @@ class AuthService { /** * Solicita reset de senha via email (público) * POST /auth/v1/recover + * + * Envia redirectTo dentro de options para controlar para onde o Supabase redireciona */ async requestPasswordReset( - email: string, - redirectUrl?: string + email: string ): Promise<{ success: boolean; message: string }> { try { + const redirectUrl = `${API_CONFIG.APP_URL}/reset-password`; + + console.log("[authService.requestPasswordReset] Email:", email); + console.log("[authService.requestPasswordReset] Redirect URL:", redirectUrl); + console.log("[authService.requestPasswordReset] Usando endpoint /auth/v1/recover"); + + const payload = { + email, + options: { + redirectTo: redirectUrl, + }, + }; + + console.log("[authService.requestPasswordReset] Payload:", JSON.stringify(payload, null, 2)); + await axios.post( `${API_CONFIG.AUTH_URL}/recover`, - { - email, - options: { - redirectTo: redirectUrl || `${API_CONFIG.APP_URL}/reset-password`, - }, - }, + payload, { headers: { "Content-Type": "application/json", @@ -176,13 +187,15 @@ class AuthService { } ); + console.log("[authService.requestPasswordReset] ✅ Email de recuperação enviado"); + return { success: true, - message: - "Email de recuperação de senha enviado com sucesso. Verifique sua caixa de entrada.", + message: "Email de recuperação de senha enviado com sucesso. Verifique sua caixa de entrada.", }; - } catch (error) { - console.error("Erro ao solicitar reset de senha:", error); + } catch (error: any) { + console.error("[authService.requestPasswordReset] ❌ Erro:", error); + console.error("[authService.requestPasswordReset] Response:", error.response?.data); throw error; } } @@ -196,7 +209,12 @@ class AuthService { newPassword: string ): Promise<{ success: boolean; message: string }> { try { - await axios.put( + console.log("[authService.updatePassword] Iniciando atualização de senha"); + console.log("[authService.updatePassword] Token (primeiros 30 chars):", accessToken.substring(0, 30)); + console.log("[authService.updatePassword] Nova senha length:", newPassword.length); + console.log("[authService.updatePassword] URL:", `${API_CONFIG.AUTH_URL}/user`); + + const response = await axios.put( `${API_CONFIG.AUTH_URL}/user`, { password: newPassword, @@ -210,12 +228,35 @@ class AuthService { } ); + console.log("[authService.updatePassword] ✅ Senha atualizada com sucesso"); + console.log("[authService.updatePassword] Response status:", response.status); + return { success: true, message: "Senha atualizada com sucesso", }; - } catch (error) { - console.error("Erro ao atualizar senha:", error); + } catch (error: any) { + console.error("[authService.updatePassword] ❌ Erro ao atualizar senha:", error); + console.error("[authService.updatePassword] Error response:", error.response?.data); + console.error("[authService.updatePassword] Error response (stringified):", JSON.stringify(error.response?.data, null, 2)); + console.error("[authService.updatePassword] Error status:", error.response?.status); + console.error("[authService.updatePassword] Error statusText:", error.response?.statusText); + console.error("[authService.updatePassword] Error headers:", error.response?.headers); + console.error("[authService.updatePassword] Request URL:", error.config?.url); + console.error("[authService.updatePassword] Request headers:", error.config?.headers); + console.error("[authService.updatePassword] Request data:", error.config?.data); + + // Se for 422, mostrar mensagem específica + if (error.response?.status === 422) { + const errorMsg = error.response?.data?.msg || error.response?.data?.message || error.response?.data?.error_description || "Token inválido ou expirado"; + console.error("[authService.updatePassword] ⚠️ Erro 422 - Possíveis causas:"); + console.error(" 1. Token de recuperação expirado (válido por 1 hora)"); + console.error(" 2. Token já foi usado"); + console.error(" 3. Formato do token incorreto"); + console.error(" 4. Usuário não existe"); + console.error("[authService.updatePassword] Mensagem do servidor:", errorMsg); + } + throw error; } }