# 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