Compare commits

...

2 Commits

11 changed files with 413 additions and 536 deletions

View File

@ -58,6 +58,7 @@ 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)
@ -65,47 +66,51 @@ 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**
@ -145,96 +150,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");
``` ```
--- ---
@ -253,9 +258,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",
} },
} };
``` ```
--- ---
@ -293,7 +298,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`) |
@ -314,38 +319,37 @@ 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");
} }
} };
``` ```
--- ---
@ -367,35 +371,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");
} }
} };
``` ```
--- ---
@ -405,6 +409,7 @@ 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)
@ -415,6 +420,7 @@ 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)
@ -427,6 +433,7 @@ 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)
@ -438,6 +445,7 @@ 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)
@ -450,6 +458,7 @@ 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)
@ -463,12 +472,13 @@ 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
@ -497,6 +507,7 @@ 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
@ -545,9 +556,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 {
@ -557,28 +568,24 @@ 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 ( return <div>{/* JSX */}</div>;
<div> };
{/* JSX */}
</div>
)
}
export default ComponentName export default ComponentName;
``` ```
--- ---
@ -586,6 +593,7 @@ 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
@ -596,11 +604,13 @@ 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
@ -615,4 +625,3 @@ export default ComponentName
--- ---
**Desenvolvido com ❤️ pela Squad 18** **Desenvolvido com ❤️ pela Squad 18**

View File

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

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

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

@ -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;

View File

@ -21,7 +21,32 @@ export default function AuthCallback() {
useEffect(() => { useEffect(() => {
const handleCallback = async () => { const handleCallback = async () => {
try { 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 // Supabase automaticamente processa os query params
const { const {

View File

@ -6,6 +6,7 @@ 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({
@ -97,6 +98,9 @@ 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,35 +14,48 @@ const ResetPassword: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { 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; const hash = window.location.hash;
console.log("[ResetPassword] Hash completo:", hash); console.log("[ResetPassword] Hash completo:", hash);
if (hash) { if (hash) {
const params = new URLSearchParams(hash.substring(1)); const hashParams = new URLSearchParams(hash.substring(1));
const token = params.get("access_token"); token = hashParams.get("access_token");
const type = params.get("type"); type = hashParams.get("type");
console.log( console.log("[ResetPassword] Token do hash:", token ? token.substring(0, 20) + "..." : "null");
"[ResetPassword] Token extraído:", console.log("[ResetPassword] Type do hash:", type);
token ? token.substring(0, 20) + "..." : "null" }
);
console.log("[ResetPassword] Type:", type); // 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) { if (token) {
setAccessToken(token); setAccessToken(token);
console.log( console.log("[ResetPassword] ✅ Token de recuperação detectado e armazenado");
"[ResetPassword] ✅ Token de recuperação detectado e armazenado" console.log("[ResetPassword] Type:", type);
);
} else { } else {
console.error("[ResetPassword] ❌ Token não encontrado no hash"); console.error("[ResetPassword] ❌ Token não encontrado no hash nem no query string");
toast.error("Link de recuperação inválido ou expirado");
setTimeout(() => navigate("/"), 3000);
}
} else {
console.error("[ResetPassword] ❌ Nenhum hash encontrado na URL");
console.log("[ResetPassword] URL completa:", window.location.href); 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); setTimeout(() => navigate("/"), 3000);
} }
}, [navigate]); }, [navigate]);
@ -93,10 +106,26 @@ 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?: { data?: { error_description?: string; message?: string } }; response?: {
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,20 +154,31 @@ 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 {
await axios.post( const redirectUrl = `${API_CONFIG.APP_URL}/reset-password`;
`${API_CONFIG.AUTH_URL}/recover`,
{ 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, email,
options: { options: {
redirectTo: redirectUrl || `${API_CONFIG.APP_URL}/reset-password`, redirectTo: redirectUrl,
},
}, },
};
console.log("[authService.requestPasswordReset] Payload:", JSON.stringify(payload, null, 2));
await axios.post(
`${API_CONFIG.AUTH_URL}/recover`,
payload,
{ {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -176,13 +187,15 @@ class AuthService {
} }
); );
console.log("[authService.requestPasswordReset] ✅ Email de recuperação enviado");
return { return {
success: true, success: true,
message: message: "Email de recuperação de senha enviado com sucesso. Verifique sua caixa de entrada.",
"Email de recuperação de senha enviado com sucesso. Verifique sua caixa de entrada.",
}; };
} catch (error) { } catch (error: any) {
console.error("Erro ao solicitar reset de senha:", error); console.error("[authService.requestPasswordReset] ❌ Erro:", error);
console.error("[authService.requestPasswordReset] Response:", error.response?.data);
throw error; throw error;
} }
} }
@ -196,7 +209,12 @@ class AuthService {
newPassword: string newPassword: string
): Promise<{ success: boolean; message: string }> { ): Promise<{ success: boolean; message: string }> {
try { 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`, `${API_CONFIG.AUTH_URL}/user`,
{ {
password: newPassword, 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 { return {
success: true, success: true,
message: "Senha atualizada com sucesso", message: "Senha atualizada com sucesso",
}; };
} catch (error) { } catch (error: any) {
console.error("Erro ao atualizar senha:", error); 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; throw error;
} }
} }

View File

@ -1,54 +0,0 @@
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,12 +61,14 @@ 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
@ -81,6 +83,7 @@ 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
@ -91,11 +94,13 @@ 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
@ -187,106 +192,112 @@ 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);
``` ```
--- ---
@ -296,54 +307,57 @@ 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 |
@ -351,6 +365,7 @@ 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
``` ```
@ -397,26 +412,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");
} };
``` ```
--- ---
@ -522,6 +537,7 @@ 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)
@ -532,6 +548,7 @@ 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)
@ -544,6 +561,7 @@ 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)
@ -555,6 +573,7 @@ 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)
@ -567,6 +586,7 @@ 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)
@ -621,9 +641,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 {
@ -633,28 +653,24 @@ 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 ( return <div>{/* JSX */}</div>;
<div> };
{/* JSX */}
</div>
)
}
export default ComponentName export default ComponentName;
``` ```
--- ---
@ -670,6 +686,7 @@ 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
@ -679,6 +696,7 @@ 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