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