diff --git a/.env.example b/.env.example index c9756d720..02821380f 100644 --- a/.env.example +++ b/.env.example @@ -20,10 +20,14 @@ VITE_SUPABASE_URL=https://doufsxqlfjyuvxuezpln.supabase.co # ID do projeto Supabase (ex: abcdefgh) VITE_SUPABASE_PROJECT_ID=doufsxqlfjyuvxuezpln -# Chave PUBLISHABLE (anon key) — segura no client +# Chave anon (pública) — segura no client-side # ⚠️ NUNCA coloque a key real aqui — este arquivo é público no GitHub. # Obtenha a key em: Dashboard → Settings → API → Project API keys → anon public -VITE_SUPABASE_PUBLISHABLE_KEY=sb_publishable_ +# +# Suporte a DOIS nomes de variável (compatibilidade legada + padrão Supabase): +VITE_SUPABASE_ANON_KEY= +# Alias legado (também suportado pelo código, mas prefira ANON_KEY acima): +# VITE_SUPABASE_PUBLISHABLE_KEY= # ---------------------------------------------------------------------------- # OBSERVABILIDADE — GlitchTip / Sentry (opcional, recomendado em produção) diff --git a/.github/workflows/quality-gate.yml b/.github/workflows/quality-gate.yml index 9e689092a..210ee96fe 100644 --- a/.github/workflows/quality-gate.yml +++ b/.github/workflows/quality-gate.yml @@ -1 +1,121 @@ -bmFtZTogUXVhbGl0eSBHYXRlCgpvbjoKICBwdWxsX3JlcXVlc3Q6CiAgICBicmFuY2hlczoKICAgICAgLSBtYWluCiAgcHVzaDoKICAgIGJyYW5jaGVzOgogICAgICAtIG1haW4KCmpvYnM6CiAgcXVhbGl0eS1nYXRlOgogICAgbmFtZTogVHlwZVNjcmlwdCArIEVTTGludCBHYXRlCiAgICBydW5zLW9uOiB1YnVudHUtbGF0ZXN0CiAgICB0aW1lb3V0LW1pbnV0ZXM6IDE1CgogICAgc3RlcHM6CiAgICAgIC0gbmFtZTogQ2hlY2tvdXQKICAgICAgICB1c2VzOiBhY3Rpb25zL2NoZWNrb3V0QHY0CgogICAgICAtIG5hbWU6IFNldHVwIE5vZGUKICAgICAgICB1c2VzOiBhY3Rpb25zL3NldHVwLW5vZGVAdjQKICAgICAgICB3aXRoOgogICAgICAgICAgbm9kZS12ZXJzaW9uLWZpbGU6IC5udm1yYwogICAgICAgICAgY2FjaGU6IG5wbQoKICAgICAgLSBuYW1lOiBJbnN0YWxsIGRlcGVuZGVuY2llcwogICAgICAgIHJ1bjogbnBtIGNpIC0tcHJlZmVyLW9mZmxpbmUKCiAgICAgICMgR0FURSAxOiBUeXBlU2NyaXB0IC0gemVybyByZWdyZXNzaW9ucyB2cyBiYXNlbGluZQogICAgICAtIG5hbWU6IFR5cGVTY3JpcHQgZ2F0ZSAoemVybyByZWdyZXNzaW9ucykKICAgICAgICBydW46IG5vZGUgc2NyaXB0cy9jaGVjay10c2MtYmFzZWxpbmUubWpzCiAgICAgICAgZW52OgogICAgICAgICAgVklURV9TVVBBQKFTRV9VUkw6ICRkdW1teQogICAgICAgICAgVklURV9TVVBBQkFTRV9BTk9OX0tFWTogJGR1bW15CgogICAgICAjIEdBVEUgMjogRVNMaW50IC0gemVybyByZWdyZXNzaW9ucyB2cyBiYXNlbGluZQogICAgICAtIG5hbWU6IEVTTGludCBnYXRlICh6ZXJvIHJlZ3Jlc3Npb25zKQogICAgICAgIHJ1bjogbnBtIHJ1biBsaW50OmJhc2VsaW5lCiAgICAgICAgZW52OgogICAgICAgICAgVklURV9TVVBBQKFTRV9VUkw6ICRkdW1teQogICAgICAgICAgVklURV9TVVBBQkFTRV9BTk9OX0tFWTogJGR1bW15CgogICAgICAjIEdBVEUgMzogQnVpbGQgKHZlcmlmeSBubyBidWlsZCBicmVha2FnZSkKICAgICAgLSBuYW1lOiBCdWlsZCBjaGVjawogICAgICAgIHJ1bjogbnBtIHJ1biBidWlsZAogICAgICAgIGVudjoKICAgICAgICAgIFZJVEVfU1VQQUJBU0VfVVJMOiAkZHVtbXkKICAgICAgICAgIFZJVEVfU1VQQUJBU0VfQU5PTl9LRVk6ICRkdW1teQoKICAjIEdBVEUgNDogVml0ZXN0IHVuaXQgdGVzdHMgKGZhc3QgLSBubyBuZXR3b3JrKQogIHVuaXQtdGVzdHM6CiAgICBuYW1lOiBWaXRlc3QgVW5pdCBUZXN0cwogICAgcnVucy1vbjogdWJ1bnR1LWxhdGVzdAogICAgdGltZW91dC1taW51dGVzOiAxMAoKICAgIHN0ZXBzOgogICAgICAtIG5hbWU6IENoZWNrb3V0CiAgICAgICAgdXNlczogYWN0aW9ucy9jaGVja291dEB2NAoKICAgICAgLSBuYW1lOiBTZXR1cCBOb2RlCiAgICAgICAgdXNlczogYWN0aW9ucy9zZXR1cC1ub2RlQHY0CiAgICAgICAgd2l0aDoKICAgICAgICAgIG5vZGUtdmVyc2lvbi1maWxlOiAubnZtcmMKICAgICAgICAgIGNhY2hlOiBucG0KCiAgICAgIC0gbmFtZTogSW5zdGFsbCBkZXBlbmRlbmNpZXMKICAgICAgICBydW46IG5wbSBjaSAtLXByZWZlci1vZmZsaW5lCgogICAgICAtIG5hbWU6IFJ1biB1bml0IHRlc3RzCiAgICAgICAgcnVuOiBucG0gcnVuIHRlc3Q6Y2ktY29yZQogICAgICAgIGVudjoKICAgICAgICAgIFZJVEVfU1VQQUJBU0VfVVJMOiAkZHVtbXkKICAgICAgICAgIFZJVEVfU1VQQUJBU0VfQU5PTl9LRVk6ICRkdW1teQo= \ No newline at end of file +name: Quality Gate + +on: + pull_request: + branches: + - main + push: + branches: + - main + +jobs: + quality-gate: + name: TypeScript + ESLint Gate + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + + - name: Install dependencies + run: npm ci --prefer-offline + + # GATE 1: TypeScript - zero regressions vs baseline + - name: TypeScript gate (zero regressions) + run: node scripts/check-tsc-baseline.mjs + env: + VITE_SUPABASE_URL: ${{ vars.VITE_SUPABASE_URL || 'https://doufsxqlfjyuvxuezpln.supabase.co' }} + VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY || 'dummy-key-for-typecheck' }} + + # GATE 2: ESLint - zero regressions vs baseline + - name: ESLint gate (zero regressions) + run: npm run lint:baseline + env: + VITE_SUPABASE_URL: ${{ vars.VITE_SUPABASE_URL || 'https://doufsxqlfjyuvxuezpln.supabase.co' }} + VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY || 'dummy-key-for-typecheck' }} + + # GATE 3: Build (verify no build breakage) + - name: Build check + run: npm run build + env: + VITE_SUPABASE_URL: ${{ vars.VITE_SUPABASE_URL || 'https://doufsxqlfjyuvxuezpln.supabase.co' }} + VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY || 'dummy-key-for-typecheck' }} + + # GATE 4: Vitest unit tests (fast - no network) + unit-tests: + name: Vitest Unit Tests + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + + - name: Install dependencies + run: npm ci --prefer-offline + + - name: Run unit tests + run: npm run test:ci-core + env: + VITE_SUPABASE_URL: ${{ vars.VITE_SUPABASE_URL || 'https://doufsxqlfjyuvxuezpln.supabase.co' }} + VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY || 'dummy-key-for-typecheck' }} + + # GATE 5: Supabase types sync check (detecta type drift) + supabase-types: + name: Supabase Types Drift Check + runs-on: ubuntu-latest + timeout-minutes: 10 + # Roda apenas em PRs para não bloquear push no main sem secrets + if: github.event_name == 'pull_request' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + + - name: Install dependencies + run: npm ci --prefer-offline + + - name: Install Supabase CLI + uses: supabase/setup-cli@v1 + with: + version: latest + + - name: Generate Supabase types + id: gen_types + run: | + npx supabase gen types typescript \ + --project-id doufsxqlfjyuvxuezpln \ + > /tmp/supabase-types-fresh.ts + env: + SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} + continue-on-error: true + + - name: Check for type drift + if: steps.gen_types.outcome == 'success' + run: | + if ! diff -q src/integrations/supabase/types.ts /tmp/supabase-types-fresh.ts > /dev/null 2>&1; then + echo "::warning::Supabase types are out of sync with the database schema!" + echo "::warning::Run: npx supabase gen types typescript --project-id doufsxqlfjyuvxuezpln > src/integrations/supabase/types.ts" + diff src/integrations/supabase/types.ts /tmp/supabase-types-fresh.ts | head -50 + else + echo "Types are in sync ✅" + fi diff --git a/supabase/migrations/20260602130000_fix_ai_description_queue_rls_policy.sql b/supabase/migrations/20260602130000_fix_ai_description_queue_rls_policy.sql new file mode 100644 index 000000000..14a9bc4e6 --- /dev/null +++ b/supabase/migrations/20260602130000_fix_ai_description_queue_rls_policy.sql @@ -0,0 +1,56 @@ +-- ============================================================ +-- Migration: Corrige policy perigosa na tabela ai_description_queue +-- Auditoria 2026-06-02 — RISCO-1 identificado +-- +-- Problema: Policy "ai_queue_service_all" com USING(true) permite +-- que QUALQUER usuário autenticado leia/escreva/delete filas de IA, +-- criando risco de IDOR (Insecure Direct Object Reference). +-- +-- Solução: Substituir por políticas granulares de mínimo privilégio: +-- - SELECT: owner OR admin +-- - INSERT: owner (auth.uid() = requested_by) +-- - UPDATE: somente admins +-- - DELETE: somente admins +-- ============================================================ + +-- Remove a policy permissiva original +DROP POLICY IF EXISTS "ai_queue_service_all" ON ai_description_queue; + +-- SELECT: usuário vê apenas suas próprias filas, admins veem tudo +CREATE POLICY "ai_queue_read_own_or_admin" + ON ai_description_queue + FOR SELECT + USING ( + auth.uid() = requested_by + OR is_admin_or_above(auth.uid()) + ); + +-- INSERT: usuário pode inserir somente como ele mesmo +CREATE POLICY "ai_queue_insert_own" + ON ai_description_queue + FOR INSERT + WITH CHECK ( + auth.uid() = requested_by + ); + +-- UPDATE: somente admins podem atualizar status de fila +CREATE POLICY "ai_queue_update_admin_only" + ON ai_description_queue + FOR UPDATE + USING ( + is_admin_or_above(auth.uid()) + ) + WITH CHECK ( + is_admin_or_above(auth.uid()) + ); + +-- DELETE: somente admins podem remover entradas de fila +CREATE POLICY "ai_queue_delete_admin_only" + ON ai_description_queue + FOR DELETE + USING ( + is_admin_or_above(auth.uid()) + ); + +-- Garantir RLS habilitado (já deve estar, mas garantia) +ALTER TABLE ai_description_queue ENABLE ROW LEVEL SECURITY;