diff --git a/database-novo/manual/freemium_f3a_blacklist.supabase_admin.sql b/database-novo/manual/freemium_f3a_blacklist.supabase_admin.sql new file mode 100644 index 0000000..d43ba0e --- /dev/null +++ b/database-novo/manual/freemium_f3a_blacklist.supabase_admin.sql @@ -0,0 +1,104 @@ +-- ============================================================================= +-- 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;