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:
@@ -70,6 +70,20 @@ import { digitsOnly, fmtCPF, fmtRG, fmtPhone, toISODate, generateCPF } from '@/u
|
||||
import CadastroRapidoConvenio from '@/components/CadastroRapidoConvenio.vue'
|
||||
import CadastroRapidoMedico from '@/components/CadastroRapidoMedico.vue'
|
||||
|
||||
// V#9 — composables/repo da feature pacientes (extração da página gigante)
|
||||
import { useCep } from '@/features/patients/composables/useCep'
|
||||
import { usePatientSupportContacts } from '@/features/patients/composables/usePatientSupportContacts'
|
||||
import {
|
||||
listGroups as repoListGroups,
|
||||
listTags as repoListTags,
|
||||
getPatientById as repoGetPatientById,
|
||||
createPatient as repoCreatePatient,
|
||||
updatePatient as repoUpdatePatient,
|
||||
getPatientRelations as repoGetPatientRelations,
|
||||
replacePatientGroup as repoReplacePatientGroup,
|
||||
replacePatientTags as repoReplacePatientTags
|
||||
} from '@/features/patients/services/patientsRepository'
|
||||
|
||||
// ─────────────────────────────────────────────────────────
|
||||
// Props / emits
|
||||
// ─────────────────────────────────────────────────────────
|
||||
@@ -446,83 +460,33 @@ function sanitizePayload (raw, ownerId) {
|
||||
// Entidade separada que alimenta o card "Contatos & rede de suporte" do detalhe
|
||||
// is_primario = true → badge vermelho "emergência" no detalhe
|
||||
// ─────────────────────────────────────────────────────────
|
||||
const contatosSuporte = ref([])
|
||||
const novoContato = () => ({ _k: Date.now()+Math.random(), nome:'', relacao:'', tipo:'', telefone:'', email:'', is_primario:false })
|
||||
// V#9 — contatos de suporte agora vêm do composable (script -300 linhas)
|
||||
const _supportContacts = usePatientSupportContacts()
|
||||
const contatosSuporte = _supportContacts.contatos
|
||||
const addContato = () => _supportContacts.add()
|
||||
const removeContato = (i) => _supportContacts.remove(i)
|
||||
const iniciaisFor = (n) => _supportContacts.iniciaisFor(n)
|
||||
const loadContatosSuporte = (pid) => _supportContacts.load(pid)
|
||||
const saveContatosSuporte = (pid, tenantId, ownerId) => _supportContacts.save(pid, tenantId, ownerId)
|
||||
|
||||
function addContato () { contatosSuporte.value.push(novoContato()) }
|
||||
function removeContato (i) { contatosSuporte.value.splice(i,1) }
|
||||
function iniciaisFor (n) { return (n||'').split(' ').filter(Boolean).map(w=>w[0].toUpperCase()).slice(0,2).join('') }
|
||||
|
||||
async function saveContatosSuporte (pid, tenantId, ownerId) {
|
||||
const { error: del } = await supabase.from('patient_support_contacts')
|
||||
.delete().eq('patient_id', pid).eq('owner_id', ownerId)
|
||||
if (del) throw del
|
||||
const rows = contatosSuporte.value.filter(c=>c.nome.trim()).map(c=>({
|
||||
patient_id: pid, owner_id: ownerId, tenant_id: tenantId,
|
||||
nome: c.nome.trim()||null,
|
||||
relacao: c.relacao||null,
|
||||
tipo: c.tipo||null,
|
||||
telefone: c.telefone ? digitsOnly(c.telefone) : null,
|
||||
email: c.email||null,
|
||||
is_primario: !!c.is_primario,
|
||||
}))
|
||||
if (!rows.length) return
|
||||
const { error: ins } = await supabase.from('patient_support_contacts').insert(rows)
|
||||
if (ins) throw ins
|
||||
}
|
||||
async function loadContatosSuporte (pid) {
|
||||
try {
|
||||
const { data, error } = await supabase.from('patient_support_contacts')
|
||||
.select('*').eq('patient_id', pid).order('is_primario', { ascending:false })
|
||||
if (error) throw error
|
||||
contatosSuporte.value = (data||[]).map(c=>({
|
||||
_k: c.id, nome: c.nome||'', relacao: c.relacao||'', tipo: c.tipo||'',
|
||||
telefone: fmtPhone(c.telefone||''), email: c.email||'', is_primario: !!c.is_primario,
|
||||
}))
|
||||
} catch (_) { contatosSuporte.value = [] }
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────
|
||||
// DB calls
|
||||
// ─────────────────────────────────────────────────────────
|
||||
// V#9 — DB calls delegadas ao patientsRepository (V#3 fundação)
|
||||
async function listGroups () {
|
||||
const tid = currentTenantId()
|
||||
let q = supabase.from('patient_groups').select('id,nome,descricao,cor,is_system,is_active').eq('is_active',true).order('nome',{ascending:true})
|
||||
if (tid) q = q.eq('tenant_id', tid)
|
||||
const { data, error } = await q
|
||||
if (error) throw error
|
||||
return (data||[]).map(g=>({...g,name:g.nome,color:g.cor}))
|
||||
return repoListGroups({ tenantId: currentTenantId() })
|
||||
}
|
||||
async function listTags () {
|
||||
const tid = currentTenantId()
|
||||
let q = supabase.from('patient_tags').select('id,nome,cor').order('nome',{ascending:true})
|
||||
if (tid) q = q.eq('tenant_id', tid)
|
||||
const { data, error } = await q
|
||||
if (error) throw error
|
||||
return (data||[]).map(t=>({...t,name:t.nome,color:t.cor}))
|
||||
return repoListTags({ tenantId: currentTenantId() })
|
||||
}
|
||||
async function getPatientById (id) {
|
||||
const tid = currentTenantId()
|
||||
let q = supabase.from('patients').select('*').eq('id',id)
|
||||
if (tid) q = q.eq('tenant_id', tid)
|
||||
const { data, error } = await q.maybeSingle()
|
||||
if (error) throw error; return data
|
||||
return repoGetPatientById(id, { tenantId: currentTenantId() })
|
||||
}
|
||||
async function getPatientRelations (id) {
|
||||
const { data:g, error:ge } = await supabase.from('patient_group_patient').select('patient_group_id').eq('patient_id',id); if (ge) throw ge
|
||||
const { data:t, error:te } = await supabase.from('patient_patient_tag').select('tag_id').eq('patient_id',id); if (te) throw te
|
||||
return { groupIds:(g||[]).map(x=>x.patient_group_id).filter(Boolean), tagIds:(t||[]).map(x=>x.tag_id).filter(Boolean) }
|
||||
return repoGetPatientRelations(id)
|
||||
}
|
||||
async function createPatient (payload) {
|
||||
const { data, error } = await supabase.from('patients').insert(payload).select('id').single()
|
||||
if (error) throw error; return data
|
||||
return repoCreatePatient(payload)
|
||||
}
|
||||
async function updatePatient (id, payload) {
|
||||
const tid = currentTenantId()
|
||||
let q = supabase.from('patients').update({ ...payload, updated_at:new Date().toISOString() }).eq('id',id)
|
||||
if (tid) q = q.eq('tenant_id', tid)
|
||||
const { error } = await q
|
||||
if (error) throw error
|
||||
return repoUpdatePatient(id, { ...payload, updated_at: new Date().toISOString() }, { tenantId: currentTenantId() })
|
||||
}
|
||||
|
||||
const groups = ref([])
|
||||
@@ -530,32 +494,28 @@ const tags = ref([])
|
||||
const grupoIdSelecionado = ref(null)
|
||||
const tagIdsSelecionadas = ref([])
|
||||
|
||||
// V#9 — replace de grupos/tags via repo
|
||||
async function replacePatientGroups (patient_id, groupId) {
|
||||
const { error } = await supabase.from('patient_group_patient').delete().eq('patient_id',patient_id); if (error) throw error
|
||||
if (!groupId) return
|
||||
const { tenantId } = await resolveTenantContextOrFail()
|
||||
const { error:ins } = await supabase.from('patient_group_patient').insert({ patient_id, patient_group_id:groupId, tenant_id:tenantId }); if (ins) throw ins
|
||||
return repoReplacePatientGroup(patient_id, groupId, { tenantId })
|
||||
}
|
||||
async function replacePatientTags (patient_id, tagIds) {
|
||||
const ownerId = await getOwnerId()
|
||||
const { error } = await supabase.from('patient_patient_tag').delete().eq('patient_id',patient_id).eq('owner_id',ownerId); if (error) throw error
|
||||
const clean = Array.from(new Set((tagIds||[]).filter(Boolean))); if (!clean.length) return
|
||||
const { tenantId } = await resolveTenantContextOrFail()
|
||||
const { error:ins } = await supabase.from('patient_patient_tag').insert(clean.map(tag_id=>({ owner_id:ownerId, patient_id, tag_id, tenant_id:tenantId }))); if (ins) throw ins
|
||||
return repoReplacePatientTags(patient_id, tagIds, { tenantId, ownerId })
|
||||
}
|
||||
|
||||
// V#9 — CEP via composable useCep (reutilizável)
|
||||
const _cep = useCep()
|
||||
async function onCepBlur () {
|
||||
try {
|
||||
const cep = digitsOnly(form.value.cep); if (cep.length!==8) return
|
||||
const res = await fetch(`https://viacep.com.br/ws/${cep}/json/`)
|
||||
const d = await res.json(); if (!d||d.erro) return
|
||||
form.value.cidade = d.localidade || form.value.cidade
|
||||
form.value.estado = d.uf || form.value.estado
|
||||
form.value.bairro = d.bairro || form.value.bairro
|
||||
form.value.endereco = d.logradouro || form.value.endereco
|
||||
if (!form.value.complemento) form.value.complemento = d.complemento||''
|
||||
toast.add({ severity:'success', summary:'CEP', detail:`${d.localidade} / ${d.uf}`, life:2000 })
|
||||
} catch (_) {}
|
||||
const result = await _cep.fetchCep(form.value.cep)
|
||||
if (!result) return
|
||||
form.value.cidade = result.cidade || form.value.cidade
|
||||
form.value.estado = result.uf || form.value.estado
|
||||
form.value.bairro = result.bairro || form.value.bairro
|
||||
form.value.endereco = result.endereco || form.value.endereco
|
||||
if (!form.value.complemento) form.value.complemento = result.complemento || ''
|
||||
toast.add({ severity:'success', summary:'CEP', detail:`${result.cidade} / ${result.uf}`, life:2000 })
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user