15 KiB
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)
# 1. Instalar dependências
pnpm install
# 2. Iniciar servidor de desenvolvimento
pnpm dev
# 3. Acessar http://localhost:5173
Build e Deploy
# 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 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
// 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)
// 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)
// 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)
// 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)
// 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)
// 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
// 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
# 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.examplecomo referência (se houver).
2. Fluxo de Login e Validação de Roles
Exemplo: Login de Médico
// 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
- Usuário clica em "Esqueceu a senha?"
- Sistema envia email com link de recuperação
- Usuário clica no link → redireciona para
/reset-password - Sistema extrai token do URL (#access_token=...)
- Usuário define nova senha
- Sistema atualiza senha e redireciona para login
Implementação
// 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
- id (uuid, PK)
- email (text, unique)
- full_name (text)
- phone (text)
- created_at (timestamp)
- updated_at (timestamp)
patients
- 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
- id (uuid, PK, FK -> profiles)
- email (text, unique)
- full_name (text)
- specialty (text)
- crm (text, unique)
- phone (text)
- created_at (timestamp)
appointments
- 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
- 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:
- CSP + bloqueio de inline script não autorizado.
- Auditoria de dependências e lockfile imutável.
- (Opcional) Migrar refresh para cookie httpOnly + rotacionamento curto (exige backend/proxy).
Fallback / Migração:
- Em primeira utilização o
tokenStoremigra chaves legacy (authToken,refreshToken,authUser) e remove dolocalStorage.
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:
- Requisição falha com 401.
- Wrapper (
http.ts) obtém refresh dotokenStore.
7. Scripts Utilitários
Gerenciamento de Usuários
# 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
# 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
// 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
Desenvolvido com ❤️ pela Squad 18