..
2025-10-24 12:03:40 -03:00
2025-10-24 12:03:40 -03:00
2025-10-24 12:03:40 -03:00
2025-10-21 13:02:56 -03:00
2025-10-07 14:53:47 -03:00
2025-10-24 12:03:40 -03:00
2025-10-22 22:28:22 -03:00
2025-10-07 14:53:47 -03:00
2025-10-24 12:03:40 -03:00
2025-10-22 22:28:22 -03:00
2025-10-22 22:28:22 -03:00
2025-10-07 14:53:47 -03:00
2025-10-07 14:53:47 -03:00
2025-10-24 12:03:40 -03:00
2025-10-24 12:03:40 -03:00
2025-10-07 14:53:47 -03:00
2025-10-07 14:53:47 -03:00
2025-10-07 14:53:47 -03:00
2025-10-12 20:18:58 -03:00
2025-10-12 20:18:58 -03:00

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


🏗️ 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


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

// 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

// 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:

  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

# 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


Desenvolvido com ❤️ pela Squad 18