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
|
||||||
9485
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": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.7",
|
"@fullcalendar/core": "^6.1.19",
|
||||||
"@heroicons/react": "^2.2.0",
|
"@fullcalendar/daygrid": "^6.1.19",
|
||||||
"date-fns": "^4.1.0",
|
"@fullcalendar/interaction": "^6.1.19",
|
||||||
"react-big-calendar": "^1.19.4",
|
"@fullcalendar/react": "^6.1.19",
|
||||||
"react-signature-canvas": "^1.1.0-alpha.2"
|
"@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 |