d6eb992f71
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>
118 lines
5.8 KiB
PL/PgSQL
118 lines
5.8 KiB
PL/PgSQL
-- =============================================================================
|
|
-- Migration: 20260419000014_financial_security_hardening
|
|
-- Sessão 6 — revisão Financeiro. Resolve V#1-V#5 (2 críticos + 3 altos).
|
|
-- V#6-V#11 adiados (médios/baixos com plano).
|
|
--
|
|
-- Auditoria prévia confirmou:
|
|
-- • 0 financial_records com tenant_id NULL
|
|
-- • 0 records com clinic_fee_amount > amount
|
|
-- → seguro aplicar NOT NULL e CHECK constraints.
|
|
-- =============================================================================
|
|
|
|
-- ─────────────────────────────────────────────────────────────────────────
|
|
-- V#1: billing_contracts policy granular
|
|
-- -----------------------------------------------------------------------------
|
|
DROP POLICY IF EXISTS "billing_contracts: owner full access" ON public.billing_contracts;
|
|
DROP POLICY IF EXISTS "billing_contracts: select" ON public.billing_contracts;
|
|
DROP POLICY IF EXISTS "billing_contracts: insert" ON public.billing_contracts;
|
|
DROP POLICY IF EXISTS "billing_contracts: update" ON public.billing_contracts;
|
|
DROP POLICY IF EXISTS "billing_contracts: delete" ON public.billing_contracts;
|
|
|
|
CREATE POLICY "billing_contracts: select" ON public.billing_contracts
|
|
FOR SELECT TO authenticated
|
|
USING (
|
|
owner_id = auth.uid()
|
|
OR public.is_saas_admin()
|
|
OR tenant_id IN (
|
|
SELECT tm.tenant_id FROM public.tenant_members tm
|
|
WHERE tm.user_id = auth.uid() AND tm.status = 'active'
|
|
)
|
|
);
|
|
|
|
CREATE POLICY "billing_contracts: insert" ON public.billing_contracts
|
|
FOR INSERT TO authenticated
|
|
WITH CHECK (
|
|
owner_id = auth.uid()
|
|
AND tenant_id IN (
|
|
SELECT tm.tenant_id FROM public.tenant_members tm
|
|
WHERE tm.user_id = auth.uid() AND tm.status = 'active'
|
|
)
|
|
);
|
|
|
|
CREATE POLICY "billing_contracts: update" ON public.billing_contracts
|
|
FOR UPDATE TO authenticated
|
|
USING (owner_id = auth.uid() OR public.is_saas_admin())
|
|
WITH CHECK (owner_id = auth.uid() OR public.is_saas_admin());
|
|
|
|
CREATE POLICY "billing_contracts: delete" ON public.billing_contracts
|
|
FOR DELETE TO authenticated
|
|
USING (owner_id = auth.uid() OR public.is_saas_admin());
|
|
|
|
-- ─────────────────────────────────────────────────────────────────────────
|
|
-- V#2: financial_records.tenant_id NOT NULL + trigger backfill
|
|
-- (auditoria: 0 órfãos, seguro aplicar)
|
|
-- -----------------------------------------------------------------------------
|
|
ALTER TABLE public.financial_records ALTER COLUMN tenant_id SET NOT NULL;
|
|
|
|
-- Trigger defensivo: se tentar inserir sem tenant_id, busca via owner_id->tenant_members
|
|
CREATE OR REPLACE FUNCTION public.financial_records_inject_tenant()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
BEGIN
|
|
IF NEW.tenant_id IS NULL AND NEW.owner_id IS NOT NULL THEN
|
|
SELECT tm.tenant_id INTO NEW.tenant_id
|
|
FROM public.tenant_members tm
|
|
WHERE tm.user_id = NEW.owner_id AND tm.status = 'active'
|
|
ORDER BY tm.created_at DESC
|
|
LIMIT 1;
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
|
|
DROP TRIGGER IF EXISTS trg_financial_records_inject_tenant ON public.financial_records;
|
|
CREATE TRIGGER trg_financial_records_inject_tenant
|
|
BEFORE INSERT ON public.financial_records
|
|
FOR EACH ROW EXECUTE FUNCTION public.financial_records_inject_tenant();
|
|
|
|
-- ─────────────────────────────────────────────────────────────────────────
|
|
-- V#5: financial_records CHECK contra net_amount negativo
|
|
-- -----------------------------------------------------------------------------
|
|
ALTER TABLE public.financial_records
|
|
DROP CONSTRAINT IF EXISTS financial_records_fee_lte_amount_chk;
|
|
|
|
ALTER TABLE public.financial_records
|
|
ADD CONSTRAINT financial_records_fee_lte_amount_chk
|
|
CHECK (clinic_fee_amount IS NULL OR (clinic_fee_amount >= 0 AND clinic_fee_amount <= amount));
|
|
|
|
-- ─────────────────────────────────────────────────────────────────────────
|
|
-- V#3: payment_settings — adicionar SELECT pra tenant_admin
|
|
-- -----------------------------------------------------------------------------
|
|
DROP POLICY IF EXISTS "payment_settings: tenant_admin read" ON public.payment_settings;
|
|
CREATE POLICY "payment_settings: tenant_admin read" ON public.payment_settings
|
|
FOR SELECT TO authenticated
|
|
USING (
|
|
tenant_id IS NOT NULL
|
|
AND tenant_id IN (
|
|
SELECT tm.tenant_id FROM public.tenant_members tm
|
|
WHERE tm.user_id = auth.uid() AND tm.status = 'active'
|
|
AND tm.role IN ('tenant_admin','admin','owner')
|
|
)
|
|
);
|
|
-- (a policy ALL "owner full access" continua — owner mexe nos próprios)
|
|
|
|
-- ─────────────────────────────────────────────────────────────────────────
|
|
-- V#4: professional_pricing — adicionar SELECT pra tenant_admin
|
|
-- -----------------------------------------------------------------------------
|
|
DROP POLICY IF EXISTS "professional_pricing: tenant_admin read" ON public.professional_pricing;
|
|
CREATE POLICY "professional_pricing: tenant_admin read" ON public.professional_pricing
|
|
FOR SELECT TO authenticated
|
|
USING (
|
|
tenant_id IN (
|
|
SELECT tm.tenant_id FROM public.tenant_members tm
|
|
WHERE tm.user_id = auth.uid() AND tm.status = 'active'
|
|
AND tm.role IN ('tenant_admin','admin','owner')
|
|
)
|
|
);
|