628 lines
15 KiB
Markdown
628 lines
15 KiB
Markdown
# MediConnect - Sistema de Agendamento Médico
|
||
|
||
Aplicação SPA (React + Vite + TypeScript) consumindo **Supabase** (Auth, PostgREST) diretamente do frontend, hospedada no **Cloudflare Pages**.
|
||
|
||
---
|
||
|
||
## 🚀 Acesso ao Sistema
|
||
|
||
- **URL Principal:** https://mediconnectbrasil.app/
|
||
- **URL Cloudflare:** https://mediconnect-5oz.pages.dev/
|
||
|
||
---
|
||
|
||
## 🏗️ Arquitetura Atual (Outubro 2025)
|
||
|
||
```
|
||
Frontend (Vite/React) → Supabase API
|
||
↓
|
||
Cloudflare Pages
|
||
```
|
||
|
||
**Mudança importante:** O sistema **não usa mais Netlify Functions**. Toda comunicação é direta entre frontend e Supabase via services (`authService`, `userService`, `patientService`, etc.).
|
||
|
||
---
|
||
|
||
## 🚀 Guias de Início Rápido
|
||
|
||
**Primeira vez rodando o projeto?**
|
||
|
||
### Instalação Rápida (5 minutos)
|
||
|
||
```powershell
|
||
# 1. Instalar dependências
|
||
pnpm install
|
||
|
||
# 2. Iniciar servidor de desenvolvimento
|
||
pnpm dev
|
||
|
||
# 3. Acessar http://localhost:5173
|
||
```
|
||
|
||
### Build e Deploy
|
||
|
||
```powershell
|
||
# Build de produção
|
||
pnpm build
|
||
|
||
# Deploy para Cloudflare
|
||
npx wrangler pages deploy dist --project-name=mediconnect --branch=production
|
||
```
|
||
|
||
📚 **Documentação completa:** Veja o [README principal](../README.md) com arquitetura, API e serviços.
|
||
|
||
---
|
||
|
||
## ⚠️ SISTEMA DE AUTENTICAÇÃO E PERMISSÕES
|
||
|
||
### 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)
|
||
|
||
### Interceptors Automáticos
|
||
|
||
```typescript
|
||
// Adiciona token automaticamente em todas as requisições
|
||
axios.interceptors.request.use((config) => {
|
||
const token = localStorage.getItem("access_token");
|
||
if (token) {
|
||
config.headers.Authorization = `Bearer ${token}`;
|
||
}
|
||
return config;
|
||
});
|
||
|
||
// Refresh automático quando token expira
|
||
axios.interceptors.response.use(
|
||
(response) => response,
|
||
async (error) => {
|
||
if (error.response?.status === 401) {
|
||
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**
|
||
- ✅ Podem criar/editar agendamentos
|
||
|
||
---
|
||
|
||
## 📡 API e Serviços
|
||
|
||
### Estrutura de Services
|
||
|
||
O projeto usa uma arquitetura de **services** que encapsulam toda comunicação com o Supabase:
|
||
|
||
```
|
||
src/services/
|
||
├── api/
|
||
│ ├── client.ts # Cliente HTTP (Axios configurado)
|
||
│ └── config.ts # Configurações da API
|
||
├── auth/
|
||
│ ├── authService.ts # Login, signup, recuperação de senha
|
||
│ └── types.ts
|
||
├── users/
|
||
│ └── userService.ts # getUserInfo, createUser, deleteUser
|
||
├── patients/
|
||
│ └── patientService.ts # CRUD de pacientes
|
||
├── doctors/
|
||
│ └── doctorService.ts # CRUD de médicos
|
||
├── appointments/
|
||
│ └── appointmentService.ts # Agendamentos
|
||
└── availability/
|
||
└── availabilityService.ts # Disponibilidade médica
|
||
```
|
||
|
||
### Principais Endpoints
|
||
|
||
#### 🔐 Autenticação (authService)
|
||
|
||
```typescript
|
||
// Login
|
||
await authService.login({ email, password });
|
||
// Retorna: { access_token, refresh_token, user }
|
||
|
||
// Recuperação de senha
|
||
await authService.requestPasswordReset(email);
|
||
// Envia email com link de reset
|
||
|
||
// Atualizar senha
|
||
await authService.updatePassword(accessToken, newPassword);
|
||
// Usado na página /reset-password
|
||
|
||
// Refresh token
|
||
await authService.refreshToken(refreshToken);
|
||
```
|
||
|
||
#### 👤 Usuários (userService)
|
||
|
||
```typescript
|
||
// Buscar informações do usuário autenticado (com roles)
|
||
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'
|
||
});
|
||
|
||
// Deletar usuário
|
||
await userService.deleteUser(userId);
|
||
```
|
||
|
||
#### 🏥 Pacientes (patientService)
|
||
|
||
```typescript
|
||
// Listar pacientes
|
||
const patients = await patientService.list();
|
||
|
||
// Buscar por 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",
|
||
});
|
||
|
||
// Atualizar paciente
|
||
await patientService.update(id, { phone_mobile: "11888888888" });
|
||
|
||
// Deletar paciente
|
||
await patientService.delete(id);
|
||
```
|
||
|
||
#### 👨⚕️ Médicos (doctorService)
|
||
|
||
```typescript
|
||
// Listar médicos
|
||
const doctors = await doctorService.list();
|
||
|
||
// Buscar por ID
|
||
const doctor = await doctorService.getById(id);
|
||
|
||
// Buscar disponibilidade
|
||
const slots = await doctorService.getAvailableSlots(doctorId, date);
|
||
```
|
||
|
||
#### 📅 Agendamentos (appointmentService)
|
||
|
||
```typescript
|
||
// Listar agendamentos (filtrado por role automaticamente)
|
||
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",
|
||
});
|
||
|
||
// Atualizar status
|
||
await appointmentService.updateStatus(id, "confirmed");
|
||
// Status: requested, confirmed, completed, cancelled
|
||
|
||
// Cancelar
|
||
await appointmentService.cancel(id, "Motivo do cancelamento");
|
||
```
|
||
|
||
---
|
||
|
||
## 🔧 Configuração da API
|
||
|
||
```typescript
|
||
// src/services/api/config.ts
|
||
export const API_CONFIG = {
|
||
SUPABASE_URL: "https://yuanqfswhberkoevtmfr.supabase.co",
|
||
SUPABASE_ANON_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||
AUTH_URL: `${SUPABASE_URL}/auth/v1`,
|
||
REST_URL: `${SUPABASE_URL}/rest/v1`,
|
||
APP_URL: "https://mediconnectbrasil.app",
|
||
TIMEOUT: 30000,
|
||
STORAGE_KEYS: {
|
||
ACCESS_TOKEN: "mediconnect_access_token",
|
||
REFRESH_TOKEN: "mediconnect_refresh_token",
|
||
USER: "mediconnect_user",
|
||
},
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## 🚀 Deploy no Cloudflare Pages
|
||
|
||
### Via Wrangler CLI
|
||
|
||
```powershell
|
||
# Build
|
||
pnpm build
|
||
|
||
# Deploy para production
|
||
npx wrangler pages deploy dist --project-name=mediconnect --branch=production
|
||
|
||
# Deploy com mudanças não commitadas
|
||
npx wrangler pages deploy dist --project-name=mediconnect --commit-dirty=true
|
||
```
|
||
|
||
### Configuração do Projeto
|
||
|
||
- **Production Branch:** `production`
|
||
- **Build Command:** `pnpm build`
|
||
- **Build Output:** `dist`
|
||
- **Custom Domain:** `mediconnectbrasil.app`
|
||
|
||
### URLs
|
||
|
||
- **Production:** https://mediconnectbrasil.app/
|
||
- **Preview:** https://mediconnect-5oz.pages.dev/
|
||
- **Branch Preview:** https://[branch].mediconnect-5oz.pages.dev/
|
||
|
||
---
|
||
|
||
## 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`) |
|
||
|
||
**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.
|
||
|
||
Boas práticas:
|
||
|
||
- Nunca exponha Service Role Key no frontend.
|
||
- Não comitar `.env` – usar `.env.example` como referência (se houver).
|
||
|
||
---
|
||
|
||
## 2. Fluxo de Login e Validação de Roles
|
||
|
||
### Exemplo: Login de Médico
|
||
|
||
```typescript
|
||
// pages/LoginMedico.tsx
|
||
const handleLogin = async (e: FormEvent) => {
|
||
e.preventDefault();
|
||
|
||
try {
|
||
// 1. Autenticar com Supabase
|
||
const loginResponse = await authService.login({ email, password });
|
||
|
||
// 2. Salvar tokens
|
||
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 || [];
|
||
|
||
// 4. Validar permissões
|
||
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;
|
||
}
|
||
|
||
// 5. Redirecionar para o painel
|
||
navigate("/painel-medico");
|
||
} catch (error) {
|
||
toast.error("Email ou senha incorretos");
|
||
}
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Recuperação de Senha
|
||
|
||
### Fluxo Completo
|
||
|
||
1. **Usuário clica em "Esqueceu a senha?"**
|
||
2. **Sistema envia email com link de recuperação**
|
||
3. **Usuário clica no link → redireciona para `/reset-password`**
|
||
4. **Sistema extrai token do URL (#access_token=...)**
|
||
5. **Usuário define nova senha**
|
||
6. **Sistema atualiza senha e redireciona para login**
|
||
|
||
### Implementação
|
||
|
||
```typescript
|
||
// 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");
|
||
} catch (error) {
|
||
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");
|
||
|
||
if (token) {
|
||
setAccessToken(token);
|
||
setIsLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
const handleSubmit = async (e: FormEvent) => {
|
||
try {
|
||
await authService.updatePassword(accessToken, newPassword);
|
||
toast.success("Senha atualizada com sucesso!");
|
||
navigate("/login-paciente");
|
||
} catch (error) {
|
||
toast.error("Erro ao atualizar senha");
|
||
}
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Estrutura do Banco de Dados
|
||
|
||
### Tabelas Principais
|
||
|
||
#### `profiles`
|
||
|
||
```sql
|
||
- id (uuid, PK)
|
||
- email (text, unique)
|
||
- full_name (text)
|
||
- phone (text)
|
||
- created_at (timestamp)
|
||
- updated_at (timestamp)
|
||
```
|
||
|
||
#### `patients`
|
||
|
||
```sql
|
||
- id (uuid, PK, FK -> profiles)
|
||
- email (text, unique)
|
||
- full_name (text)
|
||
- cpf (text, unique)
|
||
- phone_mobile (text)
|
||
- birth_date (date)
|
||
- address (text)
|
||
- created_at (timestamp)
|
||
```
|
||
|
||
#### `doctors`
|
||
|
||
```sql
|
||
- id (uuid, PK, FK -> profiles)
|
||
- email (text, unique)
|
||
- full_name (text)
|
||
- specialty (text)
|
||
- crm (text, unique)
|
||
- phone (text)
|
||
- created_at (timestamp)
|
||
```
|
||
|
||
#### `appointments`
|
||
|
||
```sql
|
||
- id (uuid, PK)
|
||
- patient_id (uuid, FK -> patients)
|
||
- doctor_id (uuid, FK -> doctors)
|
||
- scheduled_at (timestamp)
|
||
- status (enum: requested, confirmed, completed, cancelled)
|
||
- reason (text)
|
||
- notes (text)
|
||
- created_at (timestamp)
|
||
```
|
||
|
||
#### `user_roles`
|
||
|
||
```sql
|
||
- user_id (uuid, FK -> profiles)
|
||
- role (text: admin, gestor, medico, secretaria, paciente)
|
||
- created_at (timestamp)
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Armazenamento de Tokens
|
||
|
||
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 |
|
||
|
||
**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
|
||
|
||
Riscos remanescentes:
|
||
|
||
- XSS ainda pode ler refresh token dentro da mesma aba.
|
||
- Ataques supply-chain podem capturar tokens em runtime.
|
||
|
||
Mitigações planejadas:
|
||
|
||
1. CSP + bloqueio de inline script não autorizado.
|
||
2. Auditoria de dependências e lockfile imutável.
|
||
3. (Opcional) Migrar refresh para cookie httpOnly + rotacionamento curto (exige backend/proxy).
|
||
|
||
Fallback / Migração:
|
||
|
||
- Em primeira utilização o `tokenStore` migra chaves legacy (`authToken`, `refreshToken`, `authUser`) e remove do `localStorage`.
|
||
|
||
Operações:
|
||
|
||
- `tokenStore.setTokens(access, refresh?)` atualiza memória e session.
|
||
- `tokenStore.clear()` remove tudo (usado em logout e erro crítico de refresh).
|
||
|
||
Fluxo de Refresh:
|
||
|
||
1. Requisição falha com 401.
|
||
2. Wrapper (`http.ts`) obtém refresh do `tokenStore`.
|
||
|
||
---
|
||
|
||
## 7. Scripts Utilitários
|
||
|
||
### Gerenciamento de Usuários
|
||
|
||
```bash
|
||
# Listar todos os usuários
|
||
node scripts/manage-users.js list
|
||
|
||
# Criar usuário
|
||
node scripts/manage-users.js create email@example.com "Nome Completo"
|
||
|
||
# Deletar usuário
|
||
node scripts/manage-users.js delete user-id
|
||
|
||
# Limpar usuários de teste
|
||
node scripts/cleanup-users.js
|
||
```
|
||
|
||
### Testes de API
|
||
|
||
```bash
|
||
# Testar recuperação de senha
|
||
node test-password-recovery.js
|
||
|
||
# Criar usuário Fernando (exemplo)
|
||
node create-fernando.cjs
|
||
|
||
# Buscar usuário Fernando
|
||
node search-fernando.cjs
|
||
```
|
||
|
||
---
|
||
|
||
## 8. Padrões de Código
|
||
|
||
### Nomenclatura
|
||
|
||
- **Componentes:** PascalCase (`LoginPaciente.tsx`)
|
||
- **Serviços:** camelCase (`authService.ts`)
|
||
- **Hooks:** camelCase com prefixo `use` (`useAuth.ts`)
|
||
- **Tipos:** PascalCase (`LoginInput`, `AuthUser`)
|
||
|
||
### Estrutura de Componentes
|
||
|
||
```typescript
|
||
// Imports
|
||
import React, { useState, useEffect } from "react";
|
||
import { useNavigate } from "react-router-dom";
|
||
import { serviceImport } from "../services";
|
||
|
||
// Types
|
||
interface Props {
|
||
// ...
|
||
}
|
||
|
||
// Component
|
||
const ComponentName: React.FC<Props> = ({ ...props }) => {
|
||
// Hooks
|
||
const navigate = useNavigate();
|
||
const [state, setState] = useState();
|
||
|
||
// Effects
|
||
useEffect(() => {
|
||
// ...
|
||
}, []);
|
||
|
||
// Handlers
|
||
const handleAction = async () => {
|
||
// ...
|
||
};
|
||
|
||
// Render
|
||
return <div>{/* JSX */}</div>;
|
||
};
|
||
|
||
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
|
||
- **React Router** 6.30.1 - Roteamento
|
||
- **Tailwind CSS** 3.4.17 - Estilização
|
||
- **Axios** 1.12.2 - Cliente HTTP
|
||
- **React Hot Toast** 2.4.1 - Notificações
|
||
- **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
|
||
|
||
---
|
||
|
||
## 10. Suporte e Contato
|
||
|
||
- **Equipe:** Squad 18 - Rise Up
|
||
- **Repositório:** https://git.popcode.com.br/RiseUP/riseup-squad18.git
|
||
- **Trello:** [Squad 18 - Idealização/Planejamento](https://trello.com/b/CCl3Azxk/squad-18-idealizacao-planejamento)
|
||
|
||
---
|
||
|
||
**Desenvolvido com ❤️ pela Squad 18**
|