Compare commits
2 Commits
f479dcde7d
...
0e27dbf1ff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e27dbf1ff | ||
|
|
a4851de2e0 |
@ -58,6 +58,7 @@ npx wrangler pages deploy dist --project-name=mediconnect --branch=production
|
||||
### Autenticação JWT com Supabase
|
||||
|
||||
O sistema usa **Supabase Auth** com tokens JWT. Todo login retorna:
|
||||
|
||||
- `access_token` (JWT, expira em 1 hora)
|
||||
- `refresh_token` (para renovação automática)
|
||||
|
||||
@ -65,47 +66,51 @@ O sistema usa **Supabase Auth** com tokens JWT. Todo login retorna:
|
||||
|
||||
```typescript
|
||||
// Adiciona token automaticamente em todas as requisições
|
||||
axios.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('access_token')
|
||||
axios.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config
|
||||
})
|
||||
return config;
|
||||
});
|
||||
|
||||
// Refresh automático quando token expira
|
||||
axios.interceptors.response.use(
|
||||
response => response,
|
||||
async error => {
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
if (error.response?.status === 401) {
|
||||
const refreshToken = localStorage.getItem('refresh_token')
|
||||
const newTokens = await authService.refreshToken(refreshToken)
|
||||
const refreshToken = localStorage.getItem("refresh_token");
|
||||
const newTokens = await authService.refreshToken(refreshToken);
|
||||
// Retry request original
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
### Roles e Permissões (RLS)
|
||||
|
||||
#### 👑 Admin/Gestor:
|
||||
|
||||
- ✅ **Acesso completo a todos os recursos**
|
||||
- ✅ Criar/editar/deletar usuários, médicos, pacientes
|
||||
- ✅ Visualizar todos os agendamentos e prontuários
|
||||
|
||||
#### 👨⚕️ Médicos:
|
||||
|
||||
- ✅ Veem **todos os pacientes**
|
||||
- ✅ Veem apenas **seus próprios laudos** (filtro: `created_by = médico`)
|
||||
- ✅ Veem apenas **seus próprios agendamentos** (filtro: `doctor_id = médico`)
|
||||
- ✅ Editam apenas **seus próprios laudos e agendamentos**
|
||||
|
||||
#### 👤 Pacientes:
|
||||
|
||||
- ✅ Veem apenas **seus próprios dados**
|
||||
- ✅ Veem apenas **seus próprios laudos** (filtro: `patient_id = paciente`)
|
||||
- ✅ Veem apenas **seus próprios agendamentos**
|
||||
- ✅ Podem agendar consultas
|
||||
|
||||
#### 👩💼 Secretárias:
|
||||
|
||||
- ✅ Veem **todos os pacientes**
|
||||
- ✅ Veem **todos os agendamentos**
|
||||
- ✅ Veem **todos os laudos**
|
||||
@ -145,96 +150,96 @@ src/services/
|
||||
|
||||
```typescript
|
||||
// Login
|
||||
await authService.login({ email, password })
|
||||
await authService.login({ email, password });
|
||||
// Retorna: { access_token, refresh_token, user }
|
||||
|
||||
// Recuperação de senha
|
||||
await authService.requestPasswordReset(email)
|
||||
await authService.requestPasswordReset(email);
|
||||
// Envia email com link de reset
|
||||
|
||||
// Atualizar senha
|
||||
await authService.updatePassword(accessToken, newPassword)
|
||||
await authService.updatePassword(accessToken, newPassword);
|
||||
// Usado na página /reset-password
|
||||
|
||||
// Refresh token
|
||||
await authService.refreshToken(refreshToken)
|
||||
await authService.refreshToken(refreshToken);
|
||||
```
|
||||
|
||||
#### 👤 Usuários (userService)
|
||||
|
||||
```typescript
|
||||
// Buscar informações do usuário autenticado (com roles)
|
||||
const userInfo = await userService.getUserInfo()
|
||||
const userInfo = await userService.getUserInfo();
|
||||
// Retorna: { id, email, full_name, roles: ['medico', 'admin'] }
|
||||
|
||||
// Criar usuário com role
|
||||
await userService.createUser({
|
||||
email: 'user@example.com',
|
||||
full_name: 'Nome Completo',
|
||||
role: 'medico' // ou 'admin', 'paciente', 'secretaria'
|
||||
})
|
||||
email: "user@example.com",
|
||||
full_name: "Nome Completo",
|
||||
role: "medico", // ou 'admin', 'paciente', 'secretaria'
|
||||
});
|
||||
|
||||
// Deletar usuário
|
||||
await userService.deleteUser(userId)
|
||||
await userService.deleteUser(userId);
|
||||
```
|
||||
|
||||
#### 🏥 Pacientes (patientService)
|
||||
|
||||
```typescript
|
||||
// Listar pacientes
|
||||
const patients = await patientService.list()
|
||||
const patients = await patientService.list();
|
||||
|
||||
// Buscar por ID
|
||||
const patient = await patientService.getById(id)
|
||||
const patient = await patientService.getById(id);
|
||||
|
||||
// Criar paciente
|
||||
await patientService.create({
|
||||
email: 'paciente@example.com',
|
||||
full_name: 'Nome Paciente',
|
||||
cpf: '12345678900',
|
||||
phone_mobile: '11999999999'
|
||||
})
|
||||
email: "paciente@example.com",
|
||||
full_name: "Nome Paciente",
|
||||
cpf: "12345678900",
|
||||
phone_mobile: "11999999999",
|
||||
});
|
||||
|
||||
// Atualizar paciente
|
||||
await patientService.update(id, { phone_mobile: '11888888888' })
|
||||
await patientService.update(id, { phone_mobile: "11888888888" });
|
||||
|
||||
// Deletar paciente
|
||||
await patientService.delete(id)
|
||||
await patientService.delete(id);
|
||||
```
|
||||
|
||||
#### 👨⚕️ Médicos (doctorService)
|
||||
|
||||
```typescript
|
||||
// Listar médicos
|
||||
const doctors = await doctorService.list()
|
||||
const doctors = await doctorService.list();
|
||||
|
||||
// Buscar por ID
|
||||
const doctor = await doctorService.getById(id)
|
||||
const doctor = await doctorService.getById(id);
|
||||
|
||||
// Buscar disponibilidade
|
||||
const slots = await doctorService.getAvailableSlots(doctorId, date)
|
||||
const slots = await doctorService.getAvailableSlots(doctorId, date);
|
||||
```
|
||||
|
||||
#### 📅 Agendamentos (appointmentService)
|
||||
|
||||
```typescript
|
||||
// Listar agendamentos (filtrado por role automaticamente)
|
||||
const appointments = await appointmentService.list()
|
||||
const appointments = await appointmentService.list();
|
||||
|
||||
// Criar agendamento
|
||||
await appointmentService.create({
|
||||
patient_id: 'uuid-paciente',
|
||||
doctor_id: 'uuid-medico',
|
||||
scheduled_at: '2025-10-25T10:00:00',
|
||||
reason: 'Consulta de rotina'
|
||||
})
|
||||
patient_id: "uuid-paciente",
|
||||
doctor_id: "uuid-medico",
|
||||
scheduled_at: "2025-10-25T10:00:00",
|
||||
reason: "Consulta de rotina",
|
||||
});
|
||||
|
||||
// Atualizar status
|
||||
await appointmentService.updateStatus(id, 'confirmed')
|
||||
await appointmentService.updateStatus(id, "confirmed");
|
||||
// Status: requested, confirmed, completed, cancelled
|
||||
|
||||
// Cancelar
|
||||
await appointmentService.cancel(id, 'Motivo do cancelamento')
|
||||
await appointmentService.cancel(id, "Motivo do cancelamento");
|
||||
```
|
||||
|
||||
---
|
||||
@ -253,9 +258,9 @@ export const API_CONFIG = {
|
||||
STORAGE_KEYS: {
|
||||
ACCESS_TOKEN: "mediconnect_access_token",
|
||||
REFRESH_TOKEN: "mediconnect_refresh_token",
|
||||
USER: "mediconnect_user"
|
||||
}
|
||||
}
|
||||
USER: "mediconnect_user",
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
@ -292,11 +297,11 @@ npx wrangler pages deploy dist --project-name=mediconnect --commit-dirty=true
|
||||
|
||||
## 1. Variáveis de Ambiente (`.env` / `.env.local`)
|
||||
|
||||
| Variável | Obrigatória | Descrição |
|
||||
| ------------------------ | ---------------- | --------------------------------------------------------------- |
|
||||
| `VITE_SUPABASE_URL` | Sim | URL base do projeto Supabase (`https://<ref>.supabase.co`) |
|
||||
| `VITE_SUPABASE_ANON_KEY` | Sim | Chave pública (anon) usada para Auth password grant e PostgREST |
|
||||
| `VITE_APP_ENV` | Não | Identifica ambiente (ex: `dev`, `staging`, `prod`) |
|
||||
| Variável | Obrigatória | Descrição |
|
||||
| ------------------------ | ----------- | --------------------------------------------------------------- |
|
||||
| `VITE_SUPABASE_URL` | Sim | URL base do projeto Supabase (`https://<ref>.supabase.co`) |
|
||||
| `VITE_SUPABASE_ANON_KEY` | Sim | Chave pública (anon) usada para Auth password grant e PostgREST |
|
||||
| `VITE_APP_ENV` | Não | Identifica ambiente (ex: `dev`, `staging`, `prod`) |
|
||||
|
||||
**Nota:** As variáveis já estão configuradas no código em `src/services/api/config.ts`. Não é necessário criar arquivo `.env` para desenvolvimento local.
|
||||
|
||||
@ -314,38 +319,37 @@ Boas práticas:
|
||||
```typescript
|
||||
// pages/LoginMedico.tsx
|
||||
const handleLogin = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
// 1. Autenticar com Supabase
|
||||
const loginResponse = await authService.login({ email, password })
|
||||
|
||||
const loginResponse = await authService.login({ email, password });
|
||||
|
||||
// 2. Salvar tokens
|
||||
localStorage.setItem('access_token', loginResponse.access_token)
|
||||
localStorage.setItem('refresh_token', loginResponse.refresh_token)
|
||||
|
||||
localStorage.setItem("access_token", loginResponse.access_token);
|
||||
localStorage.setItem("refresh_token", loginResponse.refresh_token);
|
||||
|
||||
// 3. Buscar informações do usuário com roles
|
||||
const userInfo = await userService.getUserInfo()
|
||||
const roles = userInfo.roles || []
|
||||
|
||||
const userInfo = await userService.getUserInfo();
|
||||
const roles = userInfo.roles || [];
|
||||
|
||||
// 4. Validar permissões
|
||||
const isAdmin = roles.includes('admin')
|
||||
const isGestor = roles.includes('gestor')
|
||||
const isMedico = roles.includes('medico')
|
||||
|
||||
const isAdmin = roles.includes("admin");
|
||||
const isGestor = roles.includes("gestor");
|
||||
const isMedico = roles.includes("medico");
|
||||
|
||||
if (!isAdmin && !isGestor && !isMedico) {
|
||||
toast.error("Você não tem permissão para acessar esta área")
|
||||
await authService.logout()
|
||||
return
|
||||
toast.error("Você não tem permissão para acessar esta área");
|
||||
await authService.logout();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 5. Redirecionar para o painel
|
||||
navigate('/painel-medico')
|
||||
|
||||
navigate("/painel-medico");
|
||||
} catch (error) {
|
||||
toast.error("Email ou senha incorretos")
|
||||
toast.error("Email ou senha incorretos");
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
@ -367,35 +371,35 @@ const handleLogin = async (e: FormEvent) => {
|
||||
// Solicitar recuperação (páginas de login)
|
||||
const handlePasswordReset = async () => {
|
||||
try {
|
||||
await authService.requestPasswordReset(email)
|
||||
toast.success("Link de recuperação enviado para seu email")
|
||||
await authService.requestPasswordReset(email);
|
||||
toast.success("Link de recuperação enviado para seu email");
|
||||
} catch (error) {
|
||||
toast.error("Erro ao enviar email de recuperação")
|
||||
toast.error("Erro ao enviar email de recuperação");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Página de reset (ResetPassword.tsx)
|
||||
useEffect(() => {
|
||||
// Extrair token do URL
|
||||
const hash = window.location.hash
|
||||
const params = new URLSearchParams(hash.substring(1))
|
||||
const token = params.get('access_token')
|
||||
|
||||
const hash = window.location.hash;
|
||||
const params = new URLSearchParams(hash.substring(1));
|
||||
const token = params.get("access_token");
|
||||
|
||||
if (token) {
|
||||
setAccessToken(token)
|
||||
setIsLoading(false)
|
||||
setAccessToken(token);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
try {
|
||||
await authService.updatePassword(accessToken, newPassword)
|
||||
toast.success("Senha atualizada com sucesso!")
|
||||
navigate('/login-paciente')
|
||||
await authService.updatePassword(accessToken, newPassword);
|
||||
toast.success("Senha atualizada com sucesso!");
|
||||
navigate("/login-paciente");
|
||||
} catch (error) {
|
||||
toast.error("Erro ao atualizar senha")
|
||||
toast.error("Erro ao atualizar senha");
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
@ -405,6 +409,7 @@ const handleSubmit = async (e: FormEvent) => {
|
||||
### Tabelas Principais
|
||||
|
||||
#### `profiles`
|
||||
|
||||
```sql
|
||||
- id (uuid, PK)
|
||||
- email (text, unique)
|
||||
@ -415,6 +420,7 @@ const handleSubmit = async (e: FormEvent) => {
|
||||
```
|
||||
|
||||
#### `patients`
|
||||
|
||||
```sql
|
||||
- id (uuid, PK, FK -> profiles)
|
||||
- email (text, unique)
|
||||
@ -427,6 +433,7 @@ const handleSubmit = async (e: FormEvent) => {
|
||||
```
|
||||
|
||||
#### `doctors`
|
||||
|
||||
```sql
|
||||
- id (uuid, PK, FK -> profiles)
|
||||
- email (text, unique)
|
||||
@ -438,6 +445,7 @@ const handleSubmit = async (e: FormEvent) => {
|
||||
```
|
||||
|
||||
#### `appointments`
|
||||
|
||||
```sql
|
||||
- id (uuid, PK)
|
||||
- patient_id (uuid, FK -> patients)
|
||||
@ -450,6 +458,7 @@ const handleSubmit = async (e: FormEvent) => {
|
||||
```
|
||||
|
||||
#### `user_roles`
|
||||
|
||||
```sql
|
||||
- user_id (uuid, FK -> profiles)
|
||||
- role (text: admin, gestor, medico, secretaria, paciente)
|
||||
@ -462,13 +471,14 @@ const handleSubmit = async (e: FormEvent) => {
|
||||
|
||||
O sistema usa uma estratégia híbrida para máxima segurança:
|
||||
|
||||
| Tipo | Local | Expiração Natural |
|
||||
| -------------- | ----------------- | ------------------------------------ |
|
||||
| Access Token | localStorage | 1 hora (renovado automaticamente) |
|
||||
| Refresh Token | localStorage | 30 dias (ou revogação backend) |
|
||||
| User Snapshot | localStorage | Limpo em logout |
|
||||
| Tipo | Local | Expiração Natural |
|
||||
| ------------- | ------------ | --------------------------------- |
|
||||
| Access Token | localStorage | 1 hora (renovado automaticamente) |
|
||||
| Refresh Token | localStorage | 30 dias (ou revogação backend) |
|
||||
| User Snapshot | localStorage | Limpo em logout |
|
||||
|
||||
**Segurança:**
|
||||
|
||||
- Tokens são limpos automaticamente no logout
|
||||
- Refresh automático quando access_token expira (401)
|
||||
- Interceptors garantem tokens válidos em todas as requisições
|
||||
@ -497,6 +507,7 @@ Fluxo de Refresh:
|
||||
|
||||
1. Requisição falha com 401.
|
||||
2. Wrapper (`http.ts`) obtém refresh do `tokenStore`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Scripts Utilitários
|
||||
@ -545,9 +556,9 @@ node search-fernando.cjs
|
||||
|
||||
```typescript
|
||||
// Imports
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { serviceImport } from "../services"
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { serviceImport } from "../services";
|
||||
|
||||
// Types
|
||||
interface Props {
|
||||
@ -557,28 +568,24 @@ interface Props {
|
||||
// Component
|
||||
const ComponentName: React.FC<Props> = ({ ...props }) => {
|
||||
// Hooks
|
||||
const navigate = useNavigate()
|
||||
const [state, setState] = useState()
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [state, setState] = useState();
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
// ...
|
||||
}, [])
|
||||
|
||||
}, []);
|
||||
|
||||
// Handlers
|
||||
const handleAction = async () => {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Render
|
||||
return (
|
||||
<div>
|
||||
{/* JSX */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
export default ComponentName
|
||||
// Render
|
||||
return <div>{/* JSX */}</div>;
|
||||
};
|
||||
|
||||
export default ComponentName;
|
||||
```
|
||||
|
||||
---
|
||||
@ -586,6 +593,7 @@ export default ComponentName
|
||||
## 9. Tecnologias Utilizadas
|
||||
|
||||
### Frontend
|
||||
|
||||
- **React** 18.3.1 - Biblioteca UI
|
||||
- **TypeScript** 5.9.3 - Tipagem estática
|
||||
- **Vite** 7.1.10 - Build tool
|
||||
@ -596,11 +604,13 @@ export default ComponentName
|
||||
- **date-fns** 4.1.0 - Manipulação de datas
|
||||
|
||||
### Backend
|
||||
|
||||
- **Supabase** - Backend as a Service
|
||||
- **PostgreSQL** - Banco de dados relacional
|
||||
- **Supabase Auth** - Autenticação JWT
|
||||
|
||||
### Deploy
|
||||
|
||||
- **Cloudflare Pages** - Hospedagem frontend
|
||||
- **Wrangler** 4.44.0 - CLI Cloudflare
|
||||
|
||||
@ -615,4 +625,3 @@ export default ComponentName
|
||||
---
|
||||
|
||||
**Desenvolvido com ❤️ pela Squad 18**
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
})();
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
})();
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
})();
|
||||
54
MEDICONNECT 2/src/components/auth/RecoveryRedirect.tsx
Normal file
54
MEDICONNECT 2/src/components/auth/RecoveryRedirect.tsx
Normal 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;
|
||||
@ -21,7 +21,32 @@ export default function AuthCallback() {
|
||||
useEffect(() => {
|
||||
const handleCallback = async () => {
|
||||
try {
|
||||
console.log("[AuthCallback] Iniciando processamento do magic link");
|
||||
console.log("[AuthCallback] Iniciando processamento");
|
||||
|
||||
// Verificar se é um token de recovery
|
||||
const hash = window.location.hash;
|
||||
const hashParams = new URLSearchParams(hash.substring(1));
|
||||
const accessToken = hashParams.get("access_token");
|
||||
const type = hashParams.get("type");
|
||||
|
||||
console.log("[AuthCallback] Hash:", hash);
|
||||
console.log("[AuthCallback] Type:", type);
|
||||
console.log("[AuthCallback] Access Token presente:", !!accessToken);
|
||||
|
||||
// Se for recovery, redirecionar para página de reset
|
||||
if (type === "recovery" && accessToken) {
|
||||
console.log("[AuthCallback] ✅ Token de recovery detectado, redirecionando para /reset-password");
|
||||
setStatus("success");
|
||||
setMessage("Redirecionando para página de redefinição de senha...");
|
||||
|
||||
// Redirecionar preservando o hash com o token
|
||||
setTimeout(() => {
|
||||
navigate(`/reset-password${hash}`, { replace: true });
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[AuthCallback] Processando magic link normal");
|
||||
|
||||
// Supabase automaticamente processa os query params
|
||||
const {
|
||||
|
||||
@ -6,6 +6,7 @@ import { MetricCard } from "../components/MetricCard";
|
||||
import { HeroBanner } from "../components/HeroBanner";
|
||||
import { i18n } from "../i18n";
|
||||
import { useAuth } from "../hooks/useAuth";
|
||||
import RecoveryRedirect from "../components/auth/RecoveryRedirect";
|
||||
|
||||
const Home: React.FC = () => {
|
||||
const [stats, setStats] = useState({
|
||||
@ -97,6 +98,9 @@ const Home: React.FC = () => {
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<HeroBanner />
|
||||
|
||||
|
||||
@ -14,35 +14,48 @@ const ResetPassword: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
// Extrair access_token do hash da URL
|
||||
// Extrair access_token do hash da URL (#access_token=...)
|
||||
// OU do query string (?token=...&type=recovery)
|
||||
|
||||
let token: string | null = null;
|
||||
let type: string | null = null;
|
||||
|
||||
// Primeiro tenta no hash (formato padrão do Supabase após redirect)
|
||||
const hash = window.location.hash;
|
||||
console.log("[ResetPassword] Hash completo:", hash);
|
||||
|
||||
if (hash) {
|
||||
const params = new URLSearchParams(hash.substring(1));
|
||||
const token = params.get("access_token");
|
||||
const type = params.get("type");
|
||||
const hashParams = new URLSearchParams(hash.substring(1));
|
||||
token = hashParams.get("access_token");
|
||||
type = hashParams.get("type");
|
||||
|
||||
console.log("[ResetPassword] Token do hash:", token ? token.substring(0, 20) + "..." : "null");
|
||||
console.log("[ResetPassword] Type do hash:", type);
|
||||
}
|
||||
|
||||
console.log(
|
||||
"[ResetPassword] Token extraído:",
|
||||
token ? token.substring(0, 20) + "..." : "null"
|
||||
);
|
||||
console.log("[ResetPassword] Type:", type);
|
||||
|
||||
if (token) {
|
||||
setAccessToken(token);
|
||||
console.log(
|
||||
"[ResetPassword] ✅ Token de recuperação detectado e armazenado"
|
||||
);
|
||||
} else {
|
||||
console.error("[ResetPassword] ❌ Token não encontrado no hash");
|
||||
toast.error("Link de recuperação inválido ou expirado");
|
||||
setTimeout(() => navigate("/"), 3000);
|
||||
// Se não encontrou no hash, tenta no query string
|
||||
if (!token) {
|
||||
const search = window.location.search;
|
||||
console.log("[ResetPassword] Query string completo:", search);
|
||||
|
||||
if (search) {
|
||||
const queryParams = new URLSearchParams(search);
|
||||
token = queryParams.get("token");
|
||||
type = queryParams.get("type");
|
||||
|
||||
console.log("[ResetPassword] Token do query:", token ? token.substring(0, 20) + "..." : "null");
|
||||
console.log("[ResetPassword] Type do query:", type);
|
||||
}
|
||||
}
|
||||
|
||||
if (token) {
|
||||
setAccessToken(token);
|
||||
console.log("[ResetPassword] ✅ Token de recuperação detectado e armazenado");
|
||||
console.log("[ResetPassword] Type:", type);
|
||||
} else {
|
||||
console.error("[ResetPassword] ❌ Nenhum hash encontrado na URL");
|
||||
console.error("[ResetPassword] ❌ Token não encontrado no hash nem no query string");
|
||||
console.log("[ResetPassword] URL completa:", window.location.href);
|
||||
toast.error("Link de recuperação inválido");
|
||||
toast.error("Link de recuperação inválido ou expirado");
|
||||
setTimeout(() => navigate("/"), 3000);
|
||||
}
|
||||
}, [navigate]);
|
||||
@ -93,10 +106,26 @@ const ResetPassword: React.FC = () => {
|
||||
} catch (error: unknown) {
|
||||
console.error("[ResetPassword] Erro ao atualizar senha:", error);
|
||||
const err = error as {
|
||||
response?: { data?: { error_description?: string; message?: string } };
|
||||
response?: {
|
||||
data?: {
|
||||
error_description?: string;
|
||||
message?: string;
|
||||
msg?: string;
|
||||
error_code?: string;
|
||||
}
|
||||
};
|
||||
message?: string;
|
||||
};
|
||||
|
||||
// Mensagem específica para senha igual
|
||||
if (err?.response?.data?.error_code === "same_password") {
|
||||
toast.error("A nova senha deve ser diferente da senha atual");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const errorMessage =
|
||||
err?.response?.data?.msg ||
|
||||
err?.response?.data?.error_description ||
|
||||
err?.response?.data?.message ||
|
||||
err?.message ||
|
||||
|
||||
@ -154,20 +154,31 @@ class AuthService {
|
||||
/**
|
||||
* Solicita reset de senha via email (público)
|
||||
* POST /auth/v1/recover
|
||||
*
|
||||
* Envia redirectTo dentro de options para controlar para onde o Supabase redireciona
|
||||
*/
|
||||
async requestPasswordReset(
|
||||
email: string,
|
||||
redirectUrl?: string
|
||||
email: string
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const redirectUrl = `${API_CONFIG.APP_URL}/reset-password`;
|
||||
|
||||
console.log("[authService.requestPasswordReset] Email:", email);
|
||||
console.log("[authService.requestPasswordReset] Redirect URL:", redirectUrl);
|
||||
console.log("[authService.requestPasswordReset] Usando endpoint /auth/v1/recover");
|
||||
|
||||
const payload = {
|
||||
email,
|
||||
options: {
|
||||
redirectTo: redirectUrl,
|
||||
},
|
||||
};
|
||||
|
||||
console.log("[authService.requestPasswordReset] Payload:", JSON.stringify(payload, null, 2));
|
||||
|
||||
await axios.post(
|
||||
`${API_CONFIG.AUTH_URL}/recover`,
|
||||
{
|
||||
email,
|
||||
options: {
|
||||
redirectTo: redirectUrl || `${API_CONFIG.APP_URL}/reset-password`,
|
||||
},
|
||||
},
|
||||
payload,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@ -176,13 +187,15 @@ class AuthService {
|
||||
}
|
||||
);
|
||||
|
||||
console.log("[authService.requestPasswordReset] ✅ Email de recuperação enviado");
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message:
|
||||
"Email de recuperação de senha enviado com sucesso. Verifique sua caixa de entrada.",
|
||||
message: "Email de recuperação de senha enviado com sucesso. Verifique sua caixa de entrada.",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Erro ao solicitar reset de senha:", error);
|
||||
} catch (error: any) {
|
||||
console.error("[authService.requestPasswordReset] ❌ Erro:", error);
|
||||
console.error("[authService.requestPasswordReset] Response:", error.response?.data);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -196,7 +209,12 @@ class AuthService {
|
||||
newPassword: string
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
await axios.put(
|
||||
console.log("[authService.updatePassword] Iniciando atualização de senha");
|
||||
console.log("[authService.updatePassword] Token (primeiros 30 chars):", accessToken.substring(0, 30));
|
||||
console.log("[authService.updatePassword] Nova senha length:", newPassword.length);
|
||||
console.log("[authService.updatePassword] URL:", `${API_CONFIG.AUTH_URL}/user`);
|
||||
|
||||
const response = await axios.put(
|
||||
`${API_CONFIG.AUTH_URL}/user`,
|
||||
{
|
||||
password: newPassword,
|
||||
@ -210,12 +228,35 @@ class AuthService {
|
||||
}
|
||||
);
|
||||
|
||||
console.log("[authService.updatePassword] ✅ Senha atualizada com sucesso");
|
||||
console.log("[authService.updatePassword] Response status:", response.status);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Senha atualizada com sucesso",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Erro ao atualizar senha:", error);
|
||||
} catch (error: any) {
|
||||
console.error("[authService.updatePassword] ❌ Erro ao atualizar senha:", error);
|
||||
console.error("[authService.updatePassword] Error response:", error.response?.data);
|
||||
console.error("[authService.updatePassword] Error response (stringified):", JSON.stringify(error.response?.data, null, 2));
|
||||
console.error("[authService.updatePassword] Error status:", error.response?.status);
|
||||
console.error("[authService.updatePassword] Error statusText:", error.response?.statusText);
|
||||
console.error("[authService.updatePassword] Error headers:", error.response?.headers);
|
||||
console.error("[authService.updatePassword] Request URL:", error.config?.url);
|
||||
console.error("[authService.updatePassword] Request headers:", error.config?.headers);
|
||||
console.error("[authService.updatePassword] Request data:", error.config?.data);
|
||||
|
||||
// Se for 422, mostrar mensagem específica
|
||||
if (error.response?.status === 422) {
|
||||
const errorMsg = error.response?.data?.msg || error.response?.data?.message || error.response?.data?.error_description || "Token inválido ou expirado";
|
||||
console.error("[authService.updatePassword] ⚠️ Erro 422 - Possíveis causas:");
|
||||
console.error(" 1. Token de recuperação expirado (válido por 1 hora)");
|
||||
console.error(" 2. Token já foi usado");
|
||||
console.error(" 3. Formato do token incorreto");
|
||||
console.error(" 4. Usuário não existe");
|
||||
console.error("[authService.updatePassword] Mensagem do servidor:", errorMsg);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
})();
|
||||
188
README.md
188
README.md
@ -61,12 +61,14 @@ O MediConnect é uma plataforma web que conecta **pacientes**, **médicos** e **
|
||||
### Camadas da Aplicação
|
||||
|
||||
1. **Apresentação (UI)**
|
||||
|
||||
- React 18.3.1 com TypeScript
|
||||
- React Router para navegação
|
||||
- Tailwind CSS para estilização
|
||||
- Lucide React para ícones
|
||||
|
||||
2. **Lógica de Negócio (Services)**
|
||||
|
||||
- Services organizados por domínio
|
||||
- Axios para requisições HTTP
|
||||
- Interceptors para autenticação automática
|
||||
@ -81,6 +83,7 @@ O MediConnect é uma plataforma web que conecta **pacientes**, **médicos** e **
|
||||
## 🛠️ Tecnologias
|
||||
|
||||
### Frontend
|
||||
|
||||
- **React** 18.3.1 - Biblioteca UI
|
||||
- **TypeScript** 5.9.3 - Tipagem estática
|
||||
- **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
|
||||
|
||||
### Backend
|
||||
|
||||
- **Supabase** - Backend as a Service
|
||||
- **PostgreSQL** - Banco de dados relacional
|
||||
- **Supabase Auth** - Autenticação JWT
|
||||
|
||||
### Deploy
|
||||
|
||||
- **Cloudflare Pages** - Hospedagem frontend
|
||||
- **Wrangler** 4.44.0 - CLI Cloudflare
|
||||
|
||||
@ -187,106 +192,112 @@ export const API_CONFIG = {
|
||||
STORAGE_KEYS: {
|
||||
ACCESS_TOKEN: "mediconnect_access_token",
|
||||
REFRESH_TOKEN: "mediconnect_refresh_token",
|
||||
USER: "mediconnect_user"
|
||||
}
|
||||
}
|
||||
USER: "mediconnect_user",
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Serviços Disponíveis
|
||||
|
||||
#### 1. **authService** - Autenticação
|
||||
|
||||
```typescript
|
||||
// Login
|
||||
await authService.login({ email, password })
|
||||
await authService.login({ email, password });
|
||||
|
||||
// Registro
|
||||
await authService.signup({ email, password, full_name })
|
||||
await authService.signup({ email, password, full_name });
|
||||
|
||||
// Logout
|
||||
await authService.logout()
|
||||
await authService.logout();
|
||||
|
||||
// Recuperação de senha
|
||||
await authService.requestPasswordReset(email)
|
||||
await authService.updatePassword(accessToken, newPassword)
|
||||
await authService.requestPasswordReset(email);
|
||||
await authService.updatePassword(accessToken, newPassword);
|
||||
|
||||
// Refresh token
|
||||
await authService.refreshToken(refreshToken)
|
||||
await authService.refreshToken(refreshToken);
|
||||
```
|
||||
|
||||
#### 2. **userService** - Usuários
|
||||
|
||||
```typescript
|
||||
// Buscar informações do usuário autenticado
|
||||
const userInfo = await userService.getUserInfo()
|
||||
const userInfo = await userService.getUserInfo();
|
||||
|
||||
// Criar usuário com role
|
||||
await userService.createUser({ email, full_name, role })
|
||||
await userService.createUser({ email, full_name, role });
|
||||
|
||||
// Deletar usuário
|
||||
await userService.deleteUser(userId)
|
||||
await userService.deleteUser(userId);
|
||||
```
|
||||
|
||||
#### 3. **patientService** - Pacientes
|
||||
|
||||
```typescript
|
||||
// Listar pacientes
|
||||
const patients = await patientService.list()
|
||||
const patients = await patientService.list();
|
||||
|
||||
// Buscar por ID
|
||||
const patient = await patientService.getById(id)
|
||||
const patient = await patientService.getById(id);
|
||||
|
||||
// Criar paciente
|
||||
await patientService.create({ email, full_name, cpf, phone })
|
||||
await patientService.create({ email, full_name, cpf, phone });
|
||||
|
||||
// Atualizar
|
||||
await patientService.update(id, data)
|
||||
await patientService.update(id, data);
|
||||
|
||||
// Registrar paciente (público)
|
||||
await patientService.register({ email, full_name, cpf })
|
||||
await patientService.register({ email, full_name, cpf });
|
||||
```
|
||||
|
||||
#### 4. **doctorService** - Médicos
|
||||
|
||||
```typescript
|
||||
// Listar médicos
|
||||
const doctors = await doctorService.list()
|
||||
const doctors = await doctorService.list();
|
||||
|
||||
// Buscar por ID
|
||||
const doctor = await doctorService.getById(id)
|
||||
const doctor = await doctorService.getById(id);
|
||||
|
||||
// Buscar disponibilidade
|
||||
const slots = await doctorService.getAvailableSlots(doctorId, date)
|
||||
const slots = await doctorService.getAvailableSlots(doctorId, date);
|
||||
```
|
||||
|
||||
#### 5. **appointmentService** - Consultas
|
||||
|
||||
```typescript
|
||||
// Listar consultas
|
||||
const appointments = await appointmentService.list()
|
||||
const appointments = await appointmentService.list();
|
||||
|
||||
// Criar consulta
|
||||
await appointmentService.create({
|
||||
patient_id,
|
||||
doctor_id,
|
||||
scheduled_at,
|
||||
reason
|
||||
})
|
||||
reason,
|
||||
});
|
||||
|
||||
// Atualizar status
|
||||
await appointmentService.updateStatus(id, status)
|
||||
await appointmentService.updateStatus(id, status);
|
||||
|
||||
// Cancelar
|
||||
await appointmentService.cancel(id, reason)
|
||||
await appointmentService.cancel(id, reason);
|
||||
```
|
||||
|
||||
#### 6. **availabilityService** - Disponibilidade
|
||||
|
||||
```typescript
|
||||
// Gerenciar disponibilidade do médico
|
||||
await availabilityService.create({
|
||||
doctor_id,
|
||||
day_of_week,
|
||||
start_time,
|
||||
end_time
|
||||
})
|
||||
end_time,
|
||||
});
|
||||
|
||||
// Listar disponibilidade
|
||||
const slots = await availabilityService.listByDoctor(doctorId)
|
||||
const slots = await availabilityService.listByDoctor(doctorId);
|
||||
```
|
||||
|
||||
---
|
||||
@ -296,61 +307,65 @@ const slots = await availabilityService.listByDoctor(doctorId)
|
||||
### Fluxo de Autenticação
|
||||
|
||||
1. **Login**
|
||||
|
||||
```typescript
|
||||
// 1. Usuário envia credenciais
|
||||
const response = await authService.login({ email, password })
|
||||
const response = await authService.login({ email, password });
|
||||
|
||||
// 2. Recebe tokens JWT
|
||||
localStorage.setItem('access_token', response.access_token)
|
||||
localStorage.setItem('refresh_token', response.refresh_token)
|
||||
localStorage.setItem("access_token", response.access_token);
|
||||
localStorage.setItem("refresh_token", response.refresh_token);
|
||||
|
||||
// 3. Busca informações completas
|
||||
const userInfo = await userService.getUserInfo()
|
||||
const userInfo = await userService.getUserInfo();
|
||||
|
||||
// 4. Valida roles
|
||||
if (userInfo.roles.includes('medico')) {
|
||||
navigate('/painel-medico')
|
||||
if (userInfo.roles.includes("medico")) {
|
||||
navigate("/painel-medico");
|
||||
}
|
||||
```
|
||||
|
||||
2. **Interceptor Automático**
|
||||
|
||||
```typescript
|
||||
// Todo request adiciona o token automaticamente
|
||||
axios.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('access_token')
|
||||
axios.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config
|
||||
})
|
||||
return config;
|
||||
});
|
||||
```
|
||||
|
||||
3. **Refresh Token Automático**
|
||||
|
||||
```typescript
|
||||
axios.interceptors.response.use(
|
||||
response => response,
|
||||
async error => {
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Token expirado, tenta refresh
|
||||
const refreshToken = localStorage.getItem('refresh_token')
|
||||
const newTokens = await authService.refreshToken(refreshToken)
|
||||
const refreshToken = localStorage.getItem("refresh_token");
|
||||
const newTokens = await authService.refreshToken(refreshToken);
|
||||
// Retry request original
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
### Roles e Permissões
|
||||
|
||||
| Role | Acesso |
|
||||
|------|--------|
|
||||
| **admin** | Acesso total ao sistema |
|
||||
| **gestor** | Gestão de médicos, secretárias e relatórios |
|
||||
| **medico** | Painel médico, consultas, prontuários |
|
||||
| **secretaria** | Agendamento, gestão de pacientes |
|
||||
| **paciente** | Agendamento, visualização de consultas |
|
||||
| Role | Acesso |
|
||||
| -------------- | ------------------------------------------- |
|
||||
| **admin** | Acesso total ao sistema |
|
||||
| **gestor** | Gestão de médicos, secretárias e relatórios |
|
||||
| **medico** | Painel médico, consultas, prontuários |
|
||||
| **secretaria** | Agendamento, gestão de pacientes |
|
||||
| **paciente** | Agendamento, visualização de consultas |
|
||||
|
||||
**Hierarquia de Roles:**
|
||||
|
||||
```
|
||||
admin > gestor > medico/secretaria > paciente
|
||||
```
|
||||
@ -397,26 +412,26 @@ sequenceDiagram
|
||||
// LoginMedico.tsx
|
||||
const handleLogin = async () => {
|
||||
// 1. Login
|
||||
const loginResponse = await authService.login({ email, password })
|
||||
|
||||
const loginResponse = await authService.login({ email, password });
|
||||
|
||||
// 2. Buscar roles
|
||||
const userInfo = await userService.getUserInfo()
|
||||
const roles = userInfo.roles || []
|
||||
|
||||
const userInfo = await userService.getUserInfo();
|
||||
const roles = userInfo.roles || [];
|
||||
|
||||
// 3. Validar permissão
|
||||
const isAdmin = roles.includes('admin')
|
||||
const isGestor = roles.includes('gestor')
|
||||
const isMedico = roles.includes('medico')
|
||||
|
||||
const isAdmin = roles.includes("admin");
|
||||
const isGestor = roles.includes("gestor");
|
||||
const isMedico = roles.includes("medico");
|
||||
|
||||
if (!isAdmin && !isGestor && !isMedico) {
|
||||
toast.error("Você não tem permissão para acessar esta área")
|
||||
await authService.logout()
|
||||
return
|
||||
toast.error("Você não tem permissão para acessar esta área");
|
||||
await authService.logout();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 4. Redirecionar
|
||||
navigate('/painel-medico')
|
||||
}
|
||||
navigate("/painel-medico");
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
@ -462,7 +477,7 @@ npx wrangler pages deploy dist --project-name=mediconnect --commit-dirty=true
|
||||
|
||||
### Pré-requisitos
|
||||
|
||||
- Node.js 18+
|
||||
- Node.js 18+
|
||||
- pnpm 8+
|
||||
- Git
|
||||
|
||||
@ -522,6 +537,7 @@ VITE_APP_URL=http://localhost:5173
|
||||
### Tabelas Principais
|
||||
|
||||
#### `profiles`
|
||||
|
||||
```sql
|
||||
- id (uuid, PK)
|
||||
- email (text, unique)
|
||||
@ -532,6 +548,7 @@ VITE_APP_URL=http://localhost:5173
|
||||
```
|
||||
|
||||
#### `patients`
|
||||
|
||||
```sql
|
||||
- id (uuid, PK, FK -> profiles)
|
||||
- email (text, unique)
|
||||
@ -544,6 +561,7 @@ VITE_APP_URL=http://localhost:5173
|
||||
```
|
||||
|
||||
#### `doctors`
|
||||
|
||||
```sql
|
||||
- id (uuid, PK, FK -> profiles)
|
||||
- email (text, unique)
|
||||
@ -555,6 +573,7 @@ VITE_APP_URL=http://localhost:5173
|
||||
```
|
||||
|
||||
#### `appointments`
|
||||
|
||||
```sql
|
||||
- id (uuid, PK)
|
||||
- patient_id (uuid, FK -> patients)
|
||||
@ -567,6 +586,7 @@ VITE_APP_URL=http://localhost:5173
|
||||
```
|
||||
|
||||
#### `user_roles`
|
||||
|
||||
```sql
|
||||
- user_id (uuid, FK -> profiles)
|
||||
- role (text: admin, gestor, medico, secretaria, paciente)
|
||||
@ -621,9 +641,9 @@ node search-fernando.cjs
|
||||
|
||||
```typescript
|
||||
// Imports
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { serviceImport } from "../services"
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { serviceImport } from "../services";
|
||||
|
||||
// Types
|
||||
interface Props {
|
||||
@ -633,28 +653,24 @@ interface Props {
|
||||
// Component
|
||||
const ComponentName: React.FC<Props> = ({ ...props }) => {
|
||||
// Hooks
|
||||
const navigate = useNavigate()
|
||||
const [state, setState] = useState()
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [state, setState] = useState();
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
// ...
|
||||
}, [])
|
||||
|
||||
}, []);
|
||||
|
||||
// Handlers
|
||||
const handleAction = async () => {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Render
|
||||
return (
|
||||
<div>
|
||||
{/* JSX */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
export default ComponentName
|
||||
// Render
|
||||
return <div>{/* JSX */}</div>;
|
||||
};
|
||||
|
||||
export default ComponentName;
|
||||
```
|
||||
|
||||
---
|
||||
@ -670,6 +686,7 @@ export default ComponentName
|
||||
## 📝 Changelog
|
||||
|
||||
### v2.0.0 (Outubro 2024)
|
||||
|
||||
- ✅ Migração completa de Netlify Functions para Supabase
|
||||
- ✅ Implementação de recuperação de senha
|
||||
- ✅ Deploy no Cloudflare Pages
|
||||
@ -679,6 +696,7 @@ export default ComponentName
|
||||
- ✅ Interface responsiva e dark mode
|
||||
|
||||
### v1.0.0 (Setembro 2024)
|
||||
|
||||
- ✅ Lançamento inicial
|
||||
- ✅ Login de pacientes, médicos e secretárias
|
||||
- ✅ Agendamento de consultas
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user