Sessoes 1-6 acumuladas: hardening B2, defesa em camadas, +192 testes

Repositorio estava ha ~5 sessoes sem commit. Consolida tudo desde d088a89.

Ver commit.md na raiz para descricao completa por sessao.

# Numeros
- A# auditoria abertos: 0/30
- V# verificacoes abertos: 5/52 (todos adiados com plano)
- T# testes escritos: 10/10
- Vitest: 192/192
- SQL integration: 33/33
- E2E (Playwright, novo): 5/5
- Migrations: 17 (10 novas Sessao 6)
- Areas auditadas: 7 (+documentos com 10 V#)

# Highlights Sessao 6 (hoje)
- V#34/V#41 Opcao B2: tenant_features com plano + override (RPC SECURITY DEFINER, tela /saas/tenant-features)
- A#20 rev2 self-hosted: defesa em 5 camadas (honeypot + rate limit + math captcha condicional + paranoid mode + dashboard /saas/security)
- Documentos hardening (V#43-V#49): tenant scoping em storage policies (vazamento entre clinicas eliminado), RPC validate_share_token, signatures policy granular
- SaaS Twilio Config (/saas/twilio-config): UI editavel para SID/webhook/cotacao; AUTH_TOKEN permanece em env var
- T#9 + T#10: useAgendaEvents.spec.js + Playwright E2E (descobriu bug no front que foi corrigido)

# Sessoes anteriores (1-5) consolidadas
- Sessao 1: auth/router/session, normalizeRole extraido
- Sessao 2: agenda - composables/services consolidados
- Sessao 3: pacientes - tenant_id em todas queries
- Sessao 4: security review pagina publica - 14/15 vulnerabilidades corrigidas
- Sessao 5: SaaS - P0 (A#30: 7 tabelas com RLS off corrigidas)

# .gitignore ajustado
- supabase/* + !supabase/functions/ (mantem 10 edge functions, ignora .temp/migrations gerados pelo CLI)
- database-novo/backups/ (regeneravel via db.cjs backup)
- test-results/ + playwright-report/
- .claude/settings.local.json (config local com senha de dev removida do tracking)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-04-19 15:42:46 -03:00
parent d088a89fb7
commit 7c20b518d4
175 changed files with 37325 additions and 37968 deletions
+119 -189
View File
@@ -1,28 +1,10 @@
-- =============================================================================
-- AgenciaPsi — Views
-- =============================================================================
-- current_tenant_id, owner_feature_entitlements, subscription_intents,
-- v_auth_users_public, v_cashflow_projection, v_commitment_totals,
-- v_patient_groups_with_counts, v_plan_active_prices, v_public_pricing,
-- v_subscription_feature_mismatch, v_subscription_health, v_subscription_health_v2,
-- v_tag_patient_counts, v_tenant_active_subscription, v_tenant_entitlements,
-- v_tenant_entitlements_full, v_tenant_entitlements_json,
-- v_tenant_feature_exceptions, v_tenant_feature_mismatch,
-- v_tenant_members_with_profiles, v_tenant_people, v_tenant_staff,
-- v_user_active_subscription, v_user_entitlements
-- =============================================================================
-- Views
-- Gerado automaticamente em 2026-04-17T12:23:05.233Z
-- Total: 27
CREATE VIEW public.current_tenant_id AS
SELECT current_setting('request.jwt.claim.tenant_id'::text, true) AS current_setting;
ALTER VIEW public.current_tenant_id OWNER TO supabase_admin;
--
-- Name: determined_commitment_fields; Type: TABLE; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.owner_feature_entitlements AS
WITH base AS (
SELECT s.user_id AS owner_id,
@@ -51,14 +33,6 @@ CREATE VIEW public.owner_feature_entitlements AS
FROM base
GROUP BY owner_id, feature_key;
ALTER VIEW public.owner_feature_entitlements OWNER TO supabase_admin;
--
-- Name: owner_users; Type: TABLE; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.subscription_intents AS
SELECT t.id,
t.user_id,
@@ -98,14 +72,6 @@ UNION ALL
'therapist'::text AS plan_target
FROM public.subscription_intents_personal p;
ALTER VIEW public.subscription_intents OWNER TO supabase_admin;
--
-- Name: subscription_intents_legacy; Type: TABLE; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.v_auth_users_public AS
SELECT id AS user_id,
email,
@@ -113,13 +79,6 @@ CREATE VIEW public.v_auth_users_public AS
last_sign_in_at
FROM auth.users u;
ALTER VIEW public.v_auth_users_public OWNER TO supabase_admin;
--
-- Name: v_cashflow_projection; Type: VIEW; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.v_cashflow_projection WITH (security_invoker='on') AS
SELECT gs.mes,
to_char(gs.mes, 'YYYY-MM'::text) AS mes_label,
@@ -136,20 +95,6 @@ CREATE VIEW public.v_cashflow_projection WITH (security_invoker='on') AS
GROUP BY gs.mes
ORDER BY gs.mes;
ALTER VIEW public.v_cashflow_projection OWNER TO supabase_admin;
--
-- Name: VIEW v_cashflow_projection; Type: COMMENT; Schema: public; Owner: supabase_admin
--
COMMENT ON VIEW public.v_cashflow_projection IS 'Fluxo de caixa projetado: próximos 6 meses com totais de pending+overdue por due_date. Usa security_invoker=on — filtra automaticamente pelo auth.uid() via RLS de financial_records.';
--
-- Name: v_commitment_totals; Type: VIEW; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.v_commitment_totals AS
SELECT c.tenant_id,
c.id AS commitment_id,
@@ -158,12 +103,76 @@ CREATE VIEW public.v_commitment_totals AS
LEFT JOIN public.commitment_time_logs l ON ((l.commitment_id = c.id)))
GROUP BY c.tenant_id, c.id;
ALTER VIEW public.v_commitment_totals OWNER TO supabase_admin;
--
-- Name: v_patient_groups_with_counts; Type: VIEW; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.v_patient_engajamento WITH (security_invoker='on') AS
WITH sessoes AS (
SELECT ae.patient_id,
ae.tenant_id,
count(*) FILTER (WHERE (ae.status = 'realizado'::public.status_evento_agenda)) AS total_realizadas,
count(*) FILTER (WHERE (ae.status = ANY (ARRAY['realizado'::public.status_evento_agenda, 'cancelado'::public.status_evento_agenda, 'faltou'::public.status_evento_agenda]))) AS total_marcadas,
count(*) FILTER (WHERE (ae.status = 'faltou'::public.status_evento_agenda)) AS total_faltas,
max(ae.inicio_em) FILTER (WHERE (ae.status = 'realizado'::public.status_evento_agenda)) AS ultima_sessao_em,
min(ae.inicio_em) FILTER (WHERE (ae.status = 'realizado'::public.status_evento_agenda)) AS primeira_sessao_em,
count(*) FILTER (WHERE ((ae.status = 'realizado'::public.status_evento_agenda) AND (ae.inicio_em >= (now() - '30 days'::interval)))) AS sessoes_ultimo_mes
FROM public.agenda_eventos ae
WHERE (ae.patient_id IS NOT NULL)
GROUP BY ae.patient_id, ae.tenant_id
), financeiro AS (
SELECT fr.patient_id,
fr.tenant_id,
COALESCE(sum(fr.final_amount) FILTER (WHERE (fr.status = 'paid'::text)), (0)::numeric) AS total_pago,
COALESCE(avg(fr.final_amount) FILTER (WHERE (fr.status = 'paid'::text)), (0)::numeric) AS ticket_medio,
count(*) FILTER (WHERE ((fr.status = ANY (ARRAY['pending'::text, 'overdue'::text])) AND (fr.due_date < now()))) AS cobr_vencidas,
count(*) FILTER (WHERE (fr.status = ANY (ARRAY['pending'::text, 'overdue'::text]))) AS cobr_pendentes,
count(*) FILTER (WHERE ((fr.type = 'receita'::public.financial_record_type) AND (fr.status = 'paid'::text))) AS cobr_pagas
FROM public.financial_records fr
WHERE ((fr.patient_id IS NOT NULL) AND (fr.deleted_at IS NULL))
GROUP BY fr.patient_id, fr.tenant_id
)
SELECT p.id AS patient_id,
p.tenant_id,
p.nome_completo,
p.status,
p.risco_elevado,
COALESCE(s.total_realizadas, (0)::bigint) AS total_sessoes,
COALESCE(s.sessoes_ultimo_mes, (0)::bigint) AS sessoes_ultimo_mes,
s.primeira_sessao_em,
s.ultima_sessao_em,
(EXTRACT(day FROM (now() - s.ultima_sessao_em)))::integer AS dias_sem_sessao,
CASE
WHEN (COALESCE(s.total_marcadas, (0)::bigint) = 0) THEN NULL::numeric
ELSE round((((s.total_realizadas)::numeric / (s.total_marcadas)::numeric) * (100)::numeric), 1)
END AS taxa_comparecimento,
COALESCE(f.total_pago, (0)::numeric) AS ltv_total,
round(COALESCE(f.ticket_medio, (0)::numeric), 2) AS ticket_medio,
COALESCE(f.cobr_vencidas, (0)::bigint) AS cobr_vencidas,
COALESCE(f.cobr_pagas, (0)::bigint) AS cobr_pagas,
CASE
WHEN (COALESCE((f.cobr_pagas + f.cobr_vencidas), (0)::bigint) = 0) THEN NULL::numeric
ELSE round((((f.cobr_pagas)::numeric / ((f.cobr_pagas + f.cobr_vencidas))::numeric) * (100)::numeric), 1)
END AS taxa_pagamentos_dia,
round(LEAST((100)::numeric, COALESCE(((
CASE
WHEN (COALESCE(s.total_marcadas, (0)::bigint) = 0) THEN (50)::numeric
ELSE LEAST((50)::numeric, (((s.total_realizadas)::numeric / (s.total_marcadas)::numeric) * (50)::numeric))
END +
CASE
WHEN (COALESCE((f.cobr_pagas + f.cobr_vencidas), (0)::bigint) = 0) THEN (30)::numeric
ELSE LEAST((30)::numeric, (((f.cobr_pagas)::numeric / ((f.cobr_pagas + f.cobr_vencidas))::numeric) * (30)::numeric))
END) + (
CASE
WHEN (s.ultima_sessao_em IS NULL) THEN 0
WHEN (EXTRACT(day FROM (now() - s.ultima_sessao_em)) <= (14)::numeric) THEN 20
WHEN (EXTRACT(day FROM (now() - s.ultima_sessao_em)) <= (30)::numeric) THEN 15
WHEN (EXTRACT(day FROM (now() - s.ultima_sessao_em)) <= (60)::numeric) THEN 8
ELSE 0
END)::numeric), (0)::numeric)), 0) AS engajamento_score,
CASE
WHEN (s.primeira_sessao_em IS NULL) THEN NULL::integer
ELSE (EXTRACT(day FROM (now() - s.primeira_sessao_em)))::integer
END AS duracao_tratamento_dias
FROM ((public.patients p
LEFT JOIN sessoes s ON (((s.patient_id = p.id) AND (s.tenant_id = p.tenant_id))))
LEFT JOIN financeiro f ON (((f.patient_id = p.id) AND (f.tenant_id = p.tenant_id))));
CREATE VIEW public.v_patient_groups_with_counts AS
SELECT pg.id,
@@ -179,12 +188,27 @@ CREATE VIEW public.v_patient_groups_with_counts AS
LEFT JOIN public.patient_group_patient pgp ON ((pgp.patient_group_id = pg.id)))
GROUP BY pg.id, pg.nome, pg.cor, pg.owner_id, pg.is_system, pg.is_active, pg.created_at, pg.updated_at;
ALTER VIEW public.v_patient_groups_with_counts OWNER TO supabase_admin;
--
-- Name: v_plan_active_prices; Type: VIEW; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.v_patients_risco WITH (security_invoker='on') AS
SELECT p.id,
p.tenant_id,
p.nome_completo,
p.status,
p.risco_elevado,
p.risco_nota,
p.risco_sinalizado_em,
e.dias_sem_sessao,
e.engajamento_score,
e.taxa_comparecimento,
CASE
WHEN p.risco_elevado THEN 'risco_sinalizado'::text
WHEN ((COALESCE(e.dias_sem_sessao, 999) > 30) AND (p.status = 'Ativo'::text)) THEN 'sem_sessao_30d'::text
WHEN (COALESCE(e.taxa_comparecimento, (100)::numeric) < (60)::numeric) THEN 'baixo_comparecimento'::text
WHEN (COALESCE(e.cobr_vencidas, (0)::bigint) > 0) THEN 'cobranca_vencida'::text
ELSE 'ok'::text
END AS alerta_tipo
FROM (public.patients p
JOIN public.v_patient_engajamento e ON ((e.patient_id = p.id)))
WHERE ((p.status = 'Ativo'::text) AND ((p.risco_elevado = true) OR (COALESCE(e.dias_sem_sessao, 999) > 30) OR (COALESCE(e.taxa_comparecimento, (100)::numeric) < (60)::numeric) OR (COALESCE(e.cobr_vencidas, (0)::bigint) > 0)));
CREATE VIEW public.v_plan_active_prices AS
SELECT plan_id,
@@ -211,13 +235,6 @@ CREATE VIEW public.v_plan_active_prices AS
FROM public.plan_prices
GROUP BY plan_id;
ALTER VIEW public.v_plan_active_prices OWNER TO supabase_admin;
--
-- Name: v_public_pricing; Type: VIEW; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.v_public_pricing AS
SELECT p.id AS plan_id,
p.key AS plan_key,
@@ -241,13 +258,6 @@ CREATE VIEW public.v_public_pricing AS
LEFT JOIN public.v_plan_active_prices ap ON ((ap.plan_id = p.id)))
ORDER BY COALESCE(pp.sort_order, 0), p.key;
ALTER VIEW public.v_public_pricing OWNER TO supabase_admin;
--
-- Name: v_subscription_feature_mismatch; Type: VIEW; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.v_subscription_feature_mismatch AS
WITH expected AS (
SELECT s.user_id AS owner_id,
@@ -272,13 +282,6 @@ CREATE VIEW public.v_subscription_feature_mismatch AS
FULL JOIN actual ON (((expected.owner_id = actual.owner_id) AND (expected.feature_key = actual.feature_key))))
WHERE ((expected.feature_key IS NULL) OR (actual.feature_key IS NULL));
ALTER VIEW public.v_subscription_feature_mismatch OWNER TO supabase_admin;
--
-- Name: v_subscription_health; Type: VIEW; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.v_subscription_health AS
SELECT s.id AS subscription_id,
s.user_id AS owner_id,
@@ -303,13 +306,6 @@ CREATE VIEW public.v_subscription_health AS
FROM (public.subscriptions s
LEFT JOIN public.plans p ON ((p.id = s.plan_id)));
ALTER VIEW public.v_subscription_health OWNER TO supabase_admin;
--
-- Name: v_subscription_health_v2; Type: VIEW; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.v_subscription_health_v2 AS
SELECT s.id AS subscription_id,
s.user_id AS owner_id,
@@ -334,13 +330,6 @@ CREATE VIEW public.v_subscription_health_v2 AS
FROM (public.subscriptions s
LEFT JOIN public.plans p ON ((p.id = s.plan_id)));
ALTER VIEW public.v_subscription_health_v2 OWNER TO supabase_admin;
--
-- Name: v_tag_patient_counts; Type: VIEW; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.v_tag_patient_counts AS
SELECT t.id,
t.owner_id,
@@ -355,13 +344,6 @@ CREATE VIEW public.v_tag_patient_counts AS
LEFT JOIN public.patient_patient_tag ppt ON (((ppt.tag_id = t.id) AND (ppt.owner_id = t.owner_id))))
GROUP BY t.id, t.owner_id, t.nome, t.cor, t.is_padrao, t.created_at, t.updated_at;
ALTER VIEW public.v_tag_patient_counts OWNER TO supabase_admin;
--
-- Name: v_tenant_active_subscription; Type: VIEW; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.v_tenant_active_subscription AS
SELECT DISTINCT ON (tenant_id) tenant_id,
plan_id,
@@ -375,13 +357,6 @@ CREATE VIEW public.v_tenant_active_subscription AS
WHERE ((tenant_id IS NOT NULL) AND (status = 'active'::text) AND ((current_period_end IS NULL) OR (current_period_end > now())))
ORDER BY tenant_id, created_at DESC;
ALTER VIEW public.v_tenant_active_subscription OWNER TO supabase_admin;
--
-- Name: v_tenant_entitlements; Type: VIEW; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.v_tenant_entitlements AS
SELECT a.tenant_id,
f.key AS feature_key,
@@ -390,13 +365,6 @@ CREATE VIEW public.v_tenant_entitlements AS
JOIN public.plan_features pf ON (((pf.plan_id = a.plan_id) AND (pf.enabled = true))))
JOIN public.features f ON ((f.id = pf.feature_id)));
ALTER VIEW public.v_tenant_entitlements OWNER TO supabase_admin;
--
-- Name: v_tenant_entitlements_full; Type: VIEW; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.v_tenant_entitlements_full AS
SELECT a.tenant_id,
f.key AS feature_key,
@@ -409,13 +377,6 @@ CREATE VIEW public.v_tenant_entitlements_full AS
JOIN public.features f ON ((f.id = pf.feature_id)))
JOIN public.plans p ON ((p.id = a.plan_id)));
ALTER VIEW public.v_tenant_entitlements_full OWNER TO supabase_admin;
--
-- Name: v_tenant_entitlements_json; Type: VIEW; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.v_tenant_entitlements_json AS
SELECT tenant_id,
max(plan_key) AS plan_key,
@@ -423,13 +384,6 @@ CREATE VIEW public.v_tenant_entitlements_json AS
FROM public.v_tenant_entitlements_full
GROUP BY tenant_id;
ALTER VIEW public.v_tenant_entitlements_json OWNER TO supabase_admin;
--
-- Name: v_tenant_feature_exceptions; Type: VIEW; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.v_tenant_feature_exceptions AS
SELECT tf.tenant_id,
a.plan_key,
@@ -440,13 +394,6 @@ CREATE VIEW public.v_tenant_feature_exceptions AS
LEFT JOIN public.v_tenant_entitlements_full v ON (((v.tenant_id = tf.tenant_id) AND (v.feature_key = tf.feature_key))))
WHERE ((tf.enabled = true) AND (COALESCE(v.allowed, false) = false));
ALTER VIEW public.v_tenant_feature_exceptions OWNER TO supabase_admin;
--
-- Name: v_tenant_feature_mismatch; Type: VIEW; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.v_tenant_feature_mismatch AS
WITH plan_allowed AS (
SELECT v.tenant_id,
@@ -469,13 +416,6 @@ CREATE VIEW public.v_tenant_feature_mismatch AS
LEFT JOIN plan_allowed p ON (((p.tenant_id = o.tenant_id) AND (p.feature_key = o.feature_key))))
WHERE ((o.enabled = true) AND (COALESCE(p.allowed, false) = false));
ALTER VIEW public.v_tenant_feature_mismatch OWNER TO supabase_admin;
--
-- Name: v_tenant_members_with_profiles; Type: VIEW; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.v_tenant_members_with_profiles AS
SELECT tm.id AS tenant_member_id,
tm.tenant_id,
@@ -489,13 +429,6 @@ CREATE VIEW public.v_tenant_members_with_profiles AS
LEFT JOIN public.profiles p ON ((p.id = tm.user_id)))
LEFT JOIN auth.users au ON ((au.id = tm.user_id)));
ALTER VIEW public.v_tenant_members_with_profiles OWNER TO supabase_admin;
--
-- Name: v_tenant_people; Type: VIEW; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.v_tenant_people AS
SELECT 'member'::text AS type,
m.tenant_id,
@@ -519,13 +452,6 @@ UNION ALL
FROM public.tenant_invites i
WHERE ((i.accepted_at IS NULL) AND (i.revoked_at IS NULL));
ALTER VIEW public.v_tenant_people OWNER TO supabase_admin;
--
-- Name: v_tenant_staff; Type: VIEW; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.v_tenant_staff AS
SELECT ('m_'::text || (tm.id)::text) AS row_id,
tm.tenant_id,
@@ -552,12 +478,31 @@ UNION ALL
FROM public.tenant_invites ti
WHERE ((ti.accepted_at IS NULL) AND (ti.revoked_at IS NULL) AND (ti.expires_at > now()));
ALTER VIEW public.v_tenant_staff OWNER TO supabase_admin;
--
-- Name: v_user_active_subscription; Type: VIEW; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.v_twilio_whatsapp_overview AS
SELECT nc.id AS channel_id,
nc.tenant_id,
nc.owner_id,
nc.is_active,
nc.connection_status,
nc.display_name,
nc.twilio_subaccount_sid,
nc.twilio_phone_number,
nc.twilio_phone_sid,
nc.cost_per_message_usd,
nc.price_per_message_brl,
nc.provisioned_at,
nc.created_at,
nc.updated_at,
COALESCE(u.messages_sent, 0) AS current_month_sent,
COALESCE(u.messages_delivered, 0) AS current_month_delivered,
COALESCE(u.messages_failed, 0) AS current_month_failed,
COALESCE(u.cost_usd, (0)::numeric) AS current_month_cost_usd,
COALESCE(u.cost_brl, (0)::numeric) AS current_month_cost_brl,
COALESCE(u.revenue_brl, (0)::numeric) AS current_month_revenue_brl,
COALESCE(u.margin_brl, (0)::numeric) AS current_month_margin_brl
FROM (public.notification_channels nc
LEFT JOIN public.twilio_subaccount_usage u ON (((u.channel_id = nc.id) AND (u.period_start = (date_trunc('month'::text, (CURRENT_DATE)::timestamp with time zone))::date))))
WHERE ((nc.channel = 'whatsapp'::text) AND (nc.provider = 'twilio'::text) AND (nc.deleted_at IS NULL));
CREATE VIEW public.v_user_active_subscription AS
SELECT DISTINCT ON (user_id) user_id,
@@ -572,13 +517,6 @@ CREATE VIEW public.v_user_active_subscription AS
WHERE ((tenant_id IS NULL) AND (user_id IS NOT NULL) AND (status = 'active'::text) AND ((current_period_end IS NULL) OR (current_period_end > now())))
ORDER BY user_id, created_at DESC;
ALTER VIEW public.v_user_active_subscription OWNER TO supabase_admin;
--
-- Name: v_user_entitlements; Type: VIEW; Schema: public; Owner: supabase_admin
--
CREATE VIEW public.v_user_entitlements AS
SELECT a.user_id,
f.key AS feature_key,
@@ -586,11 +524,3 @@ CREATE VIEW public.v_user_entitlements AS
FROM ((public.v_user_active_subscription a
JOIN public.plan_features pf ON (((pf.plan_id = a.plan_id) AND (pf.enabled = true))))
JOIN public.features f ON ((f.id = pf.feature_id)));
ALTER VIEW public.v_user_entitlements OWNER TO supabase_admin;
--
-- Name: messages; Type: TABLE; Schema: realtime; Owner: supabase_realtime_admin
--