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>
This commit is contained in:
@@ -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;
|
||||||
Reference in New Issue
Block a user