Sessoes 6cont-10: hardening em 6 areas + scan completo do SaaS

Continuacao de 7c20b51. Esta etapa fechou TODA revisao senior do SaaS
(15 areas auditadas) + refator parcial de pacientes.

Ver commit.md para descricao completa por sessao.

# Estado final do projeto
- A# auditoria abertos: 1 (A#31 Deploy real)
- V# verificacoes abertos: 14 (todos medios/baixos adiados com plano)
- Criticos: 0
- Altos: 0
- Vitest: 208/208 (era 192, +16 nos novos composables)
- SQL integration: 33/33
- E2E (Playwright): 5/5
- Areas auditadas: 15

# Highlights
- Documentos 100% fechado (V#50/51/52: portal-paciente policy + content_sha256 + 4 cron jobs retention)
- Tenants V#1 P0: tenant_invites com RLS off + 0 policies (mesmo padrao A#30)
- Calendario 100% fechado: feriados WITH CHECK
- Addons V#1 P0 (dinheiro): addon_transactions WITH CHECK saas_admin
- Central SaaS V#1: faq write so saas_admin (era tenant_admin)
- Servicos/Prontuarios 100% fechado: services/medicos/insurance_plans + cascades
- Pacientes V#9: 2 composables novos (useCep, usePatientSupportContacts) + repo estendido + script extraido (template intocado, fica para quando houver E2E)

# 8 migrations novas neste commit
- 20260419000011_documents_portal_patient_policy.sql
- 20260419000012_documents_content_hash.sql
- 20260419000013_cron_retention_jobs.sql
- 20260419000014_financial_security_hardening.sql
- 20260419000015_communication_security_hardening.sql
- 20260419000016_tenants_calendario_hardening.sql
- 20260419000017_addons_central_saas_hardening.sql
- 20260419000018_servicos_prontuarios_hardening.sql

Total acumulado: 18 migrations (Sessoes 1-10).

# A#31 reformulado pra proxima sessao
"Deploy real" muda escopo: como nao ha cloud Supabase nem secrets reais
ainda (MVP), proxima sessao vira "Preparacao completa pra deploy" (DEPLOY.md,
validar migrations num container limpo, audit edge functions, listar env vars,
script db.cjs deploy-check).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-04-19 22:00:06 -03:00
parent 7c20b518d4
commit d6eb992f71
18 changed files with 1699 additions and 313 deletions
+135 -196
View File
@@ -1,237 +1,176 @@
# Sessões 1-6 acumuladas — hardening, defesa em profundidade, +192 testes
# Sessões 6 (continuação) → 10 — hardening em 6 áreas + scan completo do SaaS
Repositório estava sem commit há ~5 sessões de trabalho intenso. Este commit
consolida tudo que foi feito desde o último marco (`d088a89`).
Continuação do commit `7c20b51` (Sessões 1-6 iniciais). Esta etapa fechou
**toda revisão sênior do SaaS** + refator parcial de pacientes.
**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.
**Estado final do projeto:**
- A# auditoria abertos: **1** (A#31 Deploy real)
- V# verificações abertos: 14 (todos médios/baixos adiados com plano completo no DB)
- 🔴 Críticos: **0**
- 🟠 Altos: **0**
- Vitest: **208/208** (era 192)
- SQL integration: **33/33**
- E2E (Playwright): **5/5**
- Áreas auditadas: **15** (todas as principais do SaaS)
---
## Sessão 1 — auth/router/session
## Sessão 6 (continuação) — Documentos pendentes + Pacientes V#3
- A#7 resolvido (`window.__guardsBound`)
- 10 verificações registradas (V#1V#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`
### Documentos: 100% fechado (V#50, V#51, V#52)
- **V#50** — Policy `documents: portal patient read` adicional. Paciente lê documento via portal quando `compartilhado_portal=true` AND patient pertence a auth.uid AND não expirou.
- **V#51** — `documents.content_sha256` (nullable, índice parcial). `Documents.service.uploadDocument` calcula SHA-256 hex client-side via `crypto.subtle.digest`. Helper novo `verifyDocumentIntegrity(docId)` baixa arquivo e re-hash.
- **V#52** — Migration `...13` cron retention via pg_cron: 4 jobs (document_access_logs 1 ano, math_challenges 1h, public_submission_attempts 90 dias, submission_rate_limits 30 dias).
### Pacientes V#3 (parcial — fundação)
- `src/features/patients/services/patientsRepository.js` — list/get/create/update/softDelete + groups + tags + getSessionCounts.
- `src/features/patients/composables/usePatients.js` — wrapper reativo (rows/loading/error).
- PatientsListPage.hydrateAssociationsSupabase migrada — substitui 4 queries diretas por chamadas ao repo (paralelismo preservado).
- V#9 (PatientsCadastroPage 1991 linhas) → adiado pra Sessão 10.
---
## Sessão 2agenda
## Sessão 7Tenants + Calendário
- 11 verificações (V#11V#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
### Tenants (8 V#)
- 🔴 **V#1 P0**`tenant_invites` com RLS off + 0 policies (mesmo padrão A#30 Sessão 5). Tabela tinha 0 rows. Migration: ENABLE RLS + 4 policies (SELECT tenant_admin/saas; INSERT WITH CHECK invited_by=auth.uid; UPDATE só revogação; DELETE tenant_admin/saas). Aceitar invite continua via RPC `tenant_accept_invite` SECURITY DEFINER.
- 🟠 **V#2** profiles INSERT WITH CHECK (id = auth.uid)
- 🟠 **V#3** support_sessions INSERT WITH CHECK (admin_id = auth.uid + saas_admin guard)
- 🟡 **V#4 (signup público)** verificado: RPC `ensure_personal_tenant` SECURITY DEFINER já existia (Signup.vue:232) → **ok**
- 🟡 **V#5 (accept_invite)** verificado: RPCs `tenant_accept_invite` + `tenant_invite_member_by_email` já existiam → **ok**
- 🟡 **V#6** user_settings INSERT WITH CHECK
- 🟢 V#7/V#8 baixos — adiados
### Calendário (2 V#) — 100% fechado
- 🔴 **V#1** feriados_insert + feriados_saas_insert ganharam WITH CHECK. Spam de feriado global bloqueado.
- 🟢 **V#2** feriados_delete agora permite tenant_admin (não só owner).
---
## Sessão 3pacientes
## Sessão 8Addons + Central SaaS
- 10 verificações (V#22V#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
### Addons (4 V#)
- 🔴 **V#1 P0 (dinheiro)**`addon_transactions_admin_insert` ganhou WITH CHECK (EXISTS saas_admins). Edge functions com service_role bypassam RLS, pipeline preservado. **Authenticated comum não cria mais transação fake.**
- 🟠 **V#2** — 3 CHECK constraints em `addon_credits`: balance >= 0, total_consumed >= 0, total_purchased >= 0. Saldo negativo silencioso eliminado.
- 🟡 V#3 (UI extrato) — adiado.
- 🟡 V#4 — verificado: `addon_products` não tem `tenant_id` (catálogo global por design) → **ok**.
### Central SaaS (3 V#)
- 🟠 **V#1**`faq_admin_write` substituído por `faq_saas_admin_write` em `saas_faq` E `saas_faq_itens` — só saas_admin escreve. Tenant_admin lê via `faq_auth_read` (permanece).
- 🟢 V#2/V#3 médios/baixos — adiados.
---
## Sessão 4security review (página pública de cadastro)
## Sessão 9Serviços/Prontuários (100% fechado)
- V#31 virou security review completa: **15 vulnerabilidades (A#15A#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
5/5 V# corrigidos:
- 🔴 **V#1** services + insurance_plans → 4 policies separadas (SELECT tenant_member; INSERT/UPDATE/DELETE owner+saas).
- 🔴 **V#2** medicos4 policies separadas (catálogo de médicos referenciadores compartilhado entre profissionais do tenant).
- 🟠 **V#3** commitment_services — cascade reescrito via JOIN com services (USING permite tenant_member; WITH CHECK só owner).
- 🟠 **V#4** insurance_plan_services — cascade reescrito via JOIN com insurance_plans.
- 🟡 **V#5** commitment_time_logs/determined_commitments/determined_commitment_fields ganharam WITH CHECK em INSERT.
---
## Sessão 5SaaS (planos, preços, recursos)
## Sessão 10Pacientes V#9 (script extraído)
- 10 verificações (V#33V#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
PatientsCadastroPage.vue: 1991 → 1951 linhas (qualitativo > quantitativo).
### 2 composables novos
- **`useCep.js`** — busca ViaCEP reutilizável. 6 testes (sem rede, mock fetch).
- **`usePatientSupportContacts.js`** — CRUD de contatos de suporte encapsulado (load/save/add/remove/iniciaisFor). 10 testes com builder thenable.
### patientsRepository estendido
- `getPatientRelations(patientId)` — retorna {groupIds, tagIds}
- `replacePatientGroup(patientId, groupId, {tenantId})`
- `replacePatientTags(patientId, tagIds, {tenantId, ownerId})`
### PatientsCadastroPage refatorado
- 8 funções de query → delegação 1-linha ao patientsRepository
- onCepBlur → usa composable useCep
- Contatos de suporte → composable
- Template **não** foi tocado (zero risco de regressão visual)
- Quebra de template em sub-componentes Vue → adiado pra quando houver E2E cobrindo a página
---
## 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)
## 📦 Migrations consolidadas neste commit (8)
```
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)
20260419000011_documents_portal_patient_policy.sql (V#50)
20260419000012_documents_content_hash.sql (V#51)
20260419000013_cron_retention_jobs.sql (V#52 + math_challenges + submissions + rate_limits)
20260419000014_financial_security_hardening.sql (5 V# financeiro — fechados na Sessão 6)
20260419000015_communication_security_hardening.sql (5 V# comunicação — fechados na Sessão 6)
20260419000016_tenants_calendario_hardening.sql (Tenants V#1-V#3,V#6 + Calendário V#1-V#2)
20260419000017_addons_central_saas_hardening.sql (Addons V#1-V#2 + Central SaaS V#1)
20260419000018_servicos_prontuarios_hardening.sql (Serviços V#1-V#5)
```
**Total acumulado de migrations no histórico: 18** (Sessões 1-10).
Várias dessas exigiram conexão direta como `supabase_admin` (ver memory `project_supabase_admin_gotcha.md` e `commit.md` anterior) por causa de tabelas owned por esse role.
---
## 🆕 Novos arquivos (código)
```
src/features/patients/composables/useCep.js
src/features/patients/composables/usePatientSupportContacts.js
src/features/patients/composables/usePatients.js
src/features/patients/composables/__tests__/useCep.spec.js (+6 testes)
src/features/patients/composables/__tests__/usePatientSupportContacts.spec.js (+10 testes)
src/features/patients/services/patientsRepository.js
```
---
## 🆕 Pastas/arquivos novos importantes
## 🛠️ Modificações
- `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
- `src/features/patients/PatientsListPage.vue` — hydrateAssociationsSupabase usa repo
- `src/features/patients/cadastro/PatientsCadastroPage.vue` — script extraído (queries → repo, CEP → composable, contatos → composable). Template intocado.
- `src/services/Documents.service.js` — uploadDocument calcula content_sha256 + helper verifyDocumentIntegrity
---
## 🛠️ .gitignore ajustado neste commit
## 📊 Áreas auditadas (estado final)
- `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 |
| Área | V# total | Estado |
|---|---|---|
| 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`) |
| auth | 10 | 100% fechado/ok |
| router | 9 | 100% |
| stores | 1 | 100% |
| agenda | 11 | 100% |
| pacientes | 10 | **100% fechado** |
| seguranca | 1 | 100% |
| saas | 10 | 100% |
| documentos | 10 | **100% fechado** |
| financeiro | 11 | 5 fechados, 6 médios/baixos adiados |
| comunicacao | 10 | 5 fechados, 5 médios/baixos adiados |
| tenants | 8 | 6 fechados, 2 baixos adiados |
| calendario | 2 | **100% fechado** ✅ |
| addons | 4 | 3 resolvidos, 1 médio adiado |
| central_saas | 3 | 1 alto fechado, 2 médios adiados |
| servicos | 5 | **100% fechado** ✅ |
**Zero crítico/alto restante no sistema inteiro.**
---
## ⚠️ Adiados (próximas sessões — plano completo no DB)
## ⚠️ Pendências documentadas no DB (não esquecidas)
- **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
### A# (1 aberto)
- **A#31 Deploy real** — alto. Reformulação pendente: como ainda não há cloud Supabase nem secrets reais, próxima sessão é "Preparação completa pra deploy" (DEPLOY.md, validar migrations num container limpo, audit de edge functions, listar env vars, script `db.cjs deploy-check`).
### V# adiados (14)
Todos médios/baixos com plano completo em `dev_verificacoes_items.acao_sugerida`:
- financeiro (6): parcelamento CHECK, payouts flow, recurrence DELETE policy, composables, máscara PIX, dashboard inadimplência
- comunicacao (5): notifications/schedules silos, email_templates_global filtros, retention notification_logs, dashboard health, audit dismissals/preferences
- tenants (2): owner_users policies, company_profiles + dev_user_credentials
- central_saas (2): rate limit voto, valores tipo_acesso
- addons (1): UI de extrato
### Outros
- PatientsCadastroPage template breakdown — quando houver E2E
- Pacientes V#9 segue 100% no banco (script foi extraído; template é refator visual separado)