# 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://.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 }) => { // Hooks const navigate = useNavigate(); const [state, setState] = useState(); // Effects useEffect(() => { // ... }, []); // Handlers const handleAction = async () => { // ... }; // Render return
{/* JSX */}
; }; 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**