diff --git a/db/ESTRUTURA.md b/db/ESTRUTURA.md index ec3424c..f80bd74 100644 --- a/db/ESTRUTURA.md +++ b/db/ESTRUTURA.md @@ -3,7 +3,7 @@ > ⚠️ **ARQUIVO GERADO. Não edite à mão.** > `npx tsx scripts/db/gerar-estrutura.ts` -Fonte: `db/snapshot/schema.sql` (dump de **2026-08-11**, até a migration **00000000000000**) +Fonte: `db/snapshot/schema.sql` (dump de **2026-08-12**, até a migration **20260812010000**) cruzado com o código vivo em `src/`. A verdade do banco é **a baseline `00000000000000_baseline.sql` + as migrations @@ -18,21 +18,17 @@ posteriores a ela**. Este documento é a leitura humana disso; a leitura de máq | Views | **12** | | Funções | **73** | | Triggers | **93** | -| Policies RLS | **348** | -| Tabelas com RLS | **107** de 112 | +| Policies RLS | **360** | +| Tabelas com RLS | **112** de 112 | | Tabelas referenciadas no código | **88** de 112 | ## Sinais que pedem ação Contas que este gerador fecha toda vez que roda. Item aqui não é opinião — é o dump. -### Tabelas sem RLS (5) +### Tabelas sem RLS (0) -- `categorization_patterns` -- `company_ai_dossiers` -- `consultant_memories` -- `employees` -- `meetings` +_Nenhuma — todas as tabelas têm RLS._ ### Tabelas sem nenhuma referência em `src/` (24) @@ -73,40 +69,12 @@ Isto é bug em produção esperando o caminho ser exercitado. - `pulse_checks` — 2 referência(s) - `system_notifications` — 1 referência(s) -### Funções `SECURITY DEFINER` sem `search_path` fixo (29) +### Funções `SECURITY DEFINER` sem `search_path` fixo (0) `SECURITY DEFINER` roda com os poderes do dono. Sem `SET search_path`, um schema na frente do `public` pode sequestrar as chamadas de dentro da função. -- `accept_invite()` -- `avancar_jornada_etapa()` -- `calculate_risk_score()` -- `check_cnpj_duplicates()` -- `compile_financial_context()` -- `create_invite()` -- `create_lite_company_and_user()` -- `extract_financial_gaps()` -- `get_dre_readiness()` -- `get_lancamentos_unicos()` -- `get_status_mensal()` -- `guard_bank_account_inactive_company()` -- `handle_new_lite_company()` -- `increment_assertiva_credits()` -- `is_admin()` -- `is_consultor()` -- `list_consultores()` -- `lite_get_company_id()` -- `mark_dossier_stale()` -- `recalcular_progresso_modulo()` -- `recalcular_progresso_projeto()` -- `reset_assertiva_monthly_credits()` -- `trg_jornada_categorizacao()` -- `trg_jornada_impeditivos()` -- `trg_jornada_primeira_dre()` -- `trg_jornada_upload_feito()` -- `trg_mark_dossier_stale_direct()` -- `trg_mark_dossier_stale_via_client()` -- `validate_invite()` +_Nenhuma._ ### Views sem `security_invoker` (12 de 12) @@ -174,7 +142,7 @@ _O catálogo e as regras que dão sentido contábil a cada lançamento._ | Tabela | Cols | PK | RLS | Policies | Triggers | Índices | Usos em `src/` | |---|--:|---|:-:|--:|--:|--:|--:| | [`categories`](#categories) | 11 | id | ✅ | 2 | 1 | 2 | **0** | -| [`categorization_patterns`](#categorization_patterns) | 13 | id | ❌ | 0 | 1 | 2 | 1 | +| [`categorization_patterns`](#categorization_patterns) | 13 | id | ✅ | 4 | 1 | 2 | 1 | | [`categorization_rules`](#categorization_rules) | 18 | id | ✅ | 2 | 1 | 4 | 2 | | [`dre_categories`](#dre_categories) | 10 | id | ✅ | 1 | 0 | 1 | 4 | | [`dre_subcategories`](#dre_subcategories) | 21 | id | ✅ | 3 | 0 | 5 | 5 | @@ -288,9 +256,9 @@ _Pessoas, entregas, reuniões e catálogo de produto/serviço._ | Tabela | Cols | PK | RLS | Policies | Triggers | Índices | Usos em `src/` | |---|--:|---|:-:|--:|--:|--:|--:| -| [`employees`](#employees) | 34 | id | ❌ | 6 | 2 | 6 | 17 | +| [`employees`](#employees) | 34 | id | ✅ | 6 | 2 | 6 | 17 | | [`estoque_produtos`](#estoque_produtos) | 12 | id | ✅ | 4 | 1 | 1 | 8 | -| [`meetings`](#meetings) | 20 | id | ❌ | 0 | 1 | 1 | **0** | +| [`meetings`](#meetings) | 20 | id | ✅ | 0 | 1 | 1 | **0** | | [`products_services`](#products_services) | 12 | id | ✅ | 4 | 2 | 1 | 5 | | [`project_deliverables`](#project_deliverables) | 13 | id | ✅ | 4 | 1 | 2 | **0** | | [`project_meetings`](#project_meetings) | 16 | id | ✅ | 2 | 1 | 3 | **0** | @@ -322,8 +290,8 @@ _Telemetria de chamadas, dossiês e memória do consultor._ | Tabela | Cols | PK | RLS | Policies | Triggers | Índices | Usos em `src/` | |---|--:|---|:-:|--:|--:|--:|--:| | [`ai_calls`](#ai_calls) | 18 | id | ✅ | 4 | 0 | 4 | 3 | -| [`company_ai_dossiers`](#company_ai_dossiers) | 9 | id | ❌ | 0 | 0 | 1 | 4 | -| [`consultant_memories`](#consultant_memories) | 10 | id | ❌ | 0 | 0 | 1 | 5 | +| [`company_ai_dossiers`](#company_ai_dossiers) | 9 | id | ✅ | 4 | 0 | 1 | 4 | +| [`consultant_memories`](#consultant_memories) | 10 | id | ✅ | 4 | 0 | 1 | 5 | ### Lite (produto paralelo) @@ -1040,7 +1008,7 @@ _Tabelas do produto Lite — namespace próprio, não se mistura com o principal > Padrões genéricos de auto-categorização. Filosofia: qualidade > quantidade. Padrões específicos de empresa devem ser categorizados manualmente ou pela IA futura. -**Domínio:** Financeiro · categorização · **RLS:** ❌ **desligada** · **Usos em `src/`:** 1 +**Domínio:** Financeiro · categorização · **RLS:** ✅ 4 policies · **Usos em `src/`:** 1 | Coluna | Tipo | Nulo | Padrão | |---|---|:-:|---| @@ -2672,7 +2640,7 @@ _Tabelas do produto Lite — namespace próprio, não se mistura com o principal > Colaboradores para análise de Custo Hora-Homem -**Domínio:** Gestão e operação · **RLS:** ❌ **desligada** · **Usos em `src/`:** 17 +**Domínio:** Gestão e operação · **RLS:** ✅ 6 policies · **Usos em `src/`:** 17 | Coluna | Tipo | Nulo | Padrão | |---|---|:-:|---| @@ -2742,7 +2710,7 @@ _Tabelas do produto Lite — namespace próprio, não se mistura com o principal ### meetings -**Domínio:** Gestão e operação · **RLS:** ❌ **desligada** · **Usos em `src/`:** 0 +**Domínio:** Gestão e operação · **RLS:** ✅ 0 policies · **Usos em `src/`:** 0 | Coluna | Tipo | Nulo | Padrão | |---|---|:-:|---| @@ -3159,7 +3127,7 @@ _Tabelas do produto Lite — namespace próprio, não se mistura com o principal ### company_ai_dossiers -**Domínio:** IA · **RLS:** ❌ **desligada** · **Usos em `src/`:** 4 +**Domínio:** IA · **RLS:** ✅ 4 policies · **Usos em `src/`:** 4 | Coluna | Tipo | Nulo | Padrão | |---|---|:-:|---| @@ -3177,7 +3145,7 @@ _Tabelas do produto Lite — namespace próprio, não se mistura com o principal ### consultant_memories -**Domínio:** IA · **RLS:** ❌ **desligada** · **Usos em `src/`:** 5 +**Domínio:** IA · **RLS:** ✅ 4 policies · **Usos em `src/`:** 5 | Coluna | Tipo | Nulo | Padrão | |---|---|:-:|---| @@ -3306,62 +3274,62 @@ _Tabelas do produto Lite — namespace próprio, não se mistura com o principal | Função | `SECURITY DEFINER` | `search_path` fixo | Chamadas via `.rpc()` | |---|:-:|:-:|--:| -| `accept_invite` | ✅ | ❌ | 1 | +| `accept_invite` | ✅ | ✅ | 1 | | `analisar_impacto_mover_bank_account` | ✅ | ✅ | 1 | | `apply_demo_snapshot` | ✅ | ✅ | 0 | | `auto_move_to_em_contato` | — | — | 0 | -| `avancar_jornada_etapa` | ✅ | ❌ | 1 | +| `avancar_jornada_etapa` | ✅ | ✅ | 1 | | `calculate_monthly_dre` | — | — | 9 | | `calculate_monthly_dre_v2` | — | — | 0 | | `calculate_monthly_dre_v3` | — | — | 2 | -| `calculate_risk_score` | ✅ | ❌ | 1 | -| `check_cnpj_duplicates` | ✅ | ❌ | 1 | -| `compile_financial_context` | ✅ | ❌ | 1 | -| `create_invite` | ✅ | ❌ | 1 | -| `create_lite_company_and_user` | ✅ | ❌ | 0 | -| `extract_financial_gaps` | ✅ | ❌ | 1 | +| `calculate_risk_score` | ✅ | ✅ | 1 | +| `check_cnpj_duplicates` | ✅ | ✅ | 1 | +| `compile_financial_context` | ✅ | ✅ | 1 | +| `create_invite` | ✅ | ✅ | 1 | +| `create_lite_company_and_user` | ✅ | ✅ | 0 | +| `extract_financial_gaps` | ✅ | ✅ | 1 | | `generate_invite_token` | — | — | 0 | | `get_abc_buckets_despesas` | — | — | 2 | | `get_abc_buckets_despesas_grupo` | — | — | 1 | | `get_category_breakdown` | — | — | 0 | -| `get_dre_readiness` | ✅ | ❌ | 3 | +| `get_dre_readiness` | ✅ | ✅ | 3 | | `get_dre_summary` | — | — | 0 | | `get_exercicios_fiscais` | — | — | 1 | -| `get_lancamentos_unicos` | ✅ | ❌ | 5 | -| `get_status_mensal` | ✅ | ❌ | 2 | +| `get_lancamentos_unicos` | ✅ | ✅ | 5 | +| `get_status_mensal` | ✅ | ✅ | 2 | | `get_user_role` | ✅ | ✅ | 2 | -| `guard_bank_account_inactive_company` | ✅ | ❌ | 0 | -| `handle_new_lite_company` | ✅ | ❌ | 0 | +| `guard_bank_account_inactive_company` | ✅ | ✅ | 0 | +| `handle_new_lite_company` | ✅ | ✅ | 0 | | `handle_new_user` | ✅ | ✅ | 0 | -| `increment_assertiva_credits` | ✅ | ❌ | 1 | -| `increment_pattern_hit` | — | — | 1 | +| `increment_assertiva_credits` | ✅ | ✅ | 1 | +| `increment_pattern_hit` | ✅ | ✅ | 1 | | `invalidar_endividamento_por_ancora` | ✅ | ✅ | 0 | -| `is_admin` | ✅ | ❌ | 0 | +| `is_admin` | ✅ | ✅ | 0 | | `is_admin_or_sdr` | ✅ | ✅ | 0 | -| `is_consultor` | ✅ | ❌ | 0 | -| `list_consultores` | ✅ | ❌ | 0 | -| `lite_get_company_id` | ✅ | ❌ | 0 | +| `is_consultor` | ✅ | ✅ | 0 | +| `list_consultores` | ✅ | ✅ | 0 | +| `lite_get_company_id` | ✅ | ✅ | 0 | | `marcar_snapshot_desatualizado` | ✅ | ✅ | 0 | -| `mark_dossier_stale` | ✅ | ❌ | 0 | +| `mark_dossier_stale` | ✅ | ✅ | 0 | | `move_bank_account_with_transactions` | ✅ | ✅ | 1 | | `nivel_jornada_etapa` | — | — | 0 | -| `recalcular_progresso_modulo` | ✅ | ❌ | 0 | -| `recalcular_progresso_projeto` | ✅ | ❌ | 0 | +| `recalcular_progresso_modulo` | ✅ | ✅ | 0 | +| `recalcular_progresso_projeto` | ✅ | ✅ | 0 | | `registrar_ancora_saldo` | ✅ | ✅ | 1 | -| `reset_assertiva_monthly_credits` | ✅ | ❌ | 0 | +| `reset_assertiva_monthly_credits` | ✅ | ✅ | 0 | | `reset_demo_project` | ✅ | ✅ | 0 | | `set_feature_flag_overrides_updated_at` | — | — | 0 | | `touch_card_acquirer_rates_updated_at` | — | — | 0 | | `touch_dre_det_antecipacao_linha_updated_at` | — | — | 0 | | `touch_endividamento_contratos_updated_at` | — | — | 0 | | `touch_loan_contracts_updated_at` | — | — | 0 | -| `trg_jornada_categorizacao` | ✅ | ❌ | 0 | -| `trg_jornada_impeditivos` | ✅ | ❌ | 0 | -| `trg_jornada_primeira_dre` | ✅ | ❌ | 0 | -| `trg_jornada_upload_feito` | ✅ | ❌ | 0 | -| `trg_mark_dossier_stale_direct` | ✅ | ❌ | 0 | +| `trg_jornada_categorizacao` | ✅ | ✅ | 0 | +| `trg_jornada_impeditivos` | ✅ | ✅ | 0 | +| `trg_jornada_primeira_dre` | ✅ | ✅ | 0 | +| `trg_jornada_upload_feito` | ✅ | ✅ | 0 | +| `trg_mark_dossier_stale_direct` | ✅ | ✅ | 0 | | `trg_mark_dossier_stale_module_analyses` | ✅ | ✅ | 0 | -| `trg_mark_dossier_stale_via_client` | ✅ | ❌ | 0 | +| `trg_mark_dossier_stale_via_client` | ✅ | ✅ | 0 | | `trg_project_analise_rica` | ✅ | ✅ | 0 | | `update_cockpit_actions_updated_at` | — | — | 0 | | `update_employees_updated_at` | — | — | 0 | @@ -3378,5 +3346,5 @@ _Tabelas do produto Lite — namespace próprio, não se mistura com o principal | `upsert_cost_audit_items` | — | — | 1 | | `validar_folha_vs_dre` | — | — | 1 | | `validar_soma_transacao_linha_split` | — | — | 0 | -| `validate_invite` | ✅ | ❌ | 1 | +| `validate_invite` | ✅ | ✅ | 1 | diff --git a/db/snapshot/SNAPSHOT.md b/db/snapshot/SNAPSHOT.md index 60f5424..7103133 100644 --- a/db/snapshot/SNAPSHOT.md +++ b/db/snapshot/SNAPSHOT.md @@ -4,12 +4,12 @@ | Campo | Valor | |---|---| -| **Status** | 🟢 **AUTORITATIVO** — marco zero. A baseline `00000000000000_baseline.sql` **é** este dump, e o replay dela do zero foi provado em Postgres 17.6 limpo (log de 2026-08-11 abaixo). | -| Dump gerado em | **2026-08-11** (`supabase db dump --linked` + `--role-only`, projeto `tdlxqqgechxhkygdmsxq`, CLI v2.84.0) — byte a byte igual ao de 2026-08-10, **zero drift** em 24h | -| Commit git no dump | `chore/baseline-do-banco-real` | -| `ultima_migration_incluida` | **`00000000000000`** (a baseline) — nenhuma migration incremental depois dela ainda | -| `ultima_migration_no_repo` | **`00000000000000`** | -| **Veredito** | 🟢 **autoritativo** — `schema.sql` **14.899 linhas**; `roles.sql` **13** (diff vazio). | +| **Status** | 🟢 **AUTORITATIVO** — inclui as duas migrations de RLS de 2026-08-12, aplicadas no SQL Editor e conferidas neste dump. | +| Dump gerado em | **2026-08-12** (`supabase db dump --linked`, `--role-only` e `--data-only`, projeto `tdlxqqgechxhkygdmsxq`, CLI v2.84.0) | +| Commit git no dump | `fix/rls-tabelas-orfas` | +| `ultima_migration_incluida` | **`20260812010000`** | +| `ultima_migration_no_repo` | **`20260812010000`** | +| **Veredito** | 🟢 **autoritativo** — `schema.sql` **15.030 linhas**; `roles.sql` **13** (diff vazio); `seed.sql` **25.175**. | ## A regra, depois da baseline de 2026-08-11 @@ -26,6 +26,43 @@ O mapa legível de tudo isso é [`db/ESTRUTURA.md`](../ESTRUTURA.md), **gerado** `npx tsx scripts/db/gerar-estrutura.ts`. Depois de todo re-dump, rode o gerador: o diff do `.md` mostra exatamente o que mudou no banco. +### Validação do re-dump (2026-08-12, RLS das tabelas órfãs) + +Migrations `20260812000000` (RLS nas 5 tabelas + `search_path` em 39 funções) e +`20260812010000` (corrige o DELETE de `employees`), aplicadas no SQL Editor. + +| Objeto | Antes | Depois | +|---|--:|--:| +| Tabelas | 112 | **112** | +| Views | 12 | **12** | +| Funções | 73 | **73** | +| Triggers | 92 | **92** | +| **Policies** | 348 | **360** | +| **Tabelas com RLS** | 107 | **112 de 112** | +| Funções `SECURITY DEFINER` sem `search_path` | 29 | **0** | + +`roles.sql`: diff vazio. `seed.sql` 25.164 → 25.175: nenhuma tabela perdeu linha. + +**O re-dump pegou um efeito colateral que a migration não mostrava.** Esperávamos ++16 policies e vieram +12. A diferença revelou que `employees` **já tinha 6 +policies** — dormentes, porque policy existe mesmo com RLS desligada e só não é +aplicada. O `DROP POLICY IF EXISTS` substituiu 4 delas às cegas. Comparadas uma a +uma contra o dump anterior: `select`/`insert`/`update` não ampliaram nada (as +antigas dependiam de `project_members`, e as policies `employees_cliente_*`, não +tocadas, já traziam a mesma cláusula de staff), mas `employees_delete` era +`is_admin()` puro e passou a permitir consultor apagar linha de folha. A +`20260812010000` devolveu ao original. + +⚠️ **Correção de um número afirmado antes de aplicar.** Foi dito que as 5 tabelas +estavam com zero linhas; o método de contagem estava errado (procurava blocos +`COPY`, e este dump usa `INSERT`). Contagem correta: `employees` 0 · `meetings` 0 +· `consultant_memories` ~4 · `company_ai_dossiers` ~5 · `categorization_patterns` +~33. Conferido depois, no dado: **ninguém perdeu acesso** — `profiles` tem 1 +`owner` + 2 `admin` (cobertos por `is_admin`) e 3 `cliente`, e `project_members` +tem exatamente 3 linhas `role='cliente'` para eles (é o que a view +`client_project_access` enxerga). O 4º membro é `consultor` no projeto mas +`owner` em `profiles`, coberto pelas duas pontas. + ### Prova do replay do zero (2026-08-11) O que a baseline existe para garantir — e que **nunca** foi verdade antes dela. diff --git a/db/snapshot/schema.sql b/db/snapshot/schema.sql index 1e5dd2e..87ddffe 100644 --- a/db/snapshot/schema.sql +++ b/db/snapshot/schema.sql @@ -70,6 +70,7 @@ ALTER TYPE "public"."hypothesis_metric_source" OWNER TO "postgres"; CREATE OR REPLACE FUNCTION "public"."accept_invite"("p_token" "text", "p_user_id" "uuid") RETURNS "jsonb" LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ DECLARE v_invite RECORD; @@ -123,7 +124,7 @@ COMMENT ON FUNCTION "public"."accept_invite"("p_token" "text", "p_user_id" "uuid CREATE OR REPLACE FUNCTION "public"."analisar_impacto_mover_bank_account"("p_account_id" "uuid", "p_new_company_id" "uuid") RETURNS "jsonb" LANGUAGE "plpgsql" SECURITY DEFINER - SET "search_path" TO 'public' + SET "search_path" TO 'public', 'pg_temp' AS $$ DECLARE v_old_company_id UUID; @@ -263,7 +264,7 @@ COMMENT ON FUNCTION "public"."analisar_impacto_mover_bank_account"("p_account_id CREATE OR REPLACE FUNCTION "public"."apply_demo_snapshot"("p_state" "text", "p_project_id" "uuid") RETURNS "jsonb" LANGUAGE "plpgsql" SECURITY DEFINER - SET "search_path" TO 'public' + SET "search_path" TO 'public', 'pg_temp' AS $_$ DECLARE v_is_demo boolean; @@ -389,6 +390,7 @@ ALTER FUNCTION "public"."auto_move_to_em_contato"() OWNER TO "postgres"; CREATE OR REPLACE FUNCTION "public"."avancar_jornada_etapa"("p_project_id" "uuid", "p_nova_etapa" "text") RETURNS "void" LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ DECLARE v_atual TEXT; @@ -684,6 +686,7 @@ COMMENT ON FUNCTION "public"."calculate_monthly_dre_v3"("p_project_id" "uuid", " CREATE OR REPLACE FUNCTION "public"."calculate_risk_score"("p_project_id" "uuid", "p_segmento" "text" DEFAULT 'outro'::"text") RETURNS "jsonb" LANGUAGE "plpgsql" STABLE SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ DECLARE v_snap RECORD; @@ -854,6 +857,7 @@ COMMENT ON FUNCTION "public"."calculate_risk_score"("p_project_id" "uuid", "p_se CREATE OR REPLACE FUNCTION "public"."check_cnpj_duplicates"("cnpjs" "text"[]) RETURNS TABLE("cnpj" "text", "lead_id" "uuid", "razao_social" "text") LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ BEGIN RETURN QUERY @@ -871,6 +875,7 @@ ALTER FUNCTION "public"."check_cnpj_duplicates"("cnpjs" "text"[]) OWNER TO "post CREATE OR REPLACE FUNCTION "public"."compile_financial_context"("p_project_id" "uuid", "p_segmento" "text" DEFAULT 'outro'::"text") RETURNS "jsonb" LANGUAGE "plpgsql" STABLE SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ DECLARE v_snap RECORD; @@ -1044,6 +1049,7 @@ COMMENT ON FUNCTION "public"."compile_financial_context"("p_project_id" "uuid", CREATE OR REPLACE FUNCTION "public"."create_invite"("p_email" "text", "p_role" "text", "p_project_id" "uuid" DEFAULT NULL::"uuid", "p_invited_by" "uuid" DEFAULT NULL::"uuid", "p_expires_days" integer DEFAULT 7) RETURNS TABLE("invite_id" "uuid", "invite_token" "text") LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ DECLARE v_token TEXT; @@ -1094,6 +1100,7 @@ COMMENT ON FUNCTION "public"."create_invite"("p_email" "text", "p_role" "text", CREATE OR REPLACE FUNCTION "public"."create_lite_company_and_user"("company_name" "text") RETURNS "uuid" LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ DECLARE new_company_id UUID; @@ -1119,6 +1126,7 @@ ALTER FUNCTION "public"."create_lite_company_and_user"("company_name" "text") OW CREATE OR REPLACE FUNCTION "public"."extract_financial_gaps"("p_project_id" "uuid", "p_segmento" "text" DEFAULT 'outro'::"text") RETURNS "jsonb" LANGUAGE "plpgsql" STABLE SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $_$ DECLARE v_snap RECORD; @@ -1604,6 +1612,7 @@ ALTER FUNCTION "public"."get_category_breakdown"("p_project_id" "uuid", "p_categ CREATE OR REPLACE FUNCTION "public"."get_dre_readiness"("p_project_id" "uuid") RETURNS TABLE("project_id" "uuid", "publish_status" "text", "snapshots_total" integer, "snapshots_consecutivos" integer, "earliest_closed" "text", "latest_closed" "text", "anos_completos" integer[], "cobertura_atual_dias" integer, "pode_publicar" boolean, "target_threshold" "text", "last_published_at" timestamp with time zone, "published_by" "uuid", "bloqueios" "text"[]) LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ #variable_conflict use_column DECLARE @@ -1875,6 +1884,7 @@ ALTER FUNCTION "public"."get_exercicios_fiscais"("p_project_id" "uuid") OWNER TO CREATE OR REPLACE FUNCTION "public"."get_lancamentos_unicos"("p_project_id" "uuid", "p_ano" integer DEFAULT NULL::integer, "p_mes" integer DEFAULT NULL::integer) RETURNS TABLE("lancamento_tratado" "text", "sentido" "text", "ocorrencias" bigint, "total_credito" numeric, "total_debito" numeric, "categoria_id" "text", "categoria_nome" "text", "subcategoria_id" "text", "subcategoria_nome" "text", "excecoes" bigint, "pendentes" bigint) LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ BEGIN RETURN QUERY @@ -1941,6 +1951,7 @@ COMMENT ON FUNCTION "public"."get_lancamentos_unicos"("p_project_id" "uuid", "p_ CREATE OR REPLACE FUNCTION "public"."get_status_mensal"("p_project_id" "uuid") RETURNS TABLE("mes_referencia" "text", "total" bigint, "categorizadas" bigint, "pendentes" bigint) LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ BEGIN RETURN QUERY @@ -1968,7 +1979,7 @@ ALTER FUNCTION "public"."get_status_mensal"("p_project_id" "uuid") OWNER TO "pos CREATE OR REPLACE FUNCTION "public"."get_user_role"("p_user_id" "uuid") RETURNS "text" LANGUAGE "sql" STABLE SECURITY DEFINER - SET "search_path" TO 'public' + SET "search_path" TO 'public', 'pg_temp' AS $$ SELECT COALESCE(role, 'cliente') FROM profiles WHERE id = p_user_id; $$; @@ -1983,6 +1994,7 @@ COMMENT ON FUNCTION "public"."get_user_role"("p_user_id" "uuid") IS 'Retorna rol CREATE OR REPLACE FUNCTION "public"."guard_bank_account_inactive_company"() RETURNS "trigger" LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ DECLARE v_company_status TEXT; @@ -2012,6 +2024,7 @@ COMMENT ON FUNCTION "public"."guard_bank_account_inactive_company"() IS 'Frente CREATE OR REPLACE FUNCTION "public"."handle_new_lite_company"() RETURNS "trigger" LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ BEGIN INSERT INTO public.lite_categories (company_id, name, type, color) VALUES @@ -2031,7 +2044,7 @@ ALTER FUNCTION "public"."handle_new_lite_company"() OWNER TO "postgres"; CREATE OR REPLACE FUNCTION "public"."handle_new_user"() RETURNS "trigger" LANGUAGE "plpgsql" SECURITY DEFINER - SET "search_path" TO 'public' + SET "search_path" TO 'public', 'pg_temp' AS $$ BEGIN INSERT INTO public.profiles (id, email, nome, role, must_change_password) @@ -2062,6 +2075,7 @@ ALTER FUNCTION "public"."handle_new_user"() OWNER TO "postgres"; CREATE OR REPLACE FUNCTION "public"."increment_assertiva_credits"("credits_to_add" integer DEFAULT 1) RETURNS "void" LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ BEGIN UPDATE assertiva_config @@ -2077,10 +2091,11 @@ ALTER FUNCTION "public"."increment_assertiva_credits"("credits_to_add" integer) CREATE OR REPLACE FUNCTION "public"."increment_pattern_hit"("pattern_id" "uuid") RETURNS "void" - LANGUAGE "plpgsql" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ BEGIN - UPDATE categorization_patterns SET hit_count = hit_count + 1 WHERE id = pattern_id; + UPDATE public.categorization_patterns SET hit_count = hit_count + 1 WHERE id = pattern_id; END; $$; @@ -2090,7 +2105,7 @@ ALTER FUNCTION "public"."increment_pattern_hit"("pattern_id" "uuid") OWNER TO "p CREATE OR REPLACE FUNCTION "public"."invalidar_endividamento_por_ancora"() RETURNS "trigger" LANGUAGE "plpgsql" SECURITY DEFINER - SET "search_path" TO 'public' + SET "search_path" TO 'public', 'pg_temp' AS $$ BEGIN DELETE FROM "public"."endividamento_snapshots" es @@ -2115,6 +2130,7 @@ COMMENT ON FUNCTION "public"."invalidar_endividamento_por_ancora"() IS 'Stale-by CREATE OR REPLACE FUNCTION "public"."is_admin"("p_user_id" "uuid") RETURNS boolean LANGUAGE "plpgsql" STABLE SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ BEGIN RETURN get_user_role(p_user_id) IN ('owner', 'admin'); @@ -2131,7 +2147,7 @@ COMMENT ON FUNCTION "public"."is_admin"("p_user_id" "uuid") IS 'Retorna TRUE par CREATE OR REPLACE FUNCTION "public"."is_admin_or_sdr"("p_user_id" "uuid") RETURNS boolean LANGUAGE "sql" STABLE SECURITY DEFINER - SET "search_path" TO 'public' + SET "search_path" TO 'public', 'pg_temp' AS $$ SELECT COALESCE((SELECT role IN ('admin', 'sdr') FROM profiles WHERE id = p_user_id), false); $$; @@ -2142,6 +2158,7 @@ ALTER FUNCTION "public"."is_admin_or_sdr"("p_user_id" "uuid") OWNER TO "postgres CREATE OR REPLACE FUNCTION "public"."is_consultor"("p_user_id" "uuid") RETURNS boolean LANGUAGE "plpgsql" STABLE SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ BEGIN RETURN get_user_role(p_user_id) = 'consultor'; @@ -2158,6 +2175,7 @@ COMMENT ON FUNCTION "public"."is_consultor"("p_user_id" "uuid") IS 'Retorna TRUE CREATE OR REPLACE FUNCTION "public"."list_consultores"() RETURNS TABLE("id" "uuid", "nome" "text", "email" "text", "role" "text") LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ BEGIN RETURN QUERY @@ -2184,6 +2202,7 @@ COMMENT ON FUNCTION "public"."list_consultores"() IS 'Lista owner, admin e consu CREATE OR REPLACE FUNCTION "public"."lite_get_company_id"() RETURNS "uuid" LANGUAGE "sql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ SELECT company_id FROM public.lite_users WHERE id = auth.uid() LIMIT 1; $$; @@ -2194,7 +2213,7 @@ ALTER FUNCTION "public"."lite_get_company_id"() OWNER TO "postgres"; CREATE OR REPLACE FUNCTION "public"."marcar_snapshot_desatualizado"() RETURNS "trigger" LANGUAGE "plpgsql" SECURITY DEFINER - SET "search_path" TO 'public' + SET "search_path" TO 'public', 'pg_temp' AS $$ DECLARE v_meses jsonb; -- [{project_id, mes, linhas}] @@ -2289,6 +2308,7 @@ COMMENT ON FUNCTION "public"."marcar_snapshot_desatualizado"() IS 'Stale-by-even CREATE OR REPLACE FUNCTION "public"."mark_dossier_stale"("p_project_id" "uuid", "p_reason" "jsonb") RETURNS "void" LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ BEGIN IF p_project_id IS NULL THEN @@ -2327,7 +2347,7 @@ COMMENT ON FUNCTION "public"."mark_dossier_stale"("p_project_id" "uuid", "p_reas CREATE OR REPLACE FUNCTION "public"."move_bank_account_with_transactions"("p_account_id" "uuid", "p_new_company_id" "uuid") RETURNS "jsonb" LANGUAGE "plpgsql" SECURITY DEFINER - SET "search_path" TO 'public' + SET "search_path" TO 'public', 'pg_temp' AS $$ DECLARE v_old_company_id UUID; @@ -2504,6 +2524,7 @@ ALTER FUNCTION "public"."nivel_jornada_etapa"("p_etapa" "text") OWNER TO "postgr CREATE OR REPLACE FUNCTION "public"."recalcular_progresso_modulo"() RETURNS "trigger" LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ DECLARE v_module_id UUID; @@ -2576,6 +2597,7 @@ ALTER FUNCTION "public"."recalcular_progresso_modulo"() OWNER TO "postgres"; CREATE OR REPLACE FUNCTION "public"."recalcular_progresso_projeto"() RETURNS "trigger" LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ DECLARE v_project_id UUID; @@ -2613,7 +2635,7 @@ ALTER FUNCTION "public"."recalcular_progresso_projeto"() OWNER TO "postgres"; CREATE OR REPLACE FUNCTION "public"."registrar_ancora_saldo"("p_bank_account_id" "uuid", "p_project_id" "uuid", "p_balance_amount" numeric, "p_as_of" "date", "p_balance_type" "text" DEFAULT 'ledger'::"text", "p_source_document_id" "uuid" DEFAULT NULL::"uuid") RETURNS "jsonb" LANGUAGE "plpgsql" SECURITY DEFINER - SET "search_path" TO 'public' + SET "search_path" TO 'public', 'pg_temp' AS $$ DECLARE v_uid UUID := auth.uid(); @@ -2731,6 +2753,7 @@ COMMENT ON FUNCTION "public"."registrar_ancora_saldo"("p_bank_account_id" "uuid" CREATE OR REPLACE FUNCTION "public"."reset_assertiva_monthly_credits"() RETURNS "void" LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ BEGIN UPDATE assertiva_config @@ -2748,7 +2771,7 @@ ALTER FUNCTION "public"."reset_assertiva_monthly_credits"() OWNER TO "postgres"; CREATE OR REPLACE FUNCTION "public"."reset_demo_project"("p_project_id" "uuid") RETURNS "jsonb" LANGUAGE "plpgsql" SECURITY DEFINER - SET "search_path" TO 'public' + SET "search_path" TO 'public', 'pg_temp' AS $$ DECLARE v_is_demo boolean; @@ -2936,6 +2959,7 @@ ALTER FUNCTION "public"."touch_loan_contracts_updated_at"() OWNER TO "postgres"; CREATE OR REPLACE FUNCTION "public"."trg_jornada_categorizacao"() RETURNS "trigger" LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ DECLARE v_total INT; @@ -2965,6 +2989,7 @@ ALTER FUNCTION "public"."trg_jornada_categorizacao"() OWNER TO "postgres"; CREATE OR REPLACE FUNCTION "public"."trg_jornada_impeditivos"() RETURNS "trigger" LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ BEGIN IF NEW.project_id IS NOT NULL AND NEW.resultado IS NOT NULL THEN @@ -2980,6 +3005,7 @@ ALTER FUNCTION "public"."trg_jornada_impeditivos"() OWNER TO "postgres"; CREATE OR REPLACE FUNCTION "public"."trg_jornada_primeira_dre"() RETURNS "trigger" LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ BEGIN IF NEW.project_id IS NOT NULL THEN @@ -2995,6 +3021,7 @@ ALTER FUNCTION "public"."trg_jornada_primeira_dre"() OWNER TO "postgres"; CREATE OR REPLACE FUNCTION "public"."trg_jornada_upload_feito"() RETURNS "trigger" LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ BEGIN IF NEW.project_id IS NOT NULL THEN @@ -3010,6 +3037,7 @@ ALTER FUNCTION "public"."trg_jornada_upload_feito"() OWNER TO "postgres"; CREATE OR REPLACE FUNCTION "public"."trg_mark_dossier_stale_direct"() RETURNS "trigger" LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ DECLARE v_project_id UUID; @@ -3085,6 +3113,7 @@ COMMENT ON FUNCTION "public"."trg_mark_dossier_stale_module_analyses"() IS 'Fix CREATE OR REPLACE FUNCTION "public"."trg_mark_dossier_stale_via_client"() RETURNS "trigger" LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ DECLARE v_client_id UUID; @@ -3582,6 +3611,7 @@ ALTER FUNCTION "public"."validar_soma_transacao_linha_split"() OWNER TO "postgre CREATE OR REPLACE FUNCTION "public"."validate_invite"("p_token" "text") RETURNS "jsonb" LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' AS $$ DECLARE v_invite RECORD; @@ -11670,6 +11700,29 @@ CREATE POLICY "categories_read" ON "public"."categories" FOR SELECT USING (("aut +ALTER TABLE "public"."categorization_patterns" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "categorization_patterns_delete" ON "public"."categorization_patterns" FOR DELETE USING ("public"."is_admin"("auth"."uid"())); + + + +CREATE POLICY "categorization_patterns_insert" ON "public"."categorization_patterns" FOR INSERT WITH CHECK (("public"."is_admin"("auth"."uid"()) OR (EXISTS ( SELECT 1 + FROM "public"."profiles" "p" + WHERE (("p"."id" = "auth"."uid"()) AND ("p"."role" = ANY (ARRAY['owner'::"text", 'admin'::"text", 'consultor'::"text"]))))))); + + + +CREATE POLICY "categorization_patterns_select" ON "public"."categorization_patterns" FOR SELECT TO "authenticated" USING (true); + + + +CREATE POLICY "categorization_patterns_update" ON "public"."categorization_patterns" FOR UPDATE USING (("public"."is_admin"("auth"."uid"()) OR (EXISTS ( SELECT 1 + FROM "public"."profiles" "p" + WHERE (("p"."id" = "auth"."uid"()) AND ("p"."role" = ANY (ARRAY['owner'::"text", 'admin'::"text", 'consultor'::"text"]))))))); + + + ALTER TABLE "public"."categorization_rules" ENABLE ROW LEVEL SECURITY; @@ -11976,6 +12029,72 @@ CREATE POLICY "companies_update" ON "public"."companies" FOR UPDATE USING (("pub +ALTER TABLE "public"."company_ai_dossiers" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "company_ai_dossiers_delete" ON "public"."company_ai_dossiers" FOR DELETE USING (("public"."is_admin"("auth"."uid"()) OR (EXISTS ( SELECT 1 + FROM "public"."profiles" "p" + WHERE (("p"."id" = "auth"."uid"()) AND ("p"."role" = ANY (ARRAY['owner'::"text", 'admin'::"text", 'consultor'::"text"]))))))); + + + +CREATE POLICY "company_ai_dossiers_insert" ON "public"."company_ai_dossiers" FOR INSERT WITH CHECK (("public"."is_admin"("auth"."uid"()) OR (EXISTS ( SELECT 1 + FROM "public"."profiles" "p" + WHERE (("p"."id" = "auth"."uid"()) AND ("p"."role" = ANY (ARRAY['owner'::"text", 'admin'::"text", 'consultor'::"text"]))))) OR ("project_id" IN ( SELECT "client_project_access"."project_id" + FROM "public"."client_project_access" + WHERE ("client_project_access"."user_id" = "auth"."uid"()))))); + + + +CREATE POLICY "company_ai_dossiers_select" ON "public"."company_ai_dossiers" FOR SELECT USING (("public"."is_admin"("auth"."uid"()) OR (EXISTS ( SELECT 1 + FROM "public"."profiles" "p" + WHERE (("p"."id" = "auth"."uid"()) AND ("p"."role" = ANY (ARRAY['owner'::"text", 'admin'::"text", 'consultor'::"text"]))))) OR ("project_id" IN ( SELECT "client_project_access"."project_id" + FROM "public"."client_project_access" + WHERE ("client_project_access"."user_id" = "auth"."uid"()))))); + + + +CREATE POLICY "company_ai_dossiers_update" ON "public"."company_ai_dossiers" FOR UPDATE USING (("public"."is_admin"("auth"."uid"()) OR (EXISTS ( SELECT 1 + FROM "public"."profiles" "p" + WHERE (("p"."id" = "auth"."uid"()) AND ("p"."role" = ANY (ARRAY['owner'::"text", 'admin'::"text", 'consultor'::"text"]))))) OR ("project_id" IN ( SELECT "client_project_access"."project_id" + FROM "public"."client_project_access" + WHERE ("client_project_access"."user_id" = "auth"."uid"()))))); + + + +ALTER TABLE "public"."consultant_memories" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "consultant_memories_delete" ON "public"."consultant_memories" FOR DELETE USING (("public"."is_admin"("auth"."uid"()) OR (EXISTS ( SELECT 1 + FROM "public"."profiles" "p" + WHERE (("p"."id" = "auth"."uid"()) AND ("p"."role" = ANY (ARRAY['owner'::"text", 'admin'::"text", 'consultor'::"text"]))))))); + + + +CREATE POLICY "consultant_memories_insert" ON "public"."consultant_memories" FOR INSERT WITH CHECK (("public"."is_admin"("auth"."uid"()) OR (EXISTS ( SELECT 1 + FROM "public"."profiles" "p" + WHERE (("p"."id" = "auth"."uid"()) AND ("p"."role" = ANY (ARRAY['owner'::"text", 'admin'::"text", 'consultor'::"text"]))))) OR ("project_id" IN ( SELECT "client_project_access"."project_id" + FROM "public"."client_project_access" + WHERE ("client_project_access"."user_id" = "auth"."uid"()))))); + + + +CREATE POLICY "consultant_memories_select" ON "public"."consultant_memories" FOR SELECT USING (("public"."is_admin"("auth"."uid"()) OR (EXISTS ( SELECT 1 + FROM "public"."profiles" "p" + WHERE (("p"."id" = "auth"."uid"()) AND ("p"."role" = ANY (ARRAY['owner'::"text", 'admin'::"text", 'consultor'::"text"]))))) OR ("project_id" IN ( SELECT "client_project_access"."project_id" + FROM "public"."client_project_access" + WHERE ("client_project_access"."user_id" = "auth"."uid"()))))); + + + +CREATE POLICY "consultant_memories_update" ON "public"."consultant_memories" FOR UPDATE USING (("public"."is_admin"("auth"."uid"()) OR (EXISTS ( SELECT 1 + FROM "public"."profiles" "p" + WHERE (("p"."id" = "auth"."uid"()) AND ("p"."role" = ANY (ARRAY['owner'::"text", 'admin'::"text", 'consultor'::"text"]))))) OR ("project_id" IN ( SELECT "client_project_access"."project_id" + FROM "public"."client_project_access" + WHERE ("client_project_access"."user_id" = "auth"."uid"()))))); + + + ALTER TABLE "public"."contas_pagar" ENABLE ROW LEVEL SECURITY; @@ -12226,6 +12345,9 @@ CREATE POLICY "dre_reports_update" ON "public"."dre_reports" FOR UPDATE USING (( ALTER TABLE "public"."dre_subcategories" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "public"."employees" ENABLE ROW LEVEL SECURITY; + + CREATE POLICY "employees_cliente_insert" ON "public"."employees" FOR INSERT WITH CHECK (("public"."is_admin"("auth"."uid"()) OR (EXISTS ( SELECT 1 FROM "public"."profiles" "p" WHERE (("p"."id" = "auth"."uid"()) AND ("p"."role" = ANY (ARRAY['owner'::"text", 'admin'::"text", 'consultor'::"text"]))))) OR ("project_id" IN ( SELECT "cpa"."project_id" @@ -12251,20 +12373,26 @@ CREATE POLICY "employees_delete" ON "public"."employees" FOR DELETE USING ("publ CREATE POLICY "employees_insert" ON "public"."employees" FOR INSERT WITH CHECK (("public"."is_admin"("auth"."uid"()) OR (EXISTS ( SELECT 1 - FROM "public"."project_members" "pm" - WHERE (("pm"."project_id" = "employees"."project_id") AND ("pm"."user_id" = "auth"."uid"()) AND ("pm"."role" = 'consultor'::"text")))))); + FROM "public"."profiles" "p" + WHERE (("p"."id" = "auth"."uid"()) AND ("p"."role" = ANY (ARRAY['owner'::"text", 'admin'::"text", 'consultor'::"text"]))))) OR ("project_id" IN ( SELECT "client_project_access"."project_id" + FROM "public"."client_project_access" + WHERE ("client_project_access"."user_id" = "auth"."uid"()))))); -CREATE POLICY "employees_select" ON "public"."employees" FOR SELECT USING (("public"."is_admin"("auth"."uid"()) OR ("project_id" IN ( SELECT "pm"."project_id" - FROM "public"."project_members" "pm" - WHERE ("pm"."user_id" = "auth"."uid"()))))); +CREATE POLICY "employees_select" ON "public"."employees" FOR SELECT USING (("public"."is_admin"("auth"."uid"()) OR (EXISTS ( SELECT 1 + FROM "public"."profiles" "p" + WHERE (("p"."id" = "auth"."uid"()) AND ("p"."role" = ANY (ARRAY['owner'::"text", 'admin'::"text", 'consultor'::"text"]))))) OR ("project_id" IN ( SELECT "client_project_access"."project_id" + FROM "public"."client_project_access" + WHERE ("client_project_access"."user_id" = "auth"."uid"()))))); CREATE POLICY "employees_update" ON "public"."employees" FOR UPDATE USING (("public"."is_admin"("auth"."uid"()) OR (EXISTS ( SELECT 1 - FROM "public"."project_members" "pm" - WHERE (("pm"."project_id" = "employees"."project_id") AND ("pm"."user_id" = "auth"."uid"()) AND ("pm"."role" = 'consultor'::"text")))))); + FROM "public"."profiles" "p" + WHERE (("p"."id" = "auth"."uid"()) AND ("p"."role" = ANY (ARRAY['owner'::"text", 'admin'::"text", 'consultor'::"text"]))))) OR ("project_id" IN ( SELECT "client_project_access"."project_id" + FROM "public"."client_project_access" + WHERE ("client_project_access"."user_id" = "auth"."uid"()))))); @@ -12893,6 +13021,9 @@ CREATE POLICY "margin_items_update" ON "public"."margin_items" FOR UPDATE USING +ALTER TABLE "public"."meetings" ENABLE ROW LEVEL SECURITY; + + ALTER TABLE "public"."module_analyses" ENABLE ROW LEVEL SECURITY; diff --git a/supabase/migrations/20260812000000_rls_tabelas_orfas_e_search_path.sql b/supabase/migrations/20260812000000_rls_tabelas_orfas_e_search_path.sql new file mode 100644 index 0000000..0ebd1e7 --- /dev/null +++ b/supabase/migrations/20260812000000_rls_tabelas_orfas_e_search_path.sql @@ -0,0 +1,335 @@ +-- ============================================================================= +-- RLS nas 5 tabelas desprotegidas + search_path fixo nas funções SECURITY DEFINER +-- ============================================================================= +-- +-- Achado pelo cruzamento banco × código (`db/ESTRUTURA.md`, gerado por +-- `scripts/db/gerar-estrutura.ts`) depois do marco zero de 2026-08-11. +-- +-- O buraco: 5 das 112 tabelas estavam com RLS DESLIGADA e `GRANT ALL TO anon`. +-- A chave anônima é pública — vai no bundle do frontend. Sem RLS, qualquer um +-- com essa chave lê e escreve nessas tabelas. +-- +-- Hoje não vaza nada: as 5 estão com ZERO linhas. Mas `employees` guarda +-- `salario_bruto`, benefícios e `cpf_mascara`, e tem 17 usos em `src/` (Custo +-- Hora-Homem). No dia que a feature for usada, folha de pagamento real fica +-- legível e gravável por qualquer um. É por isso que isto entra agora: com as +-- tabelas vazias e sem tráfego, não há o que quebrar. +-- +-- Padrão copiado de `ingestion_documents` (migration 20260617), que é o caso +-- análogo — cliente escreve no próprio projeto. Três cláusulas: +-- 1. admin 2. staff ATR (owner/admin/consultor) 3. cliente com acesso AO projeto +-- +-- Os GRANTs para `anon` ficam como estão: é o default do Supabase e, com RLS +-- ligada e policy exigindo `auth.uid()`, eles viram inertes (anon tem uid nulo). +-- +-- Idempotente: pode rodar de novo sem erro. +-- ============================================================================= + + +-- ----------------------------------------------------------------------------- +-- 1. employees — colaboradores para Custo Hora-Homem (salário + CPF mascarado) +-- Leitura/escrita pelo browser em chh-service e folha-service. +-- ----------------------------------------------------------------------------- +ALTER TABLE "public"."employees" ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS "employees_select" ON "public"."employees"; +CREATE POLICY "employees_select" ON "public"."employees" FOR SELECT USING ( + public.is_admin(auth.uid()) + OR EXISTS ( + SELECT 1 FROM public.profiles p + WHERE p.id = auth.uid() AND p.role = ANY (ARRAY['owner'::text, 'admin'::text, 'consultor'::text]) + ) + OR project_id IN ( + SELECT client_project_access.project_id FROM public.client_project_access + WHERE client_project_access.user_id = auth.uid() + ) +); + +DROP POLICY IF EXISTS "employees_insert" ON "public"."employees"; +CREATE POLICY "employees_insert" ON "public"."employees" FOR INSERT WITH CHECK ( + public.is_admin(auth.uid()) + OR EXISTS ( + SELECT 1 FROM public.profiles p + WHERE p.id = auth.uid() AND p.role = ANY (ARRAY['owner'::text, 'admin'::text, 'consultor'::text]) + ) + OR project_id IN ( + SELECT client_project_access.project_id FROM public.client_project_access + WHERE client_project_access.user_id = auth.uid() + ) +); + +DROP POLICY IF EXISTS "employees_update" ON "public"."employees"; +CREATE POLICY "employees_update" ON "public"."employees" FOR UPDATE USING ( + public.is_admin(auth.uid()) + OR EXISTS ( + SELECT 1 FROM public.profiles p + WHERE p.id = auth.uid() AND p.role = ANY (ARRAY['owner'::text, 'admin'::text, 'consultor'::text]) + ) + OR project_id IN ( + SELECT client_project_access.project_id FROM public.client_project_access + WHERE client_project_access.user_id = auth.uid() + ) +); + +DROP POLICY IF EXISTS "employees_delete" ON "public"."employees"; +CREATE POLICY "employees_delete" ON "public"."employees" FOR DELETE USING ( + public.is_admin(auth.uid()) + OR EXISTS ( + SELECT 1 FROM public.profiles p + WHERE p.id = auth.uid() AND p.role = ANY (ARRAY['owner'::text, 'admin'::text, 'consultor'::text]) + ) + OR project_id IN ( + SELECT client_project_access.project_id FROM public.client_project_access + WHERE client_project_access.user_id = auth.uid() + ) +); + + +-- ----------------------------------------------------------------------------- +-- 2. company_ai_dossiers — dossiê da empresa compilado pela IA (por projeto) +-- Lido/escrito server-side em context-compiler-service e auto-conclusion-service, +-- mas com a chave ANON + sessão do usuário: RLS vale. +-- ----------------------------------------------------------------------------- +ALTER TABLE "public"."company_ai_dossiers" ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS "company_ai_dossiers_select" ON "public"."company_ai_dossiers"; +CREATE POLICY "company_ai_dossiers_select" ON "public"."company_ai_dossiers" FOR SELECT USING ( + public.is_admin(auth.uid()) + OR EXISTS ( + SELECT 1 FROM public.profiles p + WHERE p.id = auth.uid() AND p.role = ANY (ARRAY['owner'::text, 'admin'::text, 'consultor'::text]) + ) + OR project_id IN ( + SELECT client_project_access.project_id FROM public.client_project_access + WHERE client_project_access.user_id = auth.uid() + ) +); + +DROP POLICY IF EXISTS "company_ai_dossiers_insert" ON "public"."company_ai_dossiers"; +CREATE POLICY "company_ai_dossiers_insert" ON "public"."company_ai_dossiers" FOR INSERT WITH CHECK ( + public.is_admin(auth.uid()) + OR EXISTS ( + SELECT 1 FROM public.profiles p + WHERE p.id = auth.uid() AND p.role = ANY (ARRAY['owner'::text, 'admin'::text, 'consultor'::text]) + ) + OR project_id IN ( + SELECT client_project_access.project_id FROM public.client_project_access + WHERE client_project_access.user_id = auth.uid() + ) +); + +DROP POLICY IF EXISTS "company_ai_dossiers_update" ON "public"."company_ai_dossiers"; +CREATE POLICY "company_ai_dossiers_update" ON "public"."company_ai_dossiers" FOR UPDATE USING ( + public.is_admin(auth.uid()) + OR EXISTS ( + SELECT 1 FROM public.profiles p + WHERE p.id = auth.uid() AND p.role = ANY (ARRAY['owner'::text, 'admin'::text, 'consultor'::text]) + ) + OR project_id IN ( + SELECT client_project_access.project_id FROM public.client_project_access + WHERE client_project_access.user_id = auth.uid() + ) +); + +DROP POLICY IF EXISTS "company_ai_dossiers_delete" ON "public"."company_ai_dossiers"; +CREATE POLICY "company_ai_dossiers_delete" ON "public"."company_ai_dossiers" FOR DELETE USING ( + public.is_admin(auth.uid()) + OR EXISTS ( + SELECT 1 FROM public.profiles p + WHERE p.id = auth.uid() AND p.role = ANY (ARRAY['owner'::text, 'admin'::text, 'consultor'::text]) + ) +); + + +-- ----------------------------------------------------------------------------- +-- 3. consultant_memories — memória do consultor IA (por projeto) +-- O código JÁ acreditava que havia RLS aqui: learning-memory.ts:30 traz o +-- comentário "a RLS de consultant_memories barraria o insert sem sessão". +-- O banco é que discordava. +-- ----------------------------------------------------------------------------- +ALTER TABLE "public"."consultant_memories" ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS "consultant_memories_select" ON "public"."consultant_memories"; +CREATE POLICY "consultant_memories_select" ON "public"."consultant_memories" FOR SELECT USING ( + public.is_admin(auth.uid()) + OR EXISTS ( + SELECT 1 FROM public.profiles p + WHERE p.id = auth.uid() AND p.role = ANY (ARRAY['owner'::text, 'admin'::text, 'consultor'::text]) + ) + OR project_id IN ( + SELECT client_project_access.project_id FROM public.client_project_access + WHERE client_project_access.user_id = auth.uid() + ) +); + +DROP POLICY IF EXISTS "consultant_memories_insert" ON "public"."consultant_memories"; +CREATE POLICY "consultant_memories_insert" ON "public"."consultant_memories" FOR INSERT WITH CHECK ( + public.is_admin(auth.uid()) + OR EXISTS ( + SELECT 1 FROM public.profiles p + WHERE p.id = auth.uid() AND p.role = ANY (ARRAY['owner'::text, 'admin'::text, 'consultor'::text]) + ) + OR project_id IN ( + SELECT client_project_access.project_id FROM public.client_project_access + WHERE client_project_access.user_id = auth.uid() + ) +); + +DROP POLICY IF EXISTS "consultant_memories_update" ON "public"."consultant_memories"; +CREATE POLICY "consultant_memories_update" ON "public"."consultant_memories" FOR UPDATE USING ( + public.is_admin(auth.uid()) + OR EXISTS ( + SELECT 1 FROM public.profiles p + WHERE p.id = auth.uid() AND p.role = ANY (ARRAY['owner'::text, 'admin'::text, 'consultor'::text]) + ) + OR project_id IN ( + SELECT client_project_access.project_id FROM public.client_project_access + WHERE client_project_access.user_id = auth.uid() + ) +); + +DROP POLICY IF EXISTS "consultant_memories_delete" ON "public"."consultant_memories"; +CREATE POLICY "consultant_memories_delete" ON "public"."consultant_memories" FOR DELETE USING ( + public.is_admin(auth.uid()) + OR EXISTS ( + SELECT 1 FROM public.profiles p + WHERE p.id = auth.uid() AND p.role = ANY (ARRAY['owner'::text, 'admin'::text, 'consultor'::text]) + ) +); + + +-- ----------------------------------------------------------------------------- +-- 4. categorization_patterns — catálogo GLOBAL de padrões de auto-categorização +-- Não tem project_id: é dado de referência do produto, igual para todo mundo. +-- Por isso a regra é outra: todo usuário logado LÊ, só a equipe ATR ESCREVE. +-- +-- ⚠ Armadilha LATENTE, desarmada aqui: `increment_pattern_hit` é plpgsql +-- INVOKER e faz UPDATE nesta tabela. Com RLS ligada e sem policy de UPDATE +-- para o usuário comum, o contador morreria EM SILÊNCIO — UPDATE que casa +-- zero linhas não levanta erro. +-- +-- Hoje ela não morde: o único chamador é `auto-categorizer-enhanced.ts`, que +-- está MORTO — nenhum dos seus 3 exports (autoCategorize, +-- autoCategorizeTransacoes, getCategorizationStats) é importado por ninguém. +-- A categorização viva é `categorization-rules.ts`, que não toca esta tabela. +-- Desarmar agora é barato e evita que a revivência do módulo caia na +-- armadilha: em vez de abrir UPDATE para todo mundo (qualquer um poderia +-- reescrever o catálogo), a função vira SECURITY DEFINER — caminho de escrita +-- controlado, que só sabe somar 1 no hit_count de um id. +-- ----------------------------------------------------------------------------- +ALTER TABLE "public"."categorization_patterns" ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS "categorization_patterns_select" ON "public"."categorization_patterns"; +CREATE POLICY "categorization_patterns_select" ON "public"."categorization_patterns" + FOR SELECT TO "authenticated" USING (true); + +DROP POLICY IF EXISTS "categorization_patterns_insert" ON "public"."categorization_patterns"; +CREATE POLICY "categorization_patterns_insert" ON "public"."categorization_patterns" FOR INSERT WITH CHECK ( + public.is_admin(auth.uid()) + OR EXISTS ( + SELECT 1 FROM public.profiles p + WHERE p.id = auth.uid() AND p.role = ANY (ARRAY['owner'::text, 'admin'::text, 'consultor'::text]) + ) +); + +DROP POLICY IF EXISTS "categorization_patterns_update" ON "public"."categorization_patterns"; +CREATE POLICY "categorization_patterns_update" ON "public"."categorization_patterns" FOR UPDATE USING ( + public.is_admin(auth.uid()) + OR EXISTS ( + SELECT 1 FROM public.profiles p + WHERE p.id = auth.uid() AND p.role = ANY (ARRAY['owner'::text, 'admin'::text, 'consultor'::text]) + ) +); + +DROP POLICY IF EXISTS "categorization_patterns_delete" ON "public"."categorization_patterns"; +CREATE POLICY "categorization_patterns_delete" ON "public"."categorization_patterns" FOR DELETE USING ( + public.is_admin(auth.uid()) +); + +-- O contador de acertos precisa atravessar a RLS. CREATE OR REPLACE preserva os +-- GRANTs (só DROP os derruba) e o tipo de retorno não muda — `void` continua `void`. +CREATE OR REPLACE FUNCTION "public"."increment_pattern_hit"("pattern_id" "uuid") RETURNS "void" + LANGUAGE "plpgsql" SECURITY DEFINER + SET "search_path" TO 'public', 'pg_temp' + AS $$ +BEGIN + UPDATE public.categorization_patterns SET hit_count = hit_count + 1 WHERE id = pattern_id; +END; +$$; + + +-- ----------------------------------------------------------------------------- +-- 5. meetings — reuniões. ZERO linhas e ZERO referências em `src/`. +-- RLS ligada SEM policy: nada além de service_role enxerga. É de propósito — +-- para tabela que ninguém lê, policy seria adivinhação. No dia que a feature +-- existir, a policy nasce junto com ela, e o primeiro acesso falha alto (que +-- é o que se quer) em vez de silencioso. +-- ----------------------------------------------------------------------------- +ALTER TABLE "public"."meetings" ENABLE ROW LEVEL SECURITY; + + +-- ============================================================================= +-- 6. search_path fixo nas funções SECURITY DEFINER +-- ============================================================================= +-- +-- `SECURITY DEFINER` roda com os poderes do dono. Sem `search_path` fixo, um +-- schema colocado na frente do `public` sequestra as chamadas de dentro da função. +-- +-- Valor: 'public', 'pg_temp' — com `pg_temp` EXPLÍCITO no fim. Esse detalhe é o +-- ponto: quando `pg_temp` não é listado, o Postgres o procura PRIMEIRO, e um +-- objeto temporário passa na frente. Por isso as 10 funções que já tinham +-- `search_path = 'public'` também entram aqui: pareciam protegidas e não estavam. +-- +-- Conferido antes de escrever: nenhuma das 39 chama função de extensão sem +-- qualificar o schema (uuid_generate_v4, crypt, digest, http_post), então fixar +-- o search_path não quebra nenhuma. Nenhuma tem overload. +-- +-- ALTER FUNCTION (não DROP+CREATE): não toca corpo, assinatura nem GRANT. +-- ----------------------------------------------------------------------------- + +-- As 29 sem search_path nenhum +ALTER FUNCTION "public"."accept_invite"("p_token" "text", "p_user_id" "uuid") SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."avancar_jornada_etapa"("p_project_id" "uuid", "p_nova_etapa" "text") SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."calculate_risk_score"("p_project_id" "uuid", "p_segmento" "text") SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."check_cnpj_duplicates"("cnpjs" "text"[]) SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."compile_financial_context"("p_project_id" "uuid", "p_segmento" "text") SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."create_invite"("p_email" "text", "p_role" "text", "p_project_id" "uuid", "p_invited_by" "uuid", "p_expires_days" integer) SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."create_lite_company_and_user"("company_name" "text") SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."extract_financial_gaps"("p_project_id" "uuid", "p_segmento" "text") SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."get_dre_readiness"("p_project_id" "uuid") SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."get_lancamentos_unicos"("p_project_id" "uuid", "p_ano" integer, "p_mes" integer) SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."get_status_mensal"("p_project_id" "uuid") SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."guard_bank_account_inactive_company"() SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."handle_new_lite_company"() SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."increment_assertiva_credits"("credits_to_add" integer) SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."is_admin"("p_user_id" "uuid") SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."is_consultor"("p_user_id" "uuid") SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."list_consultores"() SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."lite_get_company_id"() SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."mark_dossier_stale"("p_project_id" "uuid", "p_reason" "jsonb") SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."recalcular_progresso_modulo"() SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."recalcular_progresso_projeto"() SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."reset_assertiva_monthly_credits"() SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."trg_jornada_categorizacao"() SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."trg_jornada_impeditivos"() SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."trg_jornada_primeira_dre"() SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."trg_jornada_upload_feito"() SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."trg_mark_dossier_stale_direct"() SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."trg_mark_dossier_stale_via_client"() SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."validate_invite"("p_token" "text") SET "search_path" TO 'public', 'pg_temp'; + +-- As 10 que tinham só 'public' — sem pg_temp explícito, seguiam sequestráveis +ALTER FUNCTION "public"."analisar_impacto_mover_bank_account"("p_account_id" "uuid", "p_new_company_id" "uuid") SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."apply_demo_snapshot"("p_state" "text", "p_project_id" "uuid") SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."get_user_role"("p_user_id" "uuid") SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."handle_new_user"() SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."invalidar_endividamento_por_ancora"() SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."is_admin_or_sdr"("p_user_id" "uuid") SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."marcar_snapshot_desatualizado"() SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."move_bank_account_with_transactions"("p_account_id" "uuid", "p_new_company_id" "uuid") SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."registrar_ancora_saldo"("p_bank_account_id" "uuid", "p_project_id" "uuid", "p_balance_amount" numeric, "p_as_of" "date", "p_balance_type" "text", "p_source_document_id" "uuid") SET "search_path" TO 'public', 'pg_temp'; +ALTER FUNCTION "public"."reset_demo_project"("p_project_id" "uuid") SET "search_path" TO 'public', 'pg_temp'; + + +-- Recarrega o cache de schema do PostgREST. +NOTIFY pgrst, 'reload schema'; diff --git a/supabase/migrations/20260812010000_employees_delete_volta_para_admin.sql b/supabase/migrations/20260812010000_employees_delete_volta_para_admin.sql new file mode 100644 index 0000000..1b9f092 --- /dev/null +++ b/supabase/migrations/20260812010000_employees_delete_volta_para_admin.sql @@ -0,0 +1,46 @@ +-- ============================================================================= +-- Corrige um efeito colateral da 20260812000000: DELETE de employees +-- ============================================================================= +-- +-- O que passou batido: `employees` NÃO era uma tabela sem policy. Ela já tinha +-- SEIS policies — dormentes, porque policy existe mesmo com RLS desligada e +-- simplesmente não é aplicada. A verificação anterior olhou "RLS ligada?" e não +-- olhou "que policies já existem?". +-- +-- Resultado: o `DROP POLICY IF EXISTS` da migration anterior substituiu 4 delas +-- às cegas. Comparando o dump de antes com o de depois, três substituições são +-- inócuas ou necessárias: +-- +-- employees_select — a antiga dava acesso via `project_members` (tabela VAZIA, +-- 0 linhas) e não contemplava staff por `profiles.role`. +-- Sem a cláusula de staff, a tela de CHH pararia de +-- enxergar para o consultor assim que a RLS ligasse. +-- employees_insert — a antiga exigia `project_members.role = 'consultor'` +-- employees_update (vazio). E as policies `employees_cliente_insert` / +-- `employees_cliente_update`, que a migration anterior NÃO +-- tocou, já traziam exatamente a mesma cláusula de staff. +-- Ou seja: não houve ampliação — a permissão já existia. +-- +-- Mas a quarta ampliou de verdade: +-- +-- employees_delete — era `is_admin(auth.uid())` e SÓ. Como `is_admin` é +-- role IN ('owner','admin'), a nova versão passou a +-- permitir que CONSULTOR (e cliente com acesso ao projeto) +-- apagasse linha de folha de pagamento. +-- +-- Isso não foi decisão, foi descuido. A restrição original era deliberada, e +-- nenhum caminho vivo precisa dela: o upload de folha faz INSERT/UPDATE, não +-- DELETE. Volta a ser admin puro. +-- +-- Nota: `employees_cliente_insert` e `employees_cliente_update` seguem no banco, +-- hoje idênticas em efeito a `employees_insert`/`employees_update`. Policies +-- permissivas se somam por OR, então duplicar não muda comportamento — mas é +-- cruft. Limpar fica como follow-up, fora desta correção. +-- ============================================================================= + +DROP POLICY IF EXISTS "employees_delete" ON "public"."employees"; +CREATE POLICY "employees_delete" ON "public"."employees" FOR DELETE USING ( + public.is_admin(auth.uid()) +); + +NOTIFY pgrst, 'reload schema';