diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index f26aa21..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(node:*)", - "Bash(powershell:*)", - "Bash(grep:*)", - "Bash(cd \"D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai\" && sed -i \\\\\n 's/console\\\\.time\\(tlabel\\)/const _perfEnd = logPerf\\('\\\\''router.guard'\\\\'', tlabel\\)/g' \\\\\n src/router/guards.js && echo \"console.time substituído\")", - "Bash(cd \"D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai\" && sed -i \\\\\n 's/console\\\\.timeEnd\\(tlabel\\)/_perfEnd\\(\\)/g' \\\\\n src/router/guards.js && echo \"console.timeEnd substituído\")", - "Bash(cd \"D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai\" && npm install --save-dev vitest @vitest/ui 2>&1 | tail -5)", - "Bash(find \"D:\\\\leonohama\\\\AgenciaPsi.com.br\\\\Sistema\\\\agenciapsi-primesakai/DBS\" -name \"*.sql\" -type f 2>/dev/null | head -10)", - "Bash(find \"D:\\\\leonohama\\\\AgenciaPsi.com.br\\\\Sistema\\\\agenciapsi-primesakai/DBS\" -name \"*.sql\" -type f 2>/dev/null | xargs grep -l \"agenda_eventos\" | head -3)", - "Bash(find \"D:\\\\leonohama\\\\AgenciaPsi.com.br\\\\Sistema\\\\agenciapsi-primesakai/DBS/2026-03-11\" -name \"*.sql\" -type f 2>/dev/null | head -3)", - "Bash(find \"D:\\\\leonohama\\\\AgenciaPsi.com.br\\\\Sistema\\\\agenciapsi-primesakai\" -type f -name \"*.sql\" 2>/dev/null | xargs grep -l \"agenda_eventos\" 2>/dev/null | head -5)", - "Bash(find /d/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/src -name \"*[Pp]ricing*\" -o -name \"*[Pp]reco*\" -o -name \"*[Vv]alor*\" 2>/dev/null | head -20)", - "Bash(where python:*)", - "Bash(cd \"/d/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai\" && C:/Users/lmnohama/AppData/Local/Programs/Python/Python310/python.exe -c \"\nimport zipfile\nimport xml.etree.ElementTree as ET\n\nfor fname in ['spec-wizard.docx', 'spec-v2.docx']:\n print\\('=== ' + fname + ' ==='\\)\n try:\n with zipfile.ZipFile\\(fname, 'r'\\) as z:\n with z.open\\('word/document.xml'\\) as f:\n tree = ET.parse\\(f\\)\n root = tree.getroot\\(\\)\n texts = []\n for para in root.iter\\('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}p'\\):\n parts = []\n for t in para.iter\\('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}t'\\):\n if t.text:\n parts.append\\(t.text\\)\n line = ''.join\\(parts\\)\n texts.append\\(line\\)\n print\\('\\\\n'.join\\(texts\\)\\)\n except Exception as e:\n print\\('Error: ' + str\\(e\\)\\)\n print\\(\\)\n\")", - "Bash(C:/Users/lmnohama/AppData/Local/Programs/Python/Python310/python.exe -c \"\nimport sys\nwith open\\('/d/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/DBS/2026-03-12/schema.sql', 'r', encoding='utf-8'\\) as f:\n lines = f.readlines\\(\\)\nprint\\(f'Total lines: {len\\(lines\\)}'\\)\n\" 2>&1)", - "Bash(C:/Users/lmnohama/AppData/Local/Programs/Python/Python310/python.exe -c \"\nimport sys\nfpath = 'D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/DBS/2026-03-12/schema.sql'\nwith open\\(fpath, 'r', encoding='utf-8'\\) as f:\n lines = f.readlines\\(\\)\nsys.stdout.buffer.write\\(\\('Total lines: ' + str\\(len\\(lines\\)\\) + '\\\\n'\\).encode\\('utf-8'\\)\\)\n\" 2>&1)", - "Bash(find /d/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai -type f \\\\\\( -name \"*convenio*\" -o -name \"*Convenio*\" \\\\\\) 2>/dev/null | head -20)", - "Bash(find:*)", - "Bash(ls:*)", - "Bash(npx vite:*)", - "Bash(powershell -Command \"$content = [System.IO.File]::ReadAllText\\(''src/views/pages/clinic/clinic/ClinicFeaturesPage.vue'', [System.Text.Encoding]::UTF8\\); $content = $content -replace [char]0x201C, ''\"\"'' -replace [char]0x201D, ''\"\"''; [System.IO.File]::WriteAllText\\(''src/views/pages/clinic/clinic/ClinicFeaturesPage.vue'', $content, [System.Text.Encoding]::UTF8\\)\")", - "Bash(xargs cat:*)", - "Bash(xxd \"D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/DBS/2026-03-21/schema.sql\")", - "Bash(iconv -f UTF-16LE -t UTF-8 \"D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/DBS/2026-03-21/schema.sql\")", - "Bash(mkdir -p docs/billing docs/planos docs/subscription-health docs/estrategia docs/specs)", - "Bash(mkdir -p database/backups database/migrations database/seeds database/fixes database/snippets)", - "Bash(cd:*)", - "Bash(mv disparando-whatsapp-local.md docs/whatsapp.md)", - "Bash(mv comandos.txt docs/)", - "Bash(mv dados-padrões-da-agenda.txt docs/)", - "Bash(mv USER_ARCHETYPES.html docs/)", - "Bash(mv Novo-DB/migration_*.sql database/migrations/)", - "Bash(mv Novo-DB/seed_*.sql database/seeds/)", - "Bash(mv Novo-DB/fix_*.sql database/fixes/)", - "Bash(npm install:*)", - "Bash(cp:*)", - "Bash(npm uninstall:*)", - "Bash(rm:*)", - "Bash(docker ps:*)", - "Bash(docker exec:*)", - "Bash(\"D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/database-novo/backups/2026-03-23/schema.sql\")", - "Bash(\"D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/database-novo/backups/2026-03-23/data.sql\")", - "Bash(\"D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/database-novo/backups/2026-03-23/full_dump.sql\")", - "Bash(wc:*)", - "Bash(python _wizard_patch.py)", - "Bash(rm _wizard_patch.py)", - "Bash(npm ls:*)", - "Bash(find /d/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/src/features/patients -type f \\\\\\(-name *.vue -o -name *.js \\\\\\))" - ] - } -} diff --git a/.gitignore b/.gitignore index 3a4237e..1dd5a6c 100644 --- a/.gitignore +++ b/.gitignore @@ -14,5 +14,16 @@ dist-*/ api-generator/typedoc.json **/.DS_Store Dev-documentacao/ -supabase/ +supabase/* +!supabase/functions/ evolution-api/ + +# Backups locais do banco — não comitar (regeneráveis via db.cjs backup) +database-novo/backups/ + +# Outputs do Playwright +test-results/ +playwright-report/ + +# Config local do Claude Code (cada dev tem o seu) +.claude/settings.local.json diff --git a/DOCKER-SETUP.md b/DOCKER-SETUP.md new file mode 100644 index 0000000..4fa0d62 --- /dev/null +++ b/DOCKER-SETUP.md @@ -0,0 +1,138 @@ +# Docker Setup — Projetos Locais + +## Tabela Resumo + +| Projeto | Container(s) | Porta Host | Rede | Volume(s) | +|---|---|---|---|---| +| **AgenciaPsi** | `agenciapsi_app` | `5173` → Vite dev | `agenciapsi_net` | `agenciapsi_node_modules` | +| | `agenciapsi_mysql` | `3307` → MySQL | `agenciapsi_net` | `agenciapsi_mysql_data` | +| **Evolution API** | `evolution_api` | `8080` → API | `agenciapsi_net` (external) | — | +| | `evolution_db` | interno | `agenciapsi_net` | `evolution_db_data` | +| | `evolution_redis` | interno | `agenciapsi_net` | — | +| | `evolution_mailpit` | `1025` SMTP / `8025` Web | `agenciapsi_net` | — | +| **Supabase AgenciaPsi** | `supabase_*_agenciapsi-primesakai` | `54321` API / `54322` PG / `54323` Studio | — | volumes internos | +| **Sakai-Vue** | `sakaivue_app` | `5174` → Vite dev | `sakaivue_net` | `sakaivue_node_modules` | +| | `sakaivue_mysql` | `3308` → MySQL | `sakaivue_net` | `sakaivue_mysql_data` | +| **Supabase Sakai-Vue** | `supabase_*_sakai-vue` | `54331` API / `54332` PG / `54333` Studio | — | volumes internos | +| **Gisaf Local** | `gisaf_mysql` | `3309` → MySQL | `gisaf_net` | `gisaf_mysql_data` | + +## Mapa de Portas + +| Porta | Serviço | +|---|---| +| 3307 | AgenciaPsi MySQL | +| 3308 | Sakai-Vue MySQL | +| 3309 | Gisaf MySQL | +| 5173 | AgenciaPsi Vite dev | +| 5174 | Sakai-Vue Vite dev | +| 8080 | Evolution API | +| 1025 | Mailpit SMTP | +| 8025 | Mailpit Web UI | +| 54321 | Supabase AgenciaPsi — Kong (API) | +| 54322 | Supabase AgenciaPsi — PostgreSQL | +| 54323 | Supabase AgenciaPsi — Studio | +| 54327 | Supabase AgenciaPsi — Analytics | +| 54331 | Supabase Sakai-Vue — Kong (API) | +| 54332 | Supabase Sakai-Vue — PostgreSQL | +| 54333 | Supabase Sakai-Vue — Studio | +| 54337 | Supabase Sakai-Vue — Analytics | + +## Ordem de Start + +```bash +# 1. AgenciaPsi (cria a rede agenciapsi_net) +cd "D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai" +docker compose up -d + +# 2. Supabase AgenciaPsi (porta 54321) +cd "D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai" +npx supabase start + +# 3. Evolution API (depende da rede agenciapsi_net) +cd "D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/evolution-api" +docker compose up -d + +# 4. Sakai-Vue +cd "D:/leonohama/UniaoApp.com.br/Sistema/sakai-vue" +docker compose up -d + +# 5. Supabase Sakai-Vue (porta 54331) +cd "D:/leonohama/UniaoApp.com.br/Sistema/sakai-vue" +npx supabase start + +# 6. Gisaf Local +cd "D:/leonohama/UniaoApp.com.br/Gisaf Local" +docker compose up -d +``` + +## Parar tudo + +```bash +# Na ordem inversa +cd "D:/leonohama/UniaoApp.com.br/Gisaf Local" && docker compose down +cd "D:/leonohama/UniaoApp.com.br/Sistema/sakai-vue" && npx supabase stop +cd "D:/leonohama/UniaoApp.com.br/Sistema/sakai-vue" && docker compose down +cd "D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai/evolution-api" && docker compose down +cd "D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai" && npx supabase stop +cd "D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai" && docker compose down +``` + +## Caminhos dos docker-compose.yml + +| Projeto | Caminho | +|---|---| +| AgenciaPsi | `D:\leonohama\AgenciaPsi.com.br\Sistema\agenciapsi-primesakai\docker-compose.yml` | +| Evolution API | `D:\leonohama\AgenciaPsi.com.br\Sistema\agenciapsi-primesakai\evolution-api\docker-compose.yml` | +| Sakai-Vue | `D:\leonohama\UniaoApp.com.br\Sistema\sakai-vue\docker-compose.yml` | +| Gisaf Local | `D:\leonohama\UniaoApp.com.br\Gisaf Local\docker-compose.yml` | + +## DBeaver — Conexões MySQL + +| Conexão | Host | Port | Database | User | Password | +|---|---|---|---|---|---| +| Gisaf | `localhost` | `3309` | `sindsp` | `sindsp` | `marlboro` | +| AgenciaPsi | `localhost` | `3307` | `agenciapsi` | `agenciapsi` | `agenciapsi123` | +| Sakai-Vue | `localhost` | `3308` | `sakaivue` | `sakaivue` | `sakaivue123` | + +Para criar cada conexão: **Database → New Database Connection → MySQL → preencher dados → Test Connection → Finish** + +## Supabase — Instancias Locais + +Cada projeto tem sua propria instancia Supabase (schemas diferentes, nao podem compartilhar). + +| Projeto | API URL | Studio | PostgreSQL | Anon Key | +|---|---|---|---|---| +| AgenciaPsi | `http://127.0.0.1:54321` | `http://127.0.0.1:54323` | `127.0.0.1:54322` | `sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH` | +| Sakai-Vue | `http://127.0.0.1:54331` | `http://127.0.0.1:54333` | `127.0.0.1:54332` | `sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH` | + +**Resetar banco (aplica migrations + seed):** + +```bash +# AgenciaPsi +cd "D:/leonohama/AgenciaPsi.com.br/Sistema/agenciapsi-primesakai" +npx supabase db reset + +# Sakai-Vue +cd "D:/leonohama/UniaoApp.com.br/Sistema/sakai-vue" +npx supabase db reset +``` + +### Sakai-Vue — Usuarios de teste + +| Email | Senha | Role | +|---|---|---| +| `dev@sistema.com.br` | `Dev@12345` | dev | +| `master@tenant.com.br` | `Master@12345` | master | +| `admin@tenant.com.br` | `Admin@12345` | admin | +| `chefe@tenant.com.br` | `Chefe@12345` | chefe_setor | +| `servidor@tenant.com.br` | `Servidor@12345` | servidor | +| `leitura@tenant.com.br` | `Leitura@12345` | leitura | + +## Importar dump SQL no Gisaf + +```bash +# Via CLI (já feito) +docker exec -i gisaf_mysql mysql -usindsp -pmarlboro sindsp < "D:/leonohama/UniaoApp.com.br/Gisaf Local/Dump20260330.sql" +``` + +Ou via DBeaver: conectar no banco `sindsp` → **Tools → Execute SQL Script** → selecionar `Dump20260330.sql` diff --git a/HANDOFF.md b/HANDOFF.md new file mode 100644 index 0000000..d24b21c --- /dev/null +++ b/HANDOFF.md @@ -0,0 +1,174 @@ +# HANDOFF — 2026-04-19 (Sessão 6) + +Documento de continuidade. Quando você voltar, comece lendo esta página. Todo o trabalho está registrado no banco (`/saas/desenvolvimento` → **Verificações**, **Auditoria**, **Testes**) — este arquivo é só o mapa. + +--- + +## 📊 Estado atual + +| Tipo | Aberto | Notas | +|---|---|---| +| **A# auditoria** | **0** | ✅ todas as 30 resolvidas | +| **V# verificações** | **9** | auth(3), pacientes(3), saas(2), router(1) | +| **T# testes** | **2 a escrever** | T#9 useAgendaEvents wrapper, T#10 E2E | +| **Áreas não auditadas** | **3** | financeiro, comunicação, documentos/prontuários | +| **Migrations não commitadas** | **8** | Sessão 6 (ver lista abaixo) | +| **Vitest** | **179/179** | 8 suites | +| **SQL integration tests** | **33/33** | `database-novo/tests/run.cjs` | + +--- + +## ✅ Sessão 6 (hoje, 2026-04-19) — resumo + +### Bloco 1 — V#34 + V#41 (Opção B2: plano + override + exceção comercial) +- **Migration** `20260419000001_tenant_features_b2_governance.sql`: + - Trigger `tenant_features_guard_with_plan` ganhou bypass via session flag (`current_setting('app.allow_feature_exception')`) + - Nova RPC `set_tenant_feature_exception(tenant_id, feature_key, enabled, reason)` SECURITY DEFINER com regras assimétricas: + - `enabled=false` → tenant_admin OU saas_admin (preferência do cliente) + - `enabled=true` AND plano permite → tenant_admin OU saas_admin + - `enabled=true` AND plano NÃO permite → **só saas_admin + reason obrigatório** (exceção comercial) + - Policy `tenant_features_write_saas_only` — writes diretos só saas_admin +- **Store** `tenantFeaturesStore.isEnabled` reescrito (B2): override negativo desliga, override positivo liga, sem override segue plano +- **Store** `setForTenant` agora chama RPC com `reason` opcional +- **UI nova** `/saas/tenant-features` (selector de tenant, catálogo, dialog com `reason` obrigatório p/ exceção, log de mudanças) +- **JSDoc** documentando separação semântica (`entitlementsStore.has` = "plano permite?" vs `tenantFeaturesStore.isEnabled` = "ativo agora?") +- **Testes** `src/stores/__tests__/tenantFeaturesStore.spec.js` — 17 cenários incluindo regressão V#34 + +### Bloco 2 — Pendentes Sessão 5 +- **V#42** — `entitlementsStore.loadFor*` no catch agora NÃO marca como carregado (estado fica como "not loaded" → próximo request retenta) + `logError` adicionado +- **V#40** — `features.is_active` (migration `...02`) + UI: soft delete + filtro "Mostrar depreciados" + Tag de status + botão Reativar +- **V#36** — RPC `delete_plan_safe` (migration `...03`): bloqueia DELETE se houver subscriptions ativas. SaasPlansPage migrada +- **V#35** — Migration `...04`: 17 → 11 policies. Removidas 3 read-auth duplicadas em plans/features/plan_features + 3 subsets/no-ops em subscriptions. `COMMENT ON POLICY` em todas + +### Bloco 3 — Testes T#5 / T#7 / T#8 +- **T#5** `tenantStore.spec.js` — 15 testes: singleflight, regressão V#5 (não herdar tenant de outro user), erros, setActiveTenant, reset, getters. Stub localStorage in-memory (env=node sem jsdom) +- **T#7** `validators.spec.js` — 38 testes: sanitização do intake (digitsOnly, CPF/CNPJ, phone, email, CEP, toISODate) +- **T#8** `database-novo/tests/run.cjs` — runner Node + docker exec. **33 cenários SQL** cobrindo set_tenant_feature_exception, delete_plan_safe, intake, features.is_active, defesa em camadas, twilio config + +### Bloco 4 — A#20 (CAPTCHA) — rev2 self-hosted +**Decisão:** descartado Cloudflare Turnstile / hCaptcha em favor de defesa em camadas self-hosted. Razões: zero LGPD, zero provider, zero fricção pro paciente legítimo (UX importa muito em paciente vulnerável buscando atendimento). + +5 camadas: +1. **Honeypot** — campo invisível (frontend), bot rejeita +2. **Validação** básica +3. **Rate limit por IP** — `check_rate_limit` RPC +4. **Math captcha condicional** — só ativa após N falhas do mesmo IP (default 3) +5. **Modo paranoid** global toggle (saas_security_config.captcha_required_globally) + +Implementação: +- Migration `...06` (4 tabelas: `saas_security_config`, `public_submission_attempts`, `submission_rate_limits`, `math_challenges`) +- Migration `...07` (RPCs: `check_rate_limit`, `record_submission_attempt`, `generate_math_challenge`, `verify_math_challenge`, `cleanup_expired_math_challenges`) +- Edge function `submit-patient-intake` reescrita (dual endpoint: submit + `/captcha-challenge`) +- Componente `MathCaptchaChallenge.vue` (lazy) +- Tela `/saas/security` com card explicativo (6 seções), KPIs 24h, toggles, sliders, dashboard de IPs ativos, modo paranoid + +### Bloco 5 — SaaS Twilio Config (operacional editável) +- Migration `...08` (tabela `saas_twilio_config` singleton + RPCs `get_twilio_config` / `update_twilio_config`) +- **Decisão de segurança**: `TWILIO_AUTH_TOKEN` permanece em env var (único secret); SID/webhook/rate/margem migram pra DB +- Edge function `twilio-whatsapp-provision` lê do banco com fallback pra env (back-compat) +- Tela `/saas/twilio-config` com card explicativo + status do AUTH_TOKEN (ping pra detectar se está setado) +- **Bug fix**: `friendlyErrorMessage()` em `twilioWhatsappService.js` traduz "Edge Function returned a non-2xx status code" pra mensagens contextuais + +--- + +## 🛑 Pendências (V# abertos por área) + +### auth (3) +- **V#2** (médio) — Listener `supabase.auth.onAuthStateChange` duplicado +- **V#6** (baixo) — `globalRole` cache sem TTL e sem invalidação por realtime +- **V#10** (médio) — Bloqueio SaaS em tenant-app só por `startsWith` de path (frágil) + +### pacientes (3) +- **V#3** (médio) — Pacientes não tem composables/services — toda lógica em pages +- **V#8** (baixo) — `agenda_eventos.select` patient_id com `limit 1000` arbitrário +- **V#9** (médio) — `PatientsCadastroPage` com **1985 linhas** (precisa quebrar) + +### router (1) +- **V#9** (baixo) — `ensureMenuBuilt` roda em toda navegação autenticada + +### saas (2) +- **V#17** (baixo) — 23 `console.*` em páginas SaaS +- **V#18** (baixo) — `tenantFeaturesStore` sem TTL real (cache pode ficar stale) + +--- + +## 🗂️ Migrations criadas hoje (ordem cronológica) + +``` +database-novo/migrations/ +├── 20260419000001_tenant_features_b2_governance.sql (V#34/V#41) +├── 20260419000002_features_is_active.sql (V#40) +├── 20260419000003_delete_plan_safe.sql (V#36) +├── 20260419000004_consolidate_policies.sql (V#35) +├── 20260419000005_restrict_intake_rpc.sql (A#20 — REVOKE anon) +├── 20260419000006_layered_bot_defense.sql (A#20 rev2 — schema) +├── 20260419000007_bot_defense_rpcs.sql (A#20 rev2 — RPCs) +└── 20260419000008_saas_twilio_config.sql (Twilio config) +``` + +Todas aplicadas no banco local. **Se for pra cloud, aplicar nessa ordem.** + +--- + +## 🗜️ Arquivos criados/modificados (código) + +**Criados:** +``` +src/components/security/MathCaptchaChallenge.vue +src/views/pages/saas/SaasTenantFeaturesPage.vue +src/views/pages/saas/SaasSecurityPage.vue +src/views/pages/saas/SaasTwilioConfigPage.vue +src/stores/__tests__/tenantStore.spec.js +src/stores/__tests__/tenantFeaturesStore.spec.js +src/utils/__tests__/validators.spec.js +database-novo/tests/run.cjs +supabase/functions/submit-patient-intake/index.js (reescrita) +``` + +**Modificados:** +``` +src/stores/tenantFeaturesStore.js (lógica B2 + RPC) +src/stores/entitlementsStore.js (V#42 fix + JSDoc) +src/services/twilioWhatsappService.js (friendlyErrorMessage) +src/views/pages/saas/SaasFeaturesPage.vue (V#40 soft delete) +src/views/pages/saas/SaasPlansPage.vue (V#36 RPC) +src/views/pages/saas/SaasTwilioWhatsappPage.vue (toast warn em vez de error) +src/views/pages/clinic/clinic/ClinicFeaturesPage.vue (texto erro plano-denied) +src/views/pages/public/CadastroPacienteExterno.vue (honeypot + math captcha) +src/router/routes.saas.js (3 rotas novas) +src/navigation/menus/saas.menu.js (3 itens menu novos) +supabase/functions/twilio-whatsapp-provision/index.ts (lê config DB) +.env (limpo Turnstile vars) +``` + +--- + +## 📊 Números finais + +| Métrica | Antes (Sessão 5) | Hoje | +|---|---|---| +| A# auditoria | 30 (1 aberto) | 30 (0 abertos) | +| V# verificações | 42 (15 abertos) | 42 (9 abertos) | +| Suites de teste vitest | 6 (109 tests) | 8 (179 tests) | +| Suites SQL integration | 0 | 1 (33 tests) | +| Migrations totais | 5 (Sessão 5) | 13 (+8 hoje) | +| Telas SaaS novas | — | 3 (`/tenant-features`, `/security`, `/twilio-config`) | + +--- + +## 🎯 Ordem sugerida quando voltar + +1. **Decidir se commita** o trabalho da Sessão 6 (~30 arquivos, 8 migrations). +2. **Continuar Sessão 6** (em andamento): A+B+C+D escolhidos, faltando concluir B/C/D. +3. Outras opções: + - **Nova área de revisão sênior** — financeiro / comunicação / documentos + - **Deploy real** (Supabase cloud + secrets + edge functions) + - **Testes T#9 + T#10** + +--- + +## ⚠️ Nada commitado (ainda) + +Working directory tem ~30 arquivos modificados desde Sessão 5. Revise com `git status` + `git diff` antes de decidir o que comitar. + +**Nada quebrou:** vitest 179/179 + SQL 33/33. diff --git a/commit.md b/commit.md new file mode 100644 index 0000000..515ce0e --- /dev/null +++ b/commit.md @@ -0,0 +1,237 @@ +# Sessões 1-6 acumuladas — hardening, defesa em profundidade, +192 testes + +Repositório estava sem commit há ~5 sessões de trabalho intenso. Este commit +consolida tudo que foi feito desde o último marco (`d088a89`). + +**A# auditoria:** 30 itens registrados, **0 abertos**. +**V# verificações:** 52 registradas (10 novas em Documentos), **5 abertas** (todas adiadas com plano). +**T# testes:** 10/10 escritas. **192 vitest + 33 SQL + 5 E2E** passando. + +--- + +## Sessão 1 — auth/router/session + +- A#7 resolvido (`window.__guardsBound`) +- 10 verificações registradas (V#1–V#10), 5 corrigidas: + - `session.js` logger inconsistente + - `tenantStore.ensureLoaded` polling + - `normalizeRole` duplicado → extraído pra `src/utils/roleNormalizer.js` + - `console.error` em `router/index.js` + - `.single()` em `fetchRole` + +--- + +## Sessão 2 — agenda + +- 11 verificações (V#11–V#21), 10 corrigidas: + - `useRecurrence` CRUD ganhou filtro `tenant_id` (alto) + - `agenda.service.js` vazio deletado + - `agendaRepository` ↔ `useAgendaEvents` consolidados (composable virou wrapper fino, 181→67 linhas) + - `AGENDA_EVENT_SELECT` centralizado em `agendaSelects.js` + - `_tenantGuards.js` compartilhado + - V#21 status `remarcar` → `remarcado` padronizado em 14 edições + +--- + +## Sessão 3 — pacientes + +- 10 verificações (V#22–V#31), 6 corrigidas + 4 documentadas +- 5 arquivos obsoletos deletados (PatientsCadastroPage Bkp, preview, prontuário design 1/2/3) +- `tenant_id` em todas queries de patients (alto) +- 9 `console.*` migrados pra logger +- `hydrateAssociations` paralelizada (5 round-trips → 2) +- `.maybeSingle()` onde precisava + +--- + +## Sessão 4 — security review (página pública de cadastro) + +- V#31 virou security review completa: **15 vulnerabilidades (A#15–A#29), 14 corrigidas** +- **Críticos:** + - **A#15** bucket `avatars` público → 5MB + mime whitelist + policies restritas + - **A#16** RPC v2 ignorava `active/expires/max_uses` → validação completa + incrementa `uses` + - **A#17** `notas_internas` exposto ao paciente → removido do form e RPC + - **A#18** `Math.random()` pra token → RPCs server-side via `gen_random_uuid()` + - **A#19** intake sem `tenant_id` → RPC resolve via `patient_invites` ou `tenant_members` +- **Médios:** log `patient_invite_attempts` (A#24), política LGPD (A#25), botão mock só em DEV (A#26), length caps server-side (A#27) +- **Baixos:** duplicado `PatientsExternalLinkPage` deletado, `Landingpage-v1 - bkp.vue` deletado + +--- + +## Sessão 5 — SaaS (planos, preços, recursos) + +- 10 verificações (V#33–V#42) +- **🔴 P0:** A#30 — 7 tabelas SaaS com RLS OFF + `GRANT ALL` pra anon. Migration `...05_saas_rls_emergency_fix` aplicou REVOKE + ENABLE RLS + 9 policies corretas +- 109/109 testes passando + +--- + +## Sessão 6 (HOJE, 2026-04-19) — bloco principal + +### V#34 + V#41 — Opção B2 (plano + override + exceção comercial) + +Resolve `tenantFeaturesStore.isEnabled` que retornava `true` por default +(qualquer feature aparecia ativa pra qualquer tenant) E a dupla-fonte com +`entitlementsStore`. + +**Backend** (migration `...01`): +- Trigger `tenant_features_guard_with_plan` ganhou bypass via session flag +- RPC `set_tenant_feature_exception` SECURITY DEFINER com regras assimétricas: + - `enabled=false` → tenant_admin OU saas_admin (preferência) + - `enabled=true` AND plano permite → tenant_admin OU saas_admin + - `enabled=true` AND plano NÃO permite → **só saas_admin + reason obrigatório** +- Policy `tenant_features_write_saas_only` + +**Frontend:** +- `tenantFeaturesStore.isEnabled` reescrito (B2): override negativo desliga, override positivo liga (exceção), sem override segue plano +- `setForTenant` chama RPC com `reason` +- Tela nova `/saas/tenant-features` com dialog de motivo obrigatório +- JSDoc separação semântica: `entitlementsStore.has` = "plano permite?" vs `tenantFeaturesStore.isEnabled` = "ativo agora?" +- 17 testes em `tenantFeaturesStore.spec.js` + +### Pendentes Sessão 5 fechados + +- **V#35** — 17→11 policies (consolidadas plans/features/plan_features/subscriptions) + `COMMENT ON POLICY` +- **V#36** — RPC `delete_plan_safe` bloqueia DELETE com subscriptions ativas +- **V#40** — `features.is_active` (soft delete) + UI com filtro/Reativar +- **V#42** — `entitlementsStore.loadFor*` no catch não marca como carregado + `logError` + +### Testes T#5/T#7/T#8 + +- **T#5** `tenantStore.spec.js` — 15 testes (singleflight, regressão V#5, erros, setActiveTenant, reset, getters) +- **T#7** `validators.spec.js` — 38 testes (sanitização do intake) +- **T#8** `database-novo/tests/run.cjs` — runner Node + docker exec, 33 cenários SQL + +### A#20 (CAPTCHA) — rev2 self-hosted + +**Decisão:** descartado Cloudflare Turnstile / hCaptcha em favor de defesa em +camadas self-hosted. Razões: zero LGPD, zero provider, zero fricção pro paciente +legítimo (UX importa em paciente vulnerável buscando atendimento). + +**5 camadas:** +1. **Honeypot** — campo invisível +2. **Validação** básica +3. **Rate limit por IP** — `check_rate_limit` RPC +4. **Math captcha condicional** — só ativa após N falhas (default 3) +5. **Modo paranoid** global toggle + +**Implementação:** +- Migrations `...06` (4 tabelas) + `...07` (RPCs) +- Edge function `submit-patient-intake` reescrita (dual endpoint) +- Componente `MathCaptchaChallenge.vue` lazy +- Tela `/saas/security` com card explicativo (6 seções), KPIs 24h, toggles, sliders, dashboard de IPs + +### SaaS Twilio Config (UI editável) + +- Migration `...08` (singleton + RPCs `get_twilio_config`/`update_twilio_config`) +- **AUTH_TOKEN permanece em env var** (único secret); SID/webhook/rate/margem migram pra DB +- Edge function lê do banco com fallback pra env (back-compat) +- Tela `/saas/twilio-config` com card + status do AUTH_TOKEN +- Bug fix: `friendlyErrorMessage()` traduz "Edge Function returned a non-2xx status code" + +### Revisão sênior em Documentos/prontuários + +10 V# novas registradas, 7 corrigidas, 3 adiadas. + +**Críticos:** +- 🔴 **V#43/V#44** vazamento entre clínicas via storage policies — corrigido com tenant scoping no path `(storage.foldername(name))[1]::uuid IN tenant_members` +- 🔴 **V#45** documents policy pobre (só `owner_id = auth.uid()`) — separada em SELECT/INSERT/UPDATE/DELETE com tenant scoping + +**Altos:** +- 🟠 **V#46** share_links sem incremento de usos — RPC `validate_share_token` atomicamente valida + incrementa + loga +- 🟠 **V#47** signatures policy ALL — separada (UPDATE só pra signatário) + +**Médios:** +- 🟡 **V#48** access_logs WITH CHECK +- 🟡 **V#49** templates WITH CHECK + +### B-block (V# avulsos) + +- **V#2** Listener `onAuthStateChange` consolidado (session.js virou autoridade + API `onSessionEvent`) +- **V#6** `globalRoleCache` TTL 5min +- **V#10** Bloqueio SaaS via `meta.area`/`meta.saasAdmin` em vez de `path.startsWith` +- **V#8** RPC `get_patient_session_counts` substitui `.limit(1000)` arbitrário +- **V#9 router** short-circuit `lastEnsureKey` em ensureMenuBuilt +- **V#17** 25 `console.*` eliminados em src/views/pages/saas/ +- **V#18** TTL real em tenantFeaturesStore + +### T#9 + T#10 + +- **T#9** `useAgendaEvents.spec.js` — 13 testes do wrapper +- **T#10** Playwright + Chromium instalados; 5 specs E2E em `e2e/patient-intake.spec.js` +- **Bug fix achado pelo E2E**: `CadastroPacienteExterno.enviar` não extraía body do erro 403 — corrigido + +--- + +## 📦 Migrations consolidadas (todas as sessões) + +``` +20260417000001_dev_tables (Sessão pré-1: tabelas dev) +20260417000002_dev_tables_ordem +20260418000001_dev_verificacoes (Sessão 1) +20260418000002_patient_intake_security_hardening (Sessão 4) +20260418000003_patient_invite_attempts_log (Sessão 4) +20260418000004_dev_tests (Sessão 1) +20260418000005_saas_rls_emergency_fix (Sessão 5 — P0) +20260419000001_tenant_features_b2_governance (Sessão 6 — V#34/V#41) +20260419000002_features_is_active (Sessão 6 — V#40) +20260419000003_delete_plan_safe (Sessão 6 — V#36) +20260419000004_consolidate_policies (Sessão 6 — V#35) +20260419000005_restrict_intake_rpc (Sessão 6 — A#20) +20260419000006_layered_bot_defense (Sessão 6 — A#20 rev2) +20260419000007_bot_defense_rpcs (Sessão 6 — A#20 rev2) +20260419000008_saas_twilio_config (Sessão 6) +20260419000009_patient_session_counts_rpc (Sessão 6 — V#8) +20260419000010_documents_security_hardening (Sessão 6 — V#43-V#49) +``` + +--- + +## 🆕 Pastas/arquivos novos importantes + +- `e2e/` — specs Playwright (T#10) +- `playwright.config.js` — config E2E +- `database-novo/tests/run.cjs` — runner SQL integration tests (T#8) +- `database-novo/backups/` agora ignorado (regenerável via `db.cjs backup`) +- `src/components/security/MathCaptchaChallenge.vue` — A#20 rev2 +- `src/views/pages/saas/SaasTenantFeaturesPage.vue` — V#34 +- `src/views/pages/saas/SaasSecurityPage.vue` — A#20 rev2 + card educativo +- `src/views/pages/saas/SaasTwilioConfigPage.vue` — UI Twilio editável +- `src/utils/roleNormalizer.js` — Sessão 1 +- `src/features/agenda/services/_tenantGuards.js` + `agendaSelects.js` — Sessão 2 +- 6 specs novas em `__tests__/` (vitest) +- `supabase/functions/submit-patient-intake/` — edge function reescrita A#20 rev2 + +--- + +## 🛠️ .gitignore ajustado neste commit + +- `supabase/*` + `!supabase/functions/` (mantém edge functions, ignora `.temp/`/`migrations/`/etc gerados pelo CLI) +- `database-novo/backups/` (backups regeneráveis) +- `test-results/`, `playwright-report/` (outputs Playwright) +- `.claude/settings.local.json` (config local do harness) + +--- + +## 📊 Números finais + +| Métrica | Início | Fim | +|---|---|---| +| A# abertos | 30 (a registrar) | **0** | +| V# abertos | 52 (a registrar) | **5** (adiados) | +| T# escritas | 0/10 | **10/10** | +| Vitest | — | **192/192** | +| SQL integration | — | **33/33** | +| E2E (Playwright) | — | **5/5** | +| Migrations | 0 | **17** | +| Telas SaaS novas | — | 3 | +| Edge functions reescritas | — | 1 (`submit-patient-intake`) | + +--- + +## ⚠️ Adiados (próximas sessões — plano completo no DB) + +- **V#3 + V#9 pacientes** — refatoração de composables/services (PatientsCadastroPage 1985 linhas). Sessão dedicada de 1-2h +- **V#50/V#51/V#52 documentos** — portal-paciente policy, hash SHA-256, retention cron +- **Áreas não auditadas:** financeiro, comunicação +- **Deploy real**: cloud Supabase + secrets + edge functions diff --git a/database-novo/agenciapsi-db-dashboard.html b/database-novo/agenciapsi-db-dashboard.html index fda3c58..d1185ca 100644 --- a/database-novo/agenciapsi-db-dashboard.html +++ b/database-novo/agenciapsi-db-dashboard.html @@ -3,487 +3,310 @@
-