667 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# MediConnect - Sistema de Agendamento Médico
Aplicação SPA (React + Vite + TypeScript) consumindo **Supabase** (Auth, PostgREST, Storage) 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
↓ ├── Auth (JWT)
Cloudflare Pages ├── PostgREST (PostgreSQL)
└── Storage (Avatares)
```
**Mudança importante:** O sistema **não usa mais Netlify Functions**. Toda comunicação é direta entre frontend e Supabase via services (`authService`, `userService`, `patientService`, `avatarService`, 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");
```
#### 📸 Avatares (avatarService)
```typescript
// Upload de avatar (usa FormData com x-upsert: true)
const file = event.target.files[0]; // File do input
const result = await avatarService.upload({
userId: user.id,
file: file,
});
// Retorna: { Key: "url-publica-do-avatar" }
// Obter URL pública do avatar
const url = avatarService.getPublicUrl({
userId: user.id,
ext: "png", // ou 'jpg', 'webp'
});
// Retorna: https://yuanqfswhberkoevtmfr.supabase.co/storage/v1/object/avatars/{userId}/avatar.png
// Auto-load de avatar (testa múltiplas extensões)
const extensions = ["png", "jpg", "webp"];
for (const ext of extensions) {
const url = avatarService.getPublicUrl({ userId: user.id, ext });
const response = await fetch(url, { method: "HEAD" });
if (response.ok) {
setAvatarUrl(url);
break;
}
}
```
**Detalhes importantes:**
- Upload usa **multipart/form-data** via FormData
- Header `x-upsert: true` permite sobrescrever avatares existentes
- Suporta formatos: PNG, JPG, WEBP (máx 2MB)
- URLs públicas não requerem autenticação
- Avatar é carregado automaticamente nos painéis
---
## 🔧 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**