Files
agenciapsilmno/database-novo/manual/freemium_f3a_blacklist.supabase_admin.sql
T
Leonardo c189906c58 freemium F3a: blacklist de e-mails e slugs
- tabela blacklist (kind email|slug, value normalizado, RLS saas_admin)
- is_email_blacklisted (exato + '@dominio.com') / is_slug_blacklisted
- trigger BEFORE INSERT em auth.users bloqueia cadastro DE VERDADE (EMAIL_BLOCKED)
- slug_disponivel passa a retornar motivo 'bloqueado'
- testado em ROLLBACK: email exato/dominio/signUp/slug

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:30:55 -03:00

105 lines
4.7 KiB
PL/PgSQL

-- =============================================================================
-- Freemium F3a — Blacklist de e-mails e slugs
--
-- ⚠️ APLICAR COMO supabase_admin (cria trigger em auth.users + altera
-- slug_disponivel, que é owned por supabase_admin).
--
-- Tabela blacklist (kind email|slug). E-mail bloqueia o cadastro DE VERDADE via
-- trigger BEFORE INSERT em auth.users (não só no front); suporta domínio inteiro
-- com entrada '@dominio.com'. Slug integra no slug_disponivel (motivo 'bloqueado').
-- Gerida por saas_admin (dev) em Configurações.
-- =============================================================================
BEGIN;
CREATE TABLE IF NOT EXISTS public.blacklist (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
kind text NOT NULL CHECK (kind IN ('email','slug')),
value text NOT NULL,
note text,
created_by uuid,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (kind, value)
);
-- normaliza value (lower+trim) sempre
CREATE OR REPLACE FUNCTION public.blacklist_normalize()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
NEW.value := lower(trim(NEW.value));
IF NEW.value = '' THEN RAISE EXCEPTION 'valor vazio'; END IF;
RETURN NEW;
END $$;
DROP TRIGGER IF EXISTS trg_blacklist_normalize ON public.blacklist;
CREATE TRIGGER trg_blacklist_normalize BEFORE INSERT OR UPDATE ON public.blacklist
FOR EACH ROW EXECUTE FUNCTION public.blacklist_normalize();
-- RLS: só saas_admin gere
ALTER TABLE public.blacklist ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS blacklist_saas_admin ON public.blacklist;
CREATE POLICY blacklist_saas_admin ON public.blacklist
FOR ALL USING (public.is_saas_admin()) WITH CHECK (public.is_saas_admin());
-- helpers ----------------------------------------------------------------------
CREATE OR REPLACE FUNCTION public.is_email_blacklisted(p_email text)
RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
SELECT EXISTS (
SELECT 1 FROM public.blacklist
WHERE kind = 'email'
AND value IN (
lower(trim(p_email)),
'@' || split_part(lower(trim(p_email)), '@', 2)
)
);
$$;
ALTER FUNCTION public.is_email_blacklisted(text) OWNER TO supabase_admin;
CREATE OR REPLACE FUNCTION public.is_slug_blacklisted(p_slug text)
RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
SELECT EXISTS (SELECT 1 FROM public.blacklist WHERE kind = 'slug' AND value = lower(trim(p_slug)));
$$;
ALTER FUNCTION public.is_slug_blacklisted(text) OWNER TO supabase_admin;
-- trigger de bloqueio real no cadastro -----------------------------------------
CREATE OR REPLACE FUNCTION public.enforce_email_blacklist()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
BEGIN
IF NEW.email IS NOT NULL AND public.is_email_blacklisted(NEW.email) THEN
RAISE EXCEPTION 'EMAIL_BLOCKED' USING ERRCODE = 'P0001';
END IF;
RETURN NEW;
END $$;
ALTER FUNCTION public.enforce_email_blacklist() OWNER TO supabase_admin;
DROP TRIGGER IF EXISTS trg_enforce_email_blacklist ON auth.users;
CREATE TRIGGER trg_enforce_email_blacklist BEFORE INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.enforce_email_blacklist();
-- integra no slug_disponivel (motivo 'bloqueado') ------------------------------
CREATE OR REPLACE FUNCTION public.slug_disponivel(p_slug text)
RETURNS jsonb
LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE
v text := lower(trim(coalesce(p_slug, '')));
v_reservados text[] := ARRAY['public','tenant','admin','www','api','app','auth','supabase','postgres','saas','suporte','support'];
BEGIN
IF length(v) < 3 THEN RETURN jsonb_build_object('ok', false, 'motivo', 'curto'); END IF;
IF length(v) > 48 THEN RETURN jsonb_build_object('ok', false, 'motivo', 'longo'); END IF;
IF v !~ '^[a-z][a-z0-9_]*$' THEN RETURN jsonb_build_object('ok', false, 'motivo', 'invalido'); END IF;
IF v = ANY(v_reservados) THEN RETURN jsonb_build_object('ok', false, 'motivo', 'reservado'); END IF;
IF public.is_slug_blacklisted(v) THEN RETURN jsonb_build_object('ok', false, 'motivo', 'bloqueado'); END IF;
IF EXISTS (SELECT 1 FROM public.tenants WHERE slug = v) THEN RETURN jsonb_build_object('ok', false, 'motivo', 'em_uso'); END IF;
RETURN jsonb_build_object('ok', true, 'motivo', 'disponivel');
END $$;
ALTER FUNCTION public.slug_disponivel(text) OWNER TO supabase_admin;
REVOKE ALL ON FUNCTION public.slug_disponivel(text) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.slug_disponivel(text) TO anon, authenticated, service_role;
GRANT SELECT, INSERT, UPDATE, DELETE ON public.blacklist TO authenticated;
COMMIT;