chore: Reorganize project file structure.
29
.gitignore
vendored
@ -1,2 +1,29 @@
|
||||
node_modules/
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.tsriseup-squad20/
|
||||
susconecta/riseup-squad20/
|
||||
riseup-squad20/
|
||||
|
||||
370
IMPLEMENTACAO_OPCAO2_VINCULO_EMAIL.md
Normal file
@ -0,0 +1,370 @@
|
||||
# ✅ Implementação: Opção 2 - Vínculo por Email
|
||||
|
||||
## 🎯 Solução Implementada
|
||||
|
||||
**Vínculo entre Supabase Auth e API Mock através do EMAIL**
|
||||
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ Supabase Auth │
|
||||
│ (Login/Autenticação) │
|
||||
│ │
|
||||
│ email: "user@email.com"│ ◄─┐
|
||||
│ password: "senha123!" │ │
|
||||
│ userType: "paciente" │ │
|
||||
└──────────────────────────┘ │
|
||||
│ VÍNCULO
|
||||
┌──────────────────────────┐ │ POR EMAIL
|
||||
│ API Mock (Apidog) │ │
|
||||
│ (Dados do Sistema) │ │
|
||||
│ │ │
|
||||
│ email: "user@email.com"│ ◄─┘
|
||||
│ full_name: "João Silva"│
|
||||
│ cpf: "123.456.789-00" │
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Código Implementado
|
||||
|
||||
### `lib/api.ts` - Funções de Criação de Usuários
|
||||
|
||||
```typescript
|
||||
import { ENV_CONFIG } from '@/lib/env-config';
|
||||
import { API_KEY } from '@/lib/config';
|
||||
|
||||
// Gera senha aleatória (formato: senhaXXX!)
|
||||
export function gerarSenhaAleatoria(): string {
|
||||
const num1 = Math.floor(Math.random() * 10);
|
||||
const num2 = Math.floor(Math.random() * 10);
|
||||
const num3 = Math.floor(Math.random() * 10);
|
||||
return `senha${num1}${num2}${num3}!`;
|
||||
}
|
||||
|
||||
// Cria usuário MÉDICO no Supabase Auth
|
||||
export async function criarUsuarioMedico(medico: {
|
||||
email: string;
|
||||
full_name: string;
|
||||
phone_mobile: string;
|
||||
}): Promise<CreateUserWithPasswordResponse> {
|
||||
const senha = gerarSenhaAleatoria();
|
||||
|
||||
// Endpoint do Supabase Auth (mesmo que auth.ts usa)
|
||||
const signupUrl = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/signup`;
|
||||
|
||||
const payload = {
|
||||
email: medico.email, // ◄── VÍNCULO!
|
||||
password: senha,
|
||||
data: {
|
||||
userType: 'profissional',
|
||||
full_name: medico.full_name,
|
||||
phone: medico.phone_mobile,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await fetch(signupUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
apikey: API_KEY,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Erro ao criar usuário: ${response.status}`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
user: responseData.user,
|
||||
email: medico.email,
|
||||
password: senha,
|
||||
};
|
||||
}
|
||||
|
||||
// Cria usuário PACIENTE no Supabase Auth
|
||||
export async function criarUsuarioPaciente(paciente: {
|
||||
email: string;
|
||||
full_name: string;
|
||||
phone_mobile: string;
|
||||
}): Promise<CreateUserWithPasswordResponse> {
|
||||
const senha = gerarSenhaAleatoria();
|
||||
|
||||
const signupUrl = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/signup`;
|
||||
|
||||
const payload = {
|
||||
email: paciente.email, // ◄── VÍNCULO!
|
||||
password: senha,
|
||||
data: {
|
||||
userType: 'paciente',
|
||||
full_name: paciente.full_name,
|
||||
phone: paciente.phone_mobile,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await fetch(signupUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
apikey: API_KEY,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Erro ao criar usuário: ${response.status}`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
user: responseData.user,
|
||||
email: paciente.email,
|
||||
password: senha,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Fluxo Completo
|
||||
|
||||
### 1️⃣ **Admin Cadastra Paciente**
|
||||
|
||||
```typescript
|
||||
// components/forms/patient-registration-form.tsx
|
||||
|
||||
async function handleSubmit() {
|
||||
// 1. Salva paciente na API Mock
|
||||
const saved = await salvarPaciente({
|
||||
full_name: form.nome,
|
||||
email: form.email, // ◄── EMAIL usado como vínculo
|
||||
cpf: form.cpf,
|
||||
telefone: form.telefone,
|
||||
// ...outros dados
|
||||
});
|
||||
|
||||
// 2. Cria usuário no Supabase Auth com MESMO EMAIL
|
||||
if (mode === 'create' && form.email) {
|
||||
try {
|
||||
const credentials = await criarUsuarioPaciente({
|
||||
email: form.email, // ◄── MESMO EMAIL!
|
||||
full_name: form.nome,
|
||||
phone_mobile: form.telefone,
|
||||
});
|
||||
|
||||
// 3. Mostra popup com credenciais
|
||||
setCredentials(credentials);
|
||||
setShowCredentials(true);
|
||||
} catch (error) {
|
||||
alert('Paciente cadastrado, mas houve erro ao criar usuário de acesso');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2️⃣ **Paciente Faz Login**
|
||||
|
||||
```typescript
|
||||
// Paciente vai em /login-paciente
|
||||
// Digita: email = "jonas@email.com", password = "senha481!"
|
||||
|
||||
// hooks/useAuth.tsx
|
||||
const login = async (email, password, userType) => {
|
||||
// Autentica no Supabase Auth
|
||||
const response = await loginUser(email, password, userType);
|
||||
|
||||
// Token JWT contém o email
|
||||
const token = response.access_token;
|
||||
const decoded = decodeJWT(token);
|
||||
console.log(decoded.email); // "jonas@email.com"
|
||||
|
||||
// Salva sessão
|
||||
localStorage.setItem('token', token);
|
||||
|
||||
// Redireciona para /paciente
|
||||
router.push('/paciente');
|
||||
};
|
||||
```
|
||||
|
||||
### 3️⃣ **Buscar Dados do Paciente na Área Logada**
|
||||
|
||||
```typescript
|
||||
// app/paciente/page.tsx
|
||||
|
||||
export default function PacientePage() {
|
||||
const { user } = useAuth(); // user.email = "jonas@email.com"
|
||||
|
||||
useEffect(() => {
|
||||
async function carregarDados() {
|
||||
// Busca paciente pelo EMAIL (vínculo)
|
||||
const response = await fetch(
|
||||
`https://mock.apidog.com/pacientes?email=${user.email}`
|
||||
);
|
||||
const paciente = await response.json();
|
||||
|
||||
// Agora tem os dados completos do paciente
|
||||
console.log(paciente);
|
||||
// {
|
||||
// id: "123",
|
||||
// full_name: "Jonas Francisco",
|
||||
// email: "jonas@email.com", ◄── VÍNCULO!
|
||||
// cpf: "123.456.789-00",
|
||||
// ...
|
||||
// }
|
||||
}
|
||||
|
||||
carregarDados();
|
||||
}, [user.email]);
|
||||
|
||||
return <div>Bem-vindo, {user.email}!</div>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Estrutura dos Dados
|
||||
|
||||
### **Supabase Auth (`auth.users`)**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"email": "jonas@email.com",
|
||||
"encrypted_password": "$2a$10$...",
|
||||
"created_at": "2025-10-03T00:00:00",
|
||||
"user_metadata": {
|
||||
"userType": "paciente",
|
||||
"full_name": "Jonas Francisco",
|
||||
"phone": "(79) 99649-8907"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **API Mock - Tabela `pacientes`**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "123",
|
||||
"full_name": "Jonas Francisco Nascimento Bonfim",
|
||||
"email": "jonas@email.com",
|
||||
"cpf": "123.456.789-00",
|
||||
"telefone": "(79) 99649-8907",
|
||||
"data_nascimento": "1990-01-15",
|
||||
"endereco": {
|
||||
"cep": "49000-000",
|
||||
"logradouro": "Rua Principal",
|
||||
"cidade": "Aracaju"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Vínculo:** Campo `email` presente em ambos os sistemas!
|
||||
|
||||
---
|
||||
|
||||
## 📊 Vantagens da Opção 2
|
||||
|
||||
✅ **Simples:** Não precisa modificar estrutura da API Mock
|
||||
✅ **Natural:** Email já é único e obrigatório
|
||||
✅ **Sem duplicação:** Usa campo existente
|
||||
✅ **Funcional:** Supabase Auth retorna email no token JWT
|
||||
✅ **Escalável:** Fácil de manter e debugar
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Considerações Importantes
|
||||
|
||||
### **Email DEVE ser único**
|
||||
|
||||
- Cada email só pode ter um usuário no Supabase Auth
|
||||
- Cada email só pode ter um paciente/médico na API Mock
|
||||
- Se tentar cadastrar email duplicado, Supabase retorna erro
|
||||
|
||||
### **Email DEVE ser válido**
|
||||
|
||||
- Supabase valida formato (nome@dominio.com)
|
||||
- Use validação no formulário antes de enviar
|
||||
|
||||
### **Formato da senha**
|
||||
|
||||
- Supabase exige mínimo 6 caracteres
|
||||
- Geramos: `senhaXXX!` (10 caracteres) ✅
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Logs Esperados (Sucesso)
|
||||
|
||||
```
|
||||
🏥 [CRIAR PACIENTE] Iniciando criação no Supabase Auth...
|
||||
📧 Email: jonas@email.com
|
||||
👤 Nome: Jonas Francisco Nascimento Bonfim
|
||||
📱 Telefone: (79) 99649-8907
|
||||
🔑 Senha gerada: senha481!
|
||||
📤 [CRIAR PACIENTE] Enviando para: https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/signup
|
||||
📋 [CRIAR PACIENTE] Status da resposta: 200 OK
|
||||
✅ [CRIAR PACIENTE] Usuário criado com sucesso no Supabase Auth!
|
||||
🆔 User ID: 550e8400-e29b-41d4-a716-446655440000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ❌ Possíveis Erros
|
||||
|
||||
### **Erro: "Este email já está cadastrado no sistema"**
|
||||
|
||||
```
|
||||
❌ [CRIAR PACIENTE] Erro: User already registered
|
||||
```
|
||||
|
||||
**Solução:** Email já existe no Supabase. Use outro email ou delete o usuário existente.
|
||||
|
||||
### **Erro: "Formato de email inválido"**
|
||||
|
||||
```
|
||||
❌ [CRIAR PACIENTE] Erro: Invalid email format
|
||||
```
|
||||
|
||||
**Solução:** Valide o formato do email antes de enviar.
|
||||
|
||||
### **Erro: 429 - Too Many Requests**
|
||||
|
||||
```
|
||||
❌ [CRIAR PACIENTE] Erro: 429
|
||||
```
|
||||
|
||||
**Solução:** Aguarde 1 minuto. Supabase limita taxa de requisições.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Resultado Final
|
||||
|
||||
**Agora o sistema funciona assim:**
|
||||
|
||||
1. ✅ Admin cadastra paciente → Salva na API Mock
|
||||
2. ✅ Sistema cria usuário no Supabase Auth (mesmo email)
|
||||
3. ✅ Popup mostra credenciais (email + senha)
|
||||
4. ✅ Paciente faz login em `/login-paciente`
|
||||
5. ✅ Login funciona! Token JWT é gerado
|
||||
6. ✅ Sistema busca dados do paciente pelo email
|
||||
7. ✅ Paciente acessa área `/paciente` com todos os dados
|
||||
|
||||
---
|
||||
|
||||
## 📅 Data da Implementação
|
||||
|
||||
3 de outubro de 2025
|
||||
|
||||
## 🔗 Arquivos Modificados
|
||||
|
||||
- ✅ `susconecta/lib/api.ts` - Funções de criação (Supabase Auth)
|
||||
- ✅ `susconecta/components/forms/patient-registration-form.tsx` - Já integrado
|
||||
- ✅ `susconecta/components/forms/doctor-registration-form.tsx` - Já integrado
|
||||
- ✅ `susconecta/components/credentials-dialog.tsx` - Já implementado
|
||||
9483
package-lock.json
generated
92
package.json
@ -1,9 +1,91 @@
|
||||
{
|
||||
"name": "my-v0-project",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"dev": "next dev",
|
||||
"lint": "next lint",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.7",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"react-big-calendar": "^1.19.4",
|
||||
"react-signature-canvas": "^1.1.0-alpha.2"
|
||||
"@fullcalendar/core": "^6.1.19",
|
||||
"@fullcalendar/daygrid": "^6.1.19",
|
||||
"@fullcalendar/interaction": "^6.1.19",
|
||||
"@fullcalendar/react": "^6.1.19",
|
||||
"@fullcalendar/timegrid": "^6.1.19",
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@radix-ui/react-accordion": "latest",
|
||||
"@radix-ui/react-alert-dialog": "latest",
|
||||
"@radix-ui/react-aspect-ratio": "latest",
|
||||
"@radix-ui/react-avatar": "latest",
|
||||
"@radix-ui/react-checkbox": "latest",
|
||||
"@radix-ui/react-collapsible": "latest",
|
||||
"@radix-ui/react-context-menu": "latest",
|
||||
"@radix-ui/react-dialog": "latest",
|
||||
"@radix-ui/react-dropdown-menu": "latest",
|
||||
"@radix-ui/react-hover-card": "latest",
|
||||
"@radix-ui/react-label": "latest",
|
||||
"@radix-ui/react-menubar": "latest",
|
||||
"@radix-ui/react-navigation-menu": "latest",
|
||||
"@radix-ui/react-popover": "latest",
|
||||
"@radix-ui/react-progress": "latest",
|
||||
"@radix-ui/react-radio-group": "latest",
|
||||
"@radix-ui/react-scroll-area": "latest",
|
||||
"@radix-ui/react-select": "latest",
|
||||
"@radix-ui/react-separator": "latest",
|
||||
"@radix-ui/react-slider": "latest",
|
||||
"@radix-ui/react-slot": "latest",
|
||||
"@radix-ui/react-switch": "latest",
|
||||
"@radix-ui/react-tabs": "latest",
|
||||
"@radix-ui/react-toast": "latest",
|
||||
"@radix-ui/react-toggle": "latest",
|
||||
"@radix-ui/react-toggle-group": "latest",
|
||||
"@radix-ui/react-tooltip": "latest",
|
||||
"@vercel/analytics": "1.3.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "latest",
|
||||
"date-fns": "4.1.0",
|
||||
"embla-carousel-react": "latest",
|
||||
"geist": "^1.3.1",
|
||||
"input-otp": "latest",
|
||||
"jspdf": "^3.0.3",
|
||||
"lucide-react": "^0.454.0",
|
||||
"next-themes": "latest",
|
||||
"react": "^18",
|
||||
"react-day-picker": "latest",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "latest",
|
||||
"react-quill": "^2.0.0",
|
||||
"react-resizable-panels": "latest",
|
||||
"react-signature-canvas": "^1.1.0-alpha.2",
|
||||
"recharts": "latest",
|
||||
"sonner": "latest",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "latest",
|
||||
"zod": "3.25.67"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@tailwindcss/postcss": "^4.1.9",
|
||||
"@types/node": "^22",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@typescript-eslint/eslint-plugin": "^8.45.0",
|
||||
"@typescript-eslint/parser": "^8.45.0",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-config-next": "^15.5.4",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-unicorn": "^61.0.2",
|
||||
"next": "^15.5.4",
|
||||
"postcss": "^8.5",
|
||||
"tailwindcss": "^4.1.9",
|
||||
"tw-animate-css": "1.3.3",
|
||||
"typescript": "^5",
|
||||
"typescript-eslint": "^8.45.0"
|
||||
}
|
||||
}
|
||||
|
||||
0
susconecta/pnpm-lock.yaml → pnpm-lock.yaml
generated
|
Before Width: | Height: | Size: 801 KiB After Width: | Height: | Size: 801 KiB |
|
Before Width: | Height: | Size: 8.3 MiB After Width: | Height: | Size: 8.3 MiB |
|
Before Width: | Height: | Size: 568 B After Width: | Height: | Size: 568 B |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |