Compare commits

..

No commits in common. "0e27dbf1ffbb170be659184c7e625fb099868fec" and "f479dcde7d974cafa7f1a8a86c7c7e7e156a70b7" have entirely different histories.

11 changed files with 534 additions and 411 deletions

View File

@ -58,7 +58,6 @@ npx wrangler pages deploy dist --project-name=mediconnect --branch=production
### Autenticação JWT com Supabase ### Autenticação JWT com Supabase
O sistema usa **Supabase Auth** com tokens JWT. Todo login retorna: O sistema usa **Supabase Auth** com tokens JWT. Todo login retorna:
- `access_token` (JWT, expira em 1 hora) - `access_token` (JWT, expira em 1 hora)
- `refresh_token` (para renovação automática) - `refresh_token` (para renovação automática)
@ -66,51 +65,47 @@ O sistema usa **Supabase Auth** com tokens JWT. Todo login retorna:
```typescript ```typescript
// Adiciona token automaticamente em todas as requisições // Adiciona token automaticamente em todas as requisições
axios.interceptors.request.use((config) => { axios.interceptors.request.use(config => {
const token = localStorage.getItem("access_token"); const token = localStorage.getItem('access_token')
if (token) { if (token) {
config.headers.Authorization = `Bearer ${token}`; config.headers.Authorization = `Bearer ${token}`
} }
return config; return config
}); })
// Refresh automático quando token expira // Refresh automático quando token expira
axios.interceptors.response.use( axios.interceptors.response.use(
(response) => response, response => response,
async (error) => { async error => {
if (error.response?.status === 401) { if (error.response?.status === 401) {
const refreshToken = localStorage.getItem("refresh_token"); const refreshToken = localStorage.getItem('refresh_token')
const newTokens = await authService.refreshToken(refreshToken); const newTokens = await authService.refreshToken(refreshToken)
// Retry request original // Retry request original
} }
} }
); )
``` ```
### Roles e Permissões (RLS) ### Roles e Permissões (RLS)
#### 👑 Admin/Gestor: #### 👑 Admin/Gestor:
- ✅ **Acesso completo a todos os recursos** - ✅ **Acesso completo a todos os recursos**
- ✅ Criar/editar/deletar usuários, médicos, pacientes - ✅ Criar/editar/deletar usuários, médicos, pacientes
- ✅ Visualizar todos os agendamentos e prontuários - ✅ Visualizar todos os agendamentos e prontuários
#### 👨‍⚕️ Médicos: #### 👨‍⚕️ Médicos:
- ✅ Veem **todos os pacientes** - ✅ Veem **todos os pacientes**
- ✅ Veem apenas **seus próprios laudos** (filtro: `created_by = médico`) - ✅ Veem apenas **seus próprios laudos** (filtro: `created_by = médico`)
- ✅ Veem apenas **seus próprios agendamentos** (filtro: `doctor_id = médico`) - ✅ Veem apenas **seus próprios agendamentos** (filtro: `doctor_id = médico`)
- ✅ Editam apenas **seus próprios laudos e agendamentos** - ✅ Editam apenas **seus próprios laudos e agendamentos**
#### 👤 Pacientes: #### 👤 Pacientes:
- ✅ Veem apenas **seus próprios dados** - ✅ Veem apenas **seus próprios dados**
- ✅ Veem apenas **seus próprios laudos** (filtro: `patient_id = paciente`) - ✅ Veem apenas **seus próprios laudos** (filtro: `patient_id = paciente`)
- ✅ Veem apenas **seus próprios agendamentos** - ✅ Veem apenas **seus próprios agendamentos**
- ✅ Podem agendar consultas - ✅ Podem agendar consultas
#### 👩‍💼 Secretárias: #### 👩‍💼 Secretárias:
- ✅ Veem **todos os pacientes** - ✅ Veem **todos os pacientes**
- ✅ Veem **todos os agendamentos** - ✅ Veem **todos os agendamentos**
- ✅ Veem **todos os laudos** - ✅ Veem **todos os laudos**
@ -150,96 +145,96 @@ src/services/
```typescript ```typescript
// Login // Login
await authService.login({ email, password }); await authService.login({ email, password })
// Retorna: { access_token, refresh_token, user } // Retorna: { access_token, refresh_token, user }
// Recuperação de senha // Recuperação de senha
await authService.requestPasswordReset(email); await authService.requestPasswordReset(email)
// Envia email com link de reset // Envia email com link de reset
// Atualizar senha // Atualizar senha
await authService.updatePassword(accessToken, newPassword); await authService.updatePassword(accessToken, newPassword)
// Usado na página /reset-password // Usado na página /reset-password
// Refresh token // Refresh token
await authService.refreshToken(refreshToken); await authService.refreshToken(refreshToken)
``` ```
#### 👤 Usuários (userService) #### 👤 Usuários (userService)
```typescript ```typescript
// Buscar informações do usuário autenticado (com roles) // 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'] } // Retorna: { id, email, full_name, roles: ['medico', 'admin'] }
// Criar usuário com role // Criar usuário com role
await userService.createUser({ await userService.createUser({
email: "user@example.com", email: 'user@example.com',
full_name: "Nome Completo", full_name: 'Nome Completo',
role: "medico", // ou 'admin', 'paciente', 'secretaria' role: 'medico' // ou 'admin', 'paciente', 'secretaria'
}); })
// Deletar usuário // Deletar usuário
await userService.deleteUser(userId); await userService.deleteUser(userId)
``` ```
#### 🏥 Pacientes (patientService) #### 🏥 Pacientes (patientService)
```typescript ```typescript
// Listar pacientes // Listar pacientes
const patients = await patientService.list(); const patients = await patientService.list()
// Buscar por ID // Buscar por ID
const patient = await patientService.getById(id); const patient = await patientService.getById(id)
// Criar paciente // Criar paciente
await patientService.create({ await patientService.create({
email: "paciente@example.com", email: 'paciente@example.com',
full_name: "Nome Paciente", full_name: 'Nome Paciente',
cpf: "12345678900", cpf: '12345678900',
phone_mobile: "11999999999", phone_mobile: '11999999999'
}); })
// Atualizar paciente // Atualizar paciente
await patientService.update(id, { phone_mobile: "11888888888" }); await patientService.update(id, { phone_mobile: '11888888888' })
// Deletar paciente // Deletar paciente
await patientService.delete(id); await patientService.delete(id)
``` ```
#### 👨‍⚕️ Médicos (doctorService) #### 👨‍⚕️ Médicos (doctorService)
```typescript ```typescript
// Listar médicos // Listar médicos
const doctors = await doctorService.list(); const doctors = await doctorService.list()
// Buscar por ID // Buscar por ID
const doctor = await doctorService.getById(id); const doctor = await doctorService.getById(id)
// Buscar disponibilidade // Buscar disponibilidade
const slots = await doctorService.getAvailableSlots(doctorId, date); const slots = await doctorService.getAvailableSlots(doctorId, date)
``` ```
#### 📅 Agendamentos (appointmentService) #### 📅 Agendamentos (appointmentService)
```typescript ```typescript
// Listar agendamentos (filtrado por role automaticamente) // Listar agendamentos (filtrado por role automaticamente)
const appointments = await appointmentService.list(); const appointments = await appointmentService.list()
// Criar agendamento // Criar agendamento
await appointmentService.create({ await appointmentService.create({
patient_id: "uuid-paciente", patient_id: 'uuid-paciente',
doctor_id: "uuid-medico", doctor_id: 'uuid-medico',
scheduled_at: "2025-10-25T10:00:00", scheduled_at: '2025-10-25T10:00:00',
reason: "Consulta de rotina", reason: 'Consulta de rotina'
}); })
// Atualizar status // Atualizar status
await appointmentService.updateStatus(id, "confirmed"); await appointmentService.updateStatus(id, 'confirmed')
// Status: requested, confirmed, completed, cancelled // Status: requested, confirmed, completed, cancelled
// Cancelar // Cancelar
await appointmentService.cancel(id, "Motivo do cancelamento"); await appointmentService.cancel(id, 'Motivo do cancelamento')
``` ```
--- ---
@ -258,9 +253,9 @@ export const API_CONFIG = {
STORAGE_KEYS: { STORAGE_KEYS: {
ACCESS_TOKEN: "mediconnect_access_token", ACCESS_TOKEN: "mediconnect_access_token",
REFRESH_TOKEN: "mediconnect_refresh_token", REFRESH_TOKEN: "mediconnect_refresh_token",
USER: "mediconnect_user", USER: "mediconnect_user"
}, }
}; }
``` ```
--- ---
@ -298,7 +293,7 @@ npx wrangler pages deploy dist --project-name=mediconnect --commit-dirty=true
## 1. Variáveis de Ambiente (`.env` / `.env.local`) ## 1. Variáveis de Ambiente (`.env` / `.env.local`)
| Variável | Obrigatória | Descrição | | Variável | Obrigatória | Descrição |
| ------------------------ | ----------- | --------------------------------------------------------------- | | ------------------------ | ---------------- | --------------------------------------------------------------- |
| `VITE_SUPABASE_URL` | Sim | URL base do projeto Supabase (`https://<ref>.supabase.co`) | | `VITE_SUPABASE_URL` | Sim | URL base do projeto Supabase (`https://<ref>.supabase.co`) |
| `VITE_SUPABASE_ANON_KEY` | Sim | Chave pública (anon) usada para Auth password grant e PostgREST | | `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`) | | `VITE_APP_ENV` | Não | Identifica ambiente (ex: `dev`, `staging`, `prod`) |
@ -319,37 +314,38 @@ Boas práticas:
```typescript ```typescript
// pages/LoginMedico.tsx // pages/LoginMedico.tsx
const handleLogin = async (e: FormEvent) => { const handleLogin = async (e: FormEvent) => {
e.preventDefault(); e.preventDefault()
try { try {
// 1. Autenticar com Supabase // 1. Autenticar com Supabase
const loginResponse = await authService.login({ email, password }); const loginResponse = await authService.login({ email, password })
// 2. Salvar tokens // 2. Salvar tokens
localStorage.setItem("access_token", loginResponse.access_token); localStorage.setItem('access_token', loginResponse.access_token)
localStorage.setItem("refresh_token", loginResponse.refresh_token); localStorage.setItem('refresh_token', loginResponse.refresh_token)
// 3. Buscar informações do usuário com roles // 3. Buscar informações do usuário com roles
const userInfo = await userService.getUserInfo(); const userInfo = await userService.getUserInfo()
const roles = userInfo.roles || []; const roles = userInfo.roles || []
// 4. Validar permissões // 4. Validar permissões
const isAdmin = roles.includes("admin"); const isAdmin = roles.includes('admin')
const isGestor = roles.includes("gestor"); const isGestor = roles.includes('gestor')
const isMedico = roles.includes("medico"); const isMedico = roles.includes('medico')
if (!isAdmin && !isGestor && !isMedico) { if (!isAdmin && !isGestor && !isMedico) {
toast.error("Você não tem permissão para acessar esta área"); toast.error("Você não tem permissão para acessar esta área")
await authService.logout(); await authService.logout()
return; return
} }
// 5. Redirecionar para o painel // 5. Redirecionar para o painel
navigate("/painel-medico"); navigate('/painel-medico')
} catch (error) { } catch (error) {
toast.error("Email ou senha incorretos"); toast.error("Email ou senha incorretos")
} }
}; }
``` ```
--- ---
@ -371,35 +367,35 @@ const handleLogin = async (e: FormEvent) => {
// Solicitar recuperação (páginas de login) // Solicitar recuperação (páginas de login)
const handlePasswordReset = async () => { const handlePasswordReset = async () => {
try { try {
await authService.requestPasswordReset(email); await authService.requestPasswordReset(email)
toast.success("Link de recuperação enviado para seu email"); toast.success("Link de recuperação enviado para seu email")
} catch (error) { } 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) // Página de reset (ResetPassword.tsx)
useEffect(() => { useEffect(() => {
// Extrair token do URL // Extrair token do URL
const hash = window.location.hash; const hash = window.location.hash
const params = new URLSearchParams(hash.substring(1)); const params = new URLSearchParams(hash.substring(1))
const token = params.get("access_token"); const token = params.get('access_token')
if (token) { if (token) {
setAccessToken(token); setAccessToken(token)
setIsLoading(false); setIsLoading(false)
} }
}, []); }, [])
const handleSubmit = async (e: FormEvent) => { const handleSubmit = async (e: FormEvent) => {
try { try {
await authService.updatePassword(accessToken, newPassword); await authService.updatePassword(accessToken, newPassword)
toast.success("Senha atualizada com sucesso!"); toast.success("Senha atualizada com sucesso!")
navigate("/login-paciente"); navigate('/login-paciente')
} catch (error) { } catch (error) {
toast.error("Erro ao atualizar senha"); toast.error("Erro ao atualizar senha")
} }
}; }
``` ```
--- ---
@ -409,7 +405,6 @@ const handleSubmit = async (e: FormEvent) => {
### Tabelas Principais ### Tabelas Principais
#### `profiles` #### `profiles`
```sql ```sql
- id (uuid, PK) - id (uuid, PK)
- email (text, unique) - email (text, unique)
@ -420,7 +415,6 @@ const handleSubmit = async (e: FormEvent) => {
``` ```
#### `patients` #### `patients`
```sql ```sql
- id (uuid, PK, FK -> profiles) - id (uuid, PK, FK -> profiles)
- email (text, unique) - email (text, unique)
@ -433,7 +427,6 @@ const handleSubmit = async (e: FormEvent) => {
``` ```
#### `doctors` #### `doctors`
```sql ```sql
- id (uuid, PK, FK -> profiles) - id (uuid, PK, FK -> profiles)
- email (text, unique) - email (text, unique)
@ -445,7 +438,6 @@ const handleSubmit = async (e: FormEvent) => {
``` ```
#### `appointments` #### `appointments`
```sql ```sql
- id (uuid, PK) - id (uuid, PK)
- patient_id (uuid, FK -> patients) - patient_id (uuid, FK -> patients)
@ -458,7 +450,6 @@ const handleSubmit = async (e: FormEvent) => {
``` ```
#### `user_roles` #### `user_roles`
```sql ```sql
- user_id (uuid, FK -> profiles) - user_id (uuid, FK -> profiles)
- role (text: admin, gestor, medico, secretaria, paciente) - role (text: admin, gestor, medico, secretaria, paciente)
@ -472,13 +463,12 @@ const handleSubmit = async (e: FormEvent) => {
O sistema usa uma estratégia híbrida para máxima segurança: O sistema usa uma estratégia híbrida para máxima segurança:
| Tipo | Local | Expiração Natural | | Tipo | Local | Expiração Natural |
| ------------- | ------------ | --------------------------------- | | -------------- | ----------------- | ------------------------------------ |
| Access Token | localStorage | 1 hora (renovado automaticamente) | | Access Token | localStorage | 1 hora (renovado automaticamente) |
| Refresh Token | localStorage | 30 dias (ou revogação backend) | | Refresh Token | localStorage | 30 dias (ou revogação backend) |
| User Snapshot | localStorage | Limpo em logout | | User Snapshot | localStorage | Limpo em logout |
**Segurança:** **Segurança:**
- Tokens são limpos automaticamente no logout - Tokens são limpos automaticamente no logout
- Refresh automático quando access_token expira (401) - Refresh automático quando access_token expira (401)
- Interceptors garantem tokens válidos em todas as requisições - Interceptors garantem tokens válidos em todas as requisições
@ -507,7 +497,6 @@ Fluxo de Refresh:
1. Requisição falha com 401. 1. Requisição falha com 401.
2. Wrapper (`http.ts`) obtém refresh do `tokenStore`. 2. Wrapper (`http.ts`) obtém refresh do `tokenStore`.
--- ---
## 7. Scripts Utilitários ## 7. Scripts Utilitários
@ -556,9 +545,9 @@ node search-fernando.cjs
```typescript ```typescript
// Imports // Imports
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react"
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom"
import { serviceImport } from "../services"; import { serviceImport } from "../services"
// Types // Types
interface Props { interface Props {
@ -568,24 +557,28 @@ interface Props {
// Component // Component
const ComponentName: React.FC<Props> = ({ ...props }) => { const ComponentName: React.FC<Props> = ({ ...props }) => {
// Hooks // Hooks
const navigate = useNavigate(); const navigate = useNavigate()
const [state, setState] = useState(); const [state, setState] = useState()
// Effects // Effects
useEffect(() => { useEffect(() => {
// ... // ...
}, []); }, [])
// Handlers // Handlers
const handleAction = async () => { const handleAction = async () => {
// ... // ...
}; }
// Render // Render
return <div>{/* JSX */}</div>; return (
}; <div>
{/* JSX */}
</div>
)
}
export default ComponentName; export default ComponentName
``` ```
--- ---
@ -593,7 +586,6 @@ export default ComponentName;
## 9. Tecnologias Utilizadas ## 9. Tecnologias Utilizadas
### Frontend ### Frontend
- **React** 18.3.1 - Biblioteca UI - **React** 18.3.1 - Biblioteca UI
- **TypeScript** 5.9.3 - Tipagem estática - **TypeScript** 5.9.3 - Tipagem estática
- **Vite** 7.1.10 - Build tool - **Vite** 7.1.10 - Build tool
@ -604,13 +596,11 @@ export default ComponentName;
- **date-fns** 4.1.0 - Manipulação de datas - **date-fns** 4.1.0 - Manipulação de datas
### Backend ### Backend
- **Supabase** - Backend as a Service - **Supabase** - Backend as a Service
- **PostgreSQL** - Banco de dados relacional - **PostgreSQL** - Banco de dados relacional
- **Supabase Auth** - Autenticação JWT - **Supabase Auth** - Autenticação JWT
### Deploy ### Deploy
- **Cloudflare Pages** - Hospedagem frontend - **Cloudflare Pages** - Hospedagem frontend
- **Wrangler** 4.44.0 - CLI Cloudflare - **Wrangler** 4.44.0 - CLI Cloudflare
@ -625,3 +615,4 @@ export default ComponentName;
--- ---
**Desenvolvido com ❤️ pela Squad 18** **Desenvolvido com ❤️ pela Squad 18**

View File

@ -0,0 +1,76 @@
const axios = require("axios");
const ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
const BASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
(async () => {
try {
console.log("🔐 Fazendo login como admin...");
const loginRes = await axios.post(
`${BASE_URL}/auth/v1/token?grant_type=password`,
{
email: "riseup@popcode.com.br",
password: "riseup",
},
{
headers: {
"Content-Type": "application/json",
apikey: ANON_KEY,
},
}
);
console.log("✅ Login admin bem-sucedido!\n");
const token = loginRes.data.access_token;
// Buscar o ID do Fernando no profiles
console.log("🔍 Buscando ID do Fernando...");
const profileRes = await axios.get(
`${BASE_URL}/rest/v1/profiles?email=eq.fernando.pirichowski@souunit.com.br&select=*`,
{
headers: {
apikey: ANON_KEY,
Authorization: `Bearer ${token}`,
},
}
);
if (profileRes.data.length === 0) {
console.log("❌ Fernando não encontrado no profiles");
return;
}
const fernandoId = profileRes.data[0].id;
console.log("✅ Fernando encontrado! ID:", fernandoId);
// Criar entrada na tabela patients
console.log("\n📋 Criando entrada na tabela patients...");
const patientRes = await axios.post(
`${BASE_URL}/rest/v1/patients`,
{
id: fernandoId,
email: "fernando.pirichowski@souunit.com.br",
full_name: "Fernando Pirichowski",
phone_mobile: "51999999999",
cpf: "12345678909", // CPF válido fictício
},
{
headers: {
"Content-Type": "application/json",
apikey: ANON_KEY,
Authorization: `Bearer ${token}`,
Prefer: "return=representation",
},
}
);
console.log("✅ Entrada na tabela patients criada!");
console.log("\n🎉 Usuário Fernando Pirichowski completo!");
console.log("📧 Email: fernando.pirichowski@souunit.com.br");
console.log("🔑 Senha: fernando123");
console.log("\n✨ Agora você pode testar a recuperação de senha!");
} catch (err) {
console.error("❌ Erro:", err.response?.data || err.message);
}
})();

View File

@ -0,0 +1,87 @@
const axios = require("axios");
const ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
const BASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
(async () => {
try {
console.log("🔐 Fazendo login como admin...");
const loginRes = await axios.post(
`${BASE_URL}/auth/v1/token?grant_type=password`,
{
email: "riseup@popcode.com.br",
password: "riseup",
},
{
headers: {
"Content-Type": "application/json",
apikey: ANON_KEY,
},
}
);
console.log("✅ Login admin bem-sucedido!\n");
const token = loginRes.data.access_token;
console.log("👤 Criando usuário Fernando Pirichowski...");
// Criar usuário via signup
const signupRes = await axios.post(
`${BASE_URL}/auth/v1/signup`,
{
email: "fernando.pirichowski@souunit.com.br",
password: "fernando123", // Senha temporária
options: {
data: {
full_name: "Fernando Pirichowski",
phone: "51999999999",
},
},
},
{
headers: {
"Content-Type": "application/json",
apikey: ANON_KEY,
},
}
);
console.log("✅ Usuário criado com sucesso!");
console.log("📧 Email:", signupRes.data.user.email);
console.log("🆔 ID:", signupRes.data.user.id);
console.log("🔑 Senha temporária: fernando123\n");
// Criar entrada na tabela patients
console.log("📋 Criando entrada na tabela patients...");
const patientRes = await axios.post(
`${BASE_URL}/rest/v1/patients`,
{
id: signupRes.data.user.id,
email: "fernando.pirichowski@souunit.com.br",
full_name: "Fernando Pirichowski",
phone_mobile: "51999999999",
cpf: "12345678909", // CPF válido fictício
},
{
headers: {
"Content-Type": "application/json",
apikey: ANON_KEY,
Authorization: `Bearer ${token}`,
Prefer: "return=representation",
},
}
);
console.log("✅ Entrada na tabela patients criada!");
console.log("\n🎉 Usuário Fernando Pirichowski criado com sucesso!");
console.log("📧 Email: fernando.pirichowski@souunit.com.br");
console.log("🔑 Senha: fernando123");
console.log("\n💡 Agora você pode testar a recuperação de senha!");
} catch (err) {
console.error("❌ Erro:", err.response?.data || err.message);
if (err.response?.data?.msg) {
console.error("Mensagem:", err.response.data.msg);
}
}
})();

View File

@ -0,0 +1,86 @@
const axios = require('axios');
const ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ';
const BASE_URL = 'https://yuanqfswhberkoevtmfr.supabase.co';
(async () => {
try {
console.log('🔐 Fazendo login como admin...');
const loginRes = await axios.post(`${BASE_URL}/auth/v1/token?grant_type=password`, {
email: 'riseup@popcode.com.br',
password: 'riseup'
}, {
headers: {
'Content-Type': 'application/json',
'apikey': ANON_KEY
}
});
console.log('✅ Login admin bem-sucedido!\n');
const token = loginRes.data.access_token;
console.log('🔍 Buscando usuário fernando na tabela profiles...');
const profilesRes = await axios.get(`${BASE_URL}/rest/v1/profiles?select=*`, {
headers: {
'apikey': ANON_KEY,
'Authorization': `Bearer ${token}`
}
});
console.log(`📊 Total de profiles: ${profilesRes.data.length}\n`);
const fernandoProfile = profilesRes.data.find(u =>
u.email && (
u.email.toLowerCase().includes('fernando') ||
u.full_name?.toLowerCase().includes('fernando')
)
);
if (fernandoProfile) {
console.log('✅ Fernando encontrado na tabela profiles:');
console.log(JSON.stringify(fernandoProfile, null, 2));
} else {
console.log('❌ Fernando NÃO encontrado na tabela profiles\n');
}
// Buscar nos pacientes também
console.log('\n🔍 Buscando fernando na tabela patients...');
const patientsRes = await axios.get(`${BASE_URL}/rest/v1/patients?select=*`, {
headers: {
'apikey': ANON_KEY,
'Authorization': `Bearer ${token}`
}
});
console.log(`📊 Total de patients: ${patientsRes.data.length}\n`);
const fernandoPatient = patientsRes.data.find(p =>
p.email && (
p.email.toLowerCase().includes('fernando') ||
p.full_name?.toLowerCase().includes('fernando')
)
);
if (fernandoPatient) {
console.log('✅ Fernando encontrado na tabela patients:');
console.log(JSON.stringify(fernandoPatient, null, 2));
} else {
console.log('❌ Fernando NÃO encontrado na tabela patients\n');
}
// Listar alguns emails para referência
if (!fernandoProfile && !fernandoPatient) {
console.log('\n📧 Alguns emails cadastrados nos profiles:');
profilesRes.data.slice(0, 10).forEach((u, i) => {
if (u.email) console.log(` ${i+1}. ${u.email} - ${u.full_name || 'sem nome'}`);
});
}
} catch (err) {
console.error('❌ Erro:', err.response?.data || err.message);
if (err.response) {
console.error('Status:', err.response.status);
console.error('Headers:', err.response.headers);
}
}
})();

View File

@ -1,54 +0,0 @@
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;

View File

@ -21,32 +21,7 @@ export default function AuthCallback() {
useEffect(() => { useEffect(() => {
const handleCallback = async () => { const handleCallback = async () => {
try { try {
console.log("[AuthCallback] Iniciando processamento"); console.log("[AuthCallback] Iniciando processamento do magic link");
// 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 // Supabase automaticamente processa os query params
const { const {

View File

@ -6,7 +6,6 @@ import { MetricCard } from "../components/MetricCard";
import { HeroBanner } from "../components/HeroBanner"; import { HeroBanner } from "../components/HeroBanner";
import { i18n } from "../i18n"; import { i18n } from "../i18n";
import { useAuth } from "../hooks/useAuth"; import { useAuth } from "../hooks/useAuth";
import RecoveryRedirect from "../components/auth/RecoveryRedirect";
const Home: React.FC = () => { const Home: React.FC = () => {
const [stats, setStats] = useState({ const [stats, setStats] = useState({
@ -98,9 +97,6 @@ const Home: React.FC = () => {
return ( return (
<div className="space-y-8" id="main-content"> <div className="space-y-8" id="main-content">
{/* Componente invisível que detecta tokens de recuperação e redireciona */}
<RecoveryRedirect />
{/* Hero Section com Background Rotativo */} {/* Hero Section com Background Rotativo */}
<HeroBanner /> <HeroBanner />

View File

@ -14,50 +14,37 @@ const ResetPassword: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
// Extrair access_token do hash da URL (#access_token=...) // Extrair access_token do hash da URL
// 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; const hash = window.location.hash;
console.log("[ResetPassword] Hash completo:", hash); console.log("[ResetPassword] Hash completo:", hash);
if (hash) { if (hash) {
const hashParams = new URLSearchParams(hash.substring(1)); const params = new URLSearchParams(hash.substring(1));
token = hashParams.get("access_token"); const token = params.get("access_token");
type = hashParams.get("type"); const type = params.get("type");
console.log("[ResetPassword] Token do hash:", token ? token.substring(0, 20) + "..." : "null"); console.log(
console.log("[ResetPassword] Type do hash:", type); "[ResetPassword] Token extraído:",
} token ? token.substring(0, 20) + "..." : "null"
);
// Se não encontrou no hash, tenta no query string console.log("[ResetPassword] Type:", type);
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) { if (token) {
setAccessToken(token); setAccessToken(token);
console.log("[ResetPassword] ✅ Token de recuperação detectado e armazenado"); console.log(
console.log("[ResetPassword] Type:", type); "[ResetPassword] ✅ Token de recuperação detectado e armazenado"
);
} else { } else {
console.error("[ResetPassword] ❌ Token não encontrado no hash nem no query string"); console.error("[ResetPassword] ❌ Token não encontrado no hash");
console.log("[ResetPassword] URL completa:", window.location.href);
toast.error("Link de recuperação inválido ou expirado"); toast.error("Link de recuperação inválido ou expirado");
setTimeout(() => navigate("/"), 3000); setTimeout(() => navigate("/"), 3000);
} }
} else {
console.error("[ResetPassword] ❌ Nenhum hash encontrado na URL");
console.log("[ResetPassword] URL completa:", window.location.href);
toast.error("Link de recuperação inválido");
setTimeout(() => navigate("/"), 3000);
}
}, [navigate]); }, [navigate]);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
@ -106,26 +93,10 @@ const ResetPassword: React.FC = () => {
} catch (error: unknown) { } catch (error: unknown) {
console.error("[ResetPassword] Erro ao atualizar senha:", error); console.error("[ResetPassword] Erro ao atualizar senha:", error);
const err = error as { const err = error as {
response?: { response?: { data?: { error_description?: string; message?: string } };
data?: {
error_description?: string;
message?: string;
msg?: string;
error_code?: string;
}
};
message?: 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 = const errorMessage =
err?.response?.data?.msg ||
err?.response?.data?.error_description || err?.response?.data?.error_description ||
err?.response?.data?.message || err?.response?.data?.message ||
err?.message || err?.message ||

View File

@ -154,31 +154,20 @@ class AuthService {
/** /**
* Solicita reset de senha via email (público) * Solicita reset de senha via email (público)
* POST /auth/v1/recover * POST /auth/v1/recover
*
* Envia redirectTo dentro de options para controlar para onde o Supabase redireciona
*/ */
async requestPasswordReset( async requestPasswordReset(
email: string email: string,
redirectUrl?: string
): Promise<{ success: boolean; message: string }> { ): Promise<{ success: boolean; message: string }> {
try { 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( await axios.post(
`${API_CONFIG.AUTH_URL}/recover`, `${API_CONFIG.AUTH_URL}/recover`,
payload, {
email,
options: {
redirectTo: redirectUrl || `${API_CONFIG.APP_URL}/reset-password`,
},
},
{ {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -187,15 +176,13 @@ class AuthService {
} }
); );
console.log("[authService.requestPasswordReset] ✅ Email de recuperação enviado");
return { return {
success: true, 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: any) { } catch (error) {
console.error("[authService.requestPasswordReset] ❌ Erro:", error); console.error("Erro ao solicitar reset de senha:", error);
console.error("[authService.requestPasswordReset] Response:", error.response?.data);
throw error; throw error;
} }
} }
@ -209,12 +196,7 @@ class AuthService {
newPassword: string newPassword: string
): Promise<{ success: boolean; message: string }> { ): Promise<{ success: boolean; message: string }> {
try { try {
console.log("[authService.updatePassword] Iniciando atualização de senha"); await axios.put(
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`, `${API_CONFIG.AUTH_URL}/user`,
{ {
password: newPassword, password: newPassword,
@ -228,35 +210,12 @@ class AuthService {
} }
); );
console.log("[authService.updatePassword] ✅ Senha atualizada com sucesso");
console.log("[authService.updatePassword] Response status:", response.status);
return { return {
success: true, success: true,
message: "Senha atualizada com sucesso", message: "Senha atualizada com sucesso",
}; };
} catch (error: any) { } catch (error) {
console.error("[authService.updatePassword] ❌ Erro ao atualizar senha:", error); console.error("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; throw error;
} }
} }

View File

@ -0,0 +1,54 @@
const axios = require("axios");
const ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
const BASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
(async () => {
try {
console.log("\n=== TESTE DE RECUPERAÇÃO COM REDIRECT_TO CORRETO ===\n");
const email = "fernando.pirichowski@souunit.com.br";
const redirectTo = "https://mediconnectbrasil.app/reset-password";
console.log(`📧 Enviando email de recuperação para: ${email}`);
console.log(`🔗 Redirect URL: ${redirectTo}\n`);
const response = await axios.post(
`${BASE_URL}/auth/v1/recover`,
{
email: email,
options: {
redirectTo: redirectTo,
},
},
{
headers: {
"Content-Type": "application/json",
apikey: ANON_KEY,
},
}
);
console.log("✅ Email de recuperação enviado com sucesso!");
console.log("Status:", response.status);
console.log("Response:", JSON.stringify(response.data, null, 2));
console.log("\n📬 Verifique o email:", email);
console.log("🔗 O link DEVE redirecionar para:", redirectTo);
console.log("\n💡 IMPORTANTE: Se ainda vier o link errado, você precisa:");
console.log(" 1. Acessar o painel do Supabase");
console.log(" 2. Ir em Authentication > URL Configuration");
console.log(
' 3. Atualizar o "Site URL" para: https://mediconnectbrasil.app'
);
console.log(
' 4. Adicionar https://mediconnectbrasil.app/* nos "Redirect URLs"'
);
console.log("\n=== TESTE CONCLUÍDO ===\n");
} catch (error) {
console.error("❌ Erro ao enviar email de recuperação:");
console.error(error.response?.data || error.message);
}
})();

158
README.md
View File

@ -61,14 +61,12 @@ O MediConnect é uma plataforma web que conecta **pacientes**, **médicos** e **
### Camadas da Aplicação ### Camadas da Aplicação
1. **Apresentação (UI)** 1. **Apresentação (UI)**
- React 18.3.1 com TypeScript - React 18.3.1 com TypeScript
- React Router para navegação - React Router para navegação
- Tailwind CSS para estilização - Tailwind CSS para estilização
- Lucide React para ícones - Lucide React para ícones
2. **Lógica de Negócio (Services)** 2. **Lógica de Negócio (Services)**
- Services organizados por domínio - Services organizados por domínio
- Axios para requisições HTTP - Axios para requisições HTTP
- Interceptors para autenticação automática - Interceptors para autenticação automática
@ -83,7 +81,6 @@ O MediConnect é uma plataforma web que conecta **pacientes**, **médicos** e **
## 🛠️ Tecnologias ## 🛠️ Tecnologias
### Frontend ### Frontend
- **React** 18.3.1 - Biblioteca UI - **React** 18.3.1 - Biblioteca UI
- **TypeScript** 5.9.3 - Tipagem estática - **TypeScript** 5.9.3 - Tipagem estática
- **Vite** 7.1.10 - Build tool - **Vite** 7.1.10 - Build tool
@ -94,13 +91,11 @@ O MediConnect é uma plataforma web que conecta **pacientes**, **médicos** e **
- **date-fns** 4.1.0 - Manipulação de datas - **date-fns** 4.1.0 - Manipulação de datas
### Backend ### Backend
- **Supabase** - Backend as a Service - **Supabase** - Backend as a Service
- **PostgreSQL** - Banco de dados relacional - **PostgreSQL** - Banco de dados relacional
- **Supabase Auth** - Autenticação JWT - **Supabase Auth** - Autenticação JWT
### Deploy ### Deploy
- **Cloudflare Pages** - Hospedagem frontend - **Cloudflare Pages** - Hospedagem frontend
- **Wrangler** 4.44.0 - CLI Cloudflare - **Wrangler** 4.44.0 - CLI Cloudflare
@ -192,112 +187,106 @@ export const API_CONFIG = {
STORAGE_KEYS: { STORAGE_KEYS: {
ACCESS_TOKEN: "mediconnect_access_token", ACCESS_TOKEN: "mediconnect_access_token",
REFRESH_TOKEN: "mediconnect_refresh_token", REFRESH_TOKEN: "mediconnect_refresh_token",
USER: "mediconnect_user", USER: "mediconnect_user"
}, }
}; }
``` ```
### Serviços Disponíveis ### Serviços Disponíveis
#### 1. **authService** - Autenticação #### 1. **authService** - Autenticação
```typescript ```typescript
// Login // Login
await authService.login({ email, password }); await authService.login({ email, password })
// Registro // Registro
await authService.signup({ email, password, full_name }); await authService.signup({ email, password, full_name })
// Logout // Logout
await authService.logout(); await authService.logout()
// Recuperação de senha // Recuperação de senha
await authService.requestPasswordReset(email); await authService.requestPasswordReset(email)
await authService.updatePassword(accessToken, newPassword); await authService.updatePassword(accessToken, newPassword)
// Refresh token // Refresh token
await authService.refreshToken(refreshToken); await authService.refreshToken(refreshToken)
``` ```
#### 2. **userService** - Usuários #### 2. **userService** - Usuários
```typescript ```typescript
// Buscar informações do usuário autenticado // Buscar informações do usuário autenticado
const userInfo = await userService.getUserInfo(); const userInfo = await userService.getUserInfo()
// Criar usuário com role // Criar usuário com role
await userService.createUser({ email, full_name, role }); await userService.createUser({ email, full_name, role })
// Deletar usuário // Deletar usuário
await userService.deleteUser(userId); await userService.deleteUser(userId)
``` ```
#### 3. **patientService** - Pacientes #### 3. **patientService** - Pacientes
```typescript ```typescript
// Listar pacientes // Listar pacientes
const patients = await patientService.list(); const patients = await patientService.list()
// Buscar por ID // Buscar por ID
const patient = await patientService.getById(id); const patient = await patientService.getById(id)
// Criar paciente // Criar paciente
await patientService.create({ email, full_name, cpf, phone }); await patientService.create({ email, full_name, cpf, phone })
// Atualizar // Atualizar
await patientService.update(id, data); await patientService.update(id, data)
// Registrar paciente (público) // Registrar paciente (público)
await patientService.register({ email, full_name, cpf }); await patientService.register({ email, full_name, cpf })
``` ```
#### 4. **doctorService** - Médicos #### 4. **doctorService** - Médicos
```typescript ```typescript
// Listar médicos // Listar médicos
const doctors = await doctorService.list(); const doctors = await doctorService.list()
// Buscar por ID // Buscar por ID
const doctor = await doctorService.getById(id); const doctor = await doctorService.getById(id)
// Buscar disponibilidade // Buscar disponibilidade
const slots = await doctorService.getAvailableSlots(doctorId, date); const slots = await doctorService.getAvailableSlots(doctorId, date)
``` ```
#### 5. **appointmentService** - Consultas #### 5. **appointmentService** - Consultas
```typescript ```typescript
// Listar consultas // Listar consultas
const appointments = await appointmentService.list(); const appointments = await appointmentService.list()
// Criar consulta // Criar consulta
await appointmentService.create({ await appointmentService.create({
patient_id, patient_id,
doctor_id, doctor_id,
scheduled_at, scheduled_at,
reason, reason
}); })
// Atualizar status // Atualizar status
await appointmentService.updateStatus(id, status); await appointmentService.updateStatus(id, status)
// Cancelar // Cancelar
await appointmentService.cancel(id, reason); await appointmentService.cancel(id, reason)
``` ```
#### 6. **availabilityService** - Disponibilidade #### 6. **availabilityService** - Disponibilidade
```typescript ```typescript
// Gerenciar disponibilidade do médico // Gerenciar disponibilidade do médico
await availabilityService.create({ await availabilityService.create({
doctor_id, doctor_id,
day_of_week, day_of_week,
start_time, start_time,
end_time, end_time
}); })
// Listar disponibilidade // Listar disponibilidade
const slots = await availabilityService.listByDoctor(doctorId); const slots = await availabilityService.listByDoctor(doctorId)
``` ```
--- ---
@ -307,57 +296,54 @@ const slots = await availabilityService.listByDoctor(doctorId);
### Fluxo de Autenticação ### Fluxo de Autenticação
1. **Login** 1. **Login**
```typescript ```typescript
// 1. Usuário envia credenciais // 1. Usuário envia credenciais
const response = await authService.login({ email, password }); const response = await authService.login({ email, password })
// 2. Recebe tokens JWT // 2. Recebe tokens JWT
localStorage.setItem("access_token", response.access_token); localStorage.setItem('access_token', response.access_token)
localStorage.setItem("refresh_token", response.refresh_token); localStorage.setItem('refresh_token', response.refresh_token)
// 3. Busca informações completas // 3. Busca informações completas
const userInfo = await userService.getUserInfo(); const userInfo = await userService.getUserInfo()
// 4. Valida roles // 4. Valida roles
if (userInfo.roles.includes("medico")) { if (userInfo.roles.includes('medico')) {
navigate("/painel-medico"); navigate('/painel-medico')
} }
``` ```
2. **Interceptor Automático** 2. **Interceptor Automático**
```typescript ```typescript
// Todo request adiciona o token automaticamente // Todo request adiciona o token automaticamente
axios.interceptors.request.use((config) => { axios.interceptors.request.use(config => {
const token = localStorage.getItem("access_token"); const token = localStorage.getItem('access_token')
if (token) { if (token) {
config.headers.Authorization = `Bearer ${token}`; config.headers.Authorization = `Bearer ${token}`
} }
return config; return config
}); })
``` ```
3. **Refresh Token Automático** 3. **Refresh Token Automático**
```typescript ```typescript
axios.interceptors.response.use( axios.interceptors.response.use(
(response) => response, response => response,
async (error) => { async error => {
if (error.response?.status === 401) { if (error.response?.status === 401) {
// Token expirado, tenta refresh // Token expirado, tenta refresh
const refreshToken = localStorage.getItem("refresh_token"); const refreshToken = localStorage.getItem('refresh_token')
const newTokens = await authService.refreshToken(refreshToken); const newTokens = await authService.refreshToken(refreshToken)
// Retry request original // Retry request original
} }
} }
); )
``` ```
### Roles e Permissões ### Roles e Permissões
| Role | Acesso | | Role | Acesso |
| -------------- | ------------------------------------------- | |------|--------|
| **admin** | Acesso total ao sistema | | **admin** | Acesso total ao sistema |
| **gestor** | Gestão de médicos, secretárias e relatórios | | **gestor** | Gestão de médicos, secretárias e relatórios |
| **medico** | Painel médico, consultas, prontuários | | **medico** | Painel médico, consultas, prontuários |
@ -365,7 +351,6 @@ axios.interceptors.response.use(
| **paciente** | Agendamento, visualização de consultas | | **paciente** | Agendamento, visualização de consultas |
**Hierarquia de Roles:** **Hierarquia de Roles:**
``` ```
admin > gestor > medico/secretaria > paciente admin > gestor > medico/secretaria > paciente
``` ```
@ -412,26 +397,26 @@ sequenceDiagram
// LoginMedico.tsx // LoginMedico.tsx
const handleLogin = async () => { const handleLogin = async () => {
// 1. Login // 1. Login
const loginResponse = await authService.login({ email, password }); const loginResponse = await authService.login({ email, password })
// 2. Buscar roles // 2. Buscar roles
const userInfo = await userService.getUserInfo(); const userInfo = await userService.getUserInfo()
const roles = userInfo.roles || []; const roles = userInfo.roles || []
// 3. Validar permissão // 3. Validar permissão
const isAdmin = roles.includes("admin"); const isAdmin = roles.includes('admin')
const isGestor = roles.includes("gestor"); const isGestor = roles.includes('gestor')
const isMedico = roles.includes("medico"); const isMedico = roles.includes('medico')
if (!isAdmin && !isGestor && !isMedico) { if (!isAdmin && !isGestor && !isMedico) {
toast.error("Você não tem permissão para acessar esta área"); toast.error("Você não tem permissão para acessar esta área")
await authService.logout(); await authService.logout()
return; return
} }
// 4. Redirecionar // 4. Redirecionar
navigate("/painel-medico"); navigate('/painel-medico')
}; }
``` ```
--- ---
@ -537,7 +522,6 @@ VITE_APP_URL=http://localhost:5173
### Tabelas Principais ### Tabelas Principais
#### `profiles` #### `profiles`
```sql ```sql
- id (uuid, PK) - id (uuid, PK)
- email (text, unique) - email (text, unique)
@ -548,7 +532,6 @@ VITE_APP_URL=http://localhost:5173
``` ```
#### `patients` #### `patients`
```sql ```sql
- id (uuid, PK, FK -> profiles) - id (uuid, PK, FK -> profiles)
- email (text, unique) - email (text, unique)
@ -561,7 +544,6 @@ VITE_APP_URL=http://localhost:5173
``` ```
#### `doctors` #### `doctors`
```sql ```sql
- id (uuid, PK, FK -> profiles) - id (uuid, PK, FK -> profiles)
- email (text, unique) - email (text, unique)
@ -573,7 +555,6 @@ VITE_APP_URL=http://localhost:5173
``` ```
#### `appointments` #### `appointments`
```sql ```sql
- id (uuid, PK) - id (uuid, PK)
- patient_id (uuid, FK -> patients) - patient_id (uuid, FK -> patients)
@ -586,7 +567,6 @@ VITE_APP_URL=http://localhost:5173
``` ```
#### `user_roles` #### `user_roles`
```sql ```sql
- user_id (uuid, FK -> profiles) - user_id (uuid, FK -> profiles)
- role (text: admin, gestor, medico, secretaria, paciente) - role (text: admin, gestor, medico, secretaria, paciente)
@ -641,9 +621,9 @@ node search-fernando.cjs
```typescript ```typescript
// Imports // Imports
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react"
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom"
import { serviceImport } from "../services"; import { serviceImport } from "../services"
// Types // Types
interface Props { interface Props {
@ -653,24 +633,28 @@ interface Props {
// Component // Component
const ComponentName: React.FC<Props> = ({ ...props }) => { const ComponentName: React.FC<Props> = ({ ...props }) => {
// Hooks // Hooks
const navigate = useNavigate(); const navigate = useNavigate()
const [state, setState] = useState(); const [state, setState] = useState()
// Effects // Effects
useEffect(() => { useEffect(() => {
// ... // ...
}, []); }, [])
// Handlers // Handlers
const handleAction = async () => { const handleAction = async () => {
// ... // ...
}; }
// Render // Render
return <div>{/* JSX */}</div>; return (
}; <div>
{/* JSX */}
</div>
)
}
export default ComponentName; export default ComponentName
``` ```
--- ---
@ -686,7 +670,6 @@ export default ComponentName;
## 📝 Changelog ## 📝 Changelog
### v2.0.0 (Outubro 2024) ### v2.0.0 (Outubro 2024)
- ✅ Migração completa de Netlify Functions para Supabase - ✅ Migração completa de Netlify Functions para Supabase
- ✅ Implementação de recuperação de senha - ✅ Implementação de recuperação de senha
- ✅ Deploy no Cloudflare Pages - ✅ Deploy no Cloudflare Pages
@ -696,7 +679,6 @@ export default ComponentName;
- ✅ Interface responsiva e dark mode - ✅ Interface responsiva e dark mode
### v1.0.0 (Setembro 2024) ### v1.0.0 (Setembro 2024)
- ✅ Lançamento inicial - ✅ Lançamento inicial
- ✅ Login de pacientes, médicos e secretárias - ✅ Login de pacientes, médicos e secretárias
- ✅ Agendamento de consultas - ✅ Agendamento de consultas