-- ============================================================================= -- 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;