ObraAnalytics SETUP
Voltar ao Painel

⚙️ Configurar Supabase — Controle de Usuários

Siga os passos abaixo para criar a tabela profiles no Supabase. Depois disso, você verá todos os usuários cadastrados direto no Painel Admin com controle total de planos.

Tempo estimado: 5 minutos. Você só precisa fazer isso uma vez.
1
Abrir o SQL Editor no Supabase
Acesse seu projeto e vá ao editor SQL

Clique no botão abaixo para abrir o SQL Editor diretamente no seu projeto ObraAnalytics:

Abrir SQL Editor no Supabase
Navegue por: Supabase Dashboard → Seu Projeto → SQL Editor → New Query
2
Copiar e colar o SQL completo
Cole todo o conteúdo abaixo no SQL Editor e clique em "Run"
Copie todo o SQL abaixo de uma vez. Clique no botão "Copiar SQL" e cole no editor do Supabase.
-- ============================================================
--  ObraAnalytics — SETUP COMPLETO (execute uma única vez)
-- ============================================================

-- 1. TABELA PROFILES (cria se não existir)
CREATE TABLE IF NOT EXISTS public.profiles (
  id            UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  nome          TEXT,
  email         TEXT UNIQUE,
  telefone      TEXT DEFAULT '',
  plano         TEXT DEFAULT 'trial',
  trial_expires TIMESTAMPTZ DEFAULT (NOW() + INTERVAL '24 hours'),
  avatar_url    TEXT DEFAULT '',
  empresa       TEXT DEFAULT '',
  observacoes   TEXT DEFAULT '',
  criado_em     TIMESTAMPTZ DEFAULT NOW(),
  atualizado_em TIMESTAMPTZ DEFAULT NOW()
);

-- Adiciona colunas que podem estar faltando (seguro rodar várias vezes)
ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS telefone      TEXT DEFAULT '';
ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS empresa       TEXT DEFAULT '';
ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS observacoes   TEXT DEFAULT '';
ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS avatar_url    TEXT DEFAULT '';
ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS atualizado_em TIMESTAMPTZ DEFAULT NOW();
ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS criado_em     TIMESTAMPTZ DEFAULT NOW();

-- ✅ NOVA COLUNA: data de expiração do plano pago
-- NULL = plano ativo sem expiração manual
-- Preencha esta coluna quando o usuário cancelar ou não renovar
ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS plan_expires  TIMESTAMPTZ DEFAULT NULL;

-- Atualiza constraint do plano (remove a antiga se existir e recria)
ALTER TABLE public.profiles DROP CONSTRAINT IF EXISTS profiles_plano_check;
ALTER TABLE public.profiles ADD CONSTRAINT profiles_plano_check
  CHECK (plano IN ('trial','basico','pro','mensal','anual','enterprise','equipe','inativo'));

-- 2. RLS (segurança por linha)
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;

-- Remove policies antigas se existirem (evita erro de duplicata)
DROP POLICY IF EXISTS "Usuário lê próprio perfil"  ON public.profiles;
DROP POLICY IF EXISTS "Usuário edita próprio perfil" ON public.profiles;
DROP POLICY IF EXISTS "Service role acesso total"   ON public.profiles;

CREATE POLICY "Usuário lê próprio perfil"
  ON public.profiles FOR SELECT USING (auth.uid() = id);

CREATE POLICY "Usuário edita próprio perfil"
  ON public.profiles FOR UPDATE USING (auth.uid() = id);

CREATE POLICY "Service role acesso total"
  ON public.profiles FOR ALL USING (auth.role() = 'service_role');

-- 3. TRIGGER: cria perfil automaticamente no cadastro
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER
SET search_path = public AS $$
BEGIN
  INSERT INTO public.profiles (id, nome, email, telefone, plano, trial_expires)
  VALUES (
    NEW.id,
    COALESCE(NEW.raw_user_meta_data->>'nome', split_part(NEW.email, '@', 1)),
    NEW.email,
    COALESCE(NEW.raw_user_meta_data->>'telefone', ''),
    'trial',
    NOW() + INTERVAL '24 hours'
  ) ON CONFLICT (id) DO NOTHING;
  RETURN NEW;
END; $$;

DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();

-- 4. TRIGGER: atualiza timestamp
CREATE OR REPLACE FUNCTION public.update_updated_at()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN NEW.atualizado_em = NOW(); RETURN NEW; END; $$;

DROP TRIGGER IF EXISTS set_profiles_updated_at ON public.profiles;
CREATE TRIGGER set_profiles_updated_at
  BEFORE UPDATE ON public.profiles
  FOR EACH ROW EXECUTE FUNCTION public.update_updated_at();

-- 5. ÍNDICES
CREATE INDEX IF NOT EXISTS idx_profiles_email  ON public.profiles(email);
CREATE INDEX IF NOT EXISTS idx_profiles_plano  ON public.profiles(plano);
CREATE INDEX IF NOT EXISTS idx_profiles_criado ON public.profiles(criado_em DESC);

-- 6. FUNÇÃO ADMIN: listar usuários (bypassa RLS)
-- DROP obrigatório: não é possível alterar tipo de retorno com CREATE OR REPLACE
-- O Postgres registra INT como "integer" internamente
DROP FUNCTION IF EXISTS public.admin_listar_usuarios(integer,integer,text,text);
DROP FUNCTION IF EXISTS public.admin_listar_usuarios(int4,int4,text,text);
DROP FUNCTION IF EXISTS public.admin_listar_usuarios(int,int,text,text);
CREATE OR REPLACE FUNCTION public.admin_listar_usuarios(
  p_page    INT DEFAULT 1,
  p_limit   INT DEFAULT 50,
  p_search  TEXT DEFAULT '',
  p_plano   TEXT DEFAULT ''
)
RETURNS TABLE (
  id UUID, nome TEXT, email TEXT, telefone TEXT, plano TEXT,
  trial_expires TIMESTAMPTZ, plan_expires TIMESTAMPTZ, empresa TEXT,
  observacoes TEXT, criado_em TIMESTAMPTZ, atualizado_em TIMESTAMPTZ, total_count BIGINT
)
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
BEGIN
  RETURN QUERY
  SELECT p.id, p.nome, p.email, p.telefone, p.plano,
         p.trial_expires, p.plan_expires, p.empresa,
         p.observacoes, p.criado_em, p.atualizado_em,
         COUNT(*) OVER() AS total_count
  FROM public.profiles p
  WHERE
    (p_search = '' OR p.email ILIKE '%'||p_search||'%'
     OR p.nome ILIKE '%'||p_search||'%' OR p.empresa ILIKE '%'||p_search||'%')
    AND (p_plano = '' OR p.plano = p_plano)
  ORDER BY p.criado_em DESC
  LIMIT p_limit OFFSET (p_page - 1) * p_limit;
END; $$;

GRANT EXECUTE ON FUNCTION public.admin_listar_usuarios TO anon, authenticated;

-- 7. FUNÇÃO ADMIN: atualizar plano (com suporte a plan_expires)
DROP FUNCTION IF EXISTS public.admin_atualizar_plano(uuid,text,text,timestamptz);
DROP FUNCTION IF EXISTS public.admin_atualizar_plano(uuid,text,text);
CREATE OR REPLACE FUNCTION public.admin_atualizar_plano(
  p_user_id    UUID,
  p_plano      TEXT,
  p_obs        TEXT DEFAULT '',
  p_plan_expires TIMESTAMPTZ DEFAULT NULL
)
RETURNS BOOLEAN LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
BEGIN
  UPDATE public.profiles
  SET plano       = p_plano,
      plan_expires = p_plan_expires,
      observacoes  = CASE WHEN p_obs = '' THEN observacoes ELSE p_obs END,
      atualizado_em = NOW()
  WHERE id = p_user_id;
  RETURN FOUND;
END; $$;

GRANT EXECUTE ON FUNCTION public.admin_atualizar_plano TO anon, authenticated;

-- 7b. FUNÇÃO: bloquear usuário (define plano=inativo + plan_expires=NOW)
DROP FUNCTION IF EXISTS public.admin_bloquear_usuario(uuid,text);
DROP FUNCTION IF EXISTS public.admin_bloquear_usuario(uuid);
CREATE OR REPLACE FUNCTION public.admin_bloquear_usuario(p_user_id UUID, p_motivo TEXT DEFAULT '')
RETURNS BOOLEAN LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
BEGIN
  UPDATE public.profiles
  SET plano        = 'inativo',
      plan_expires = NOW(),
      observacoes  = CASE WHEN p_motivo = '' THEN observacoes
                     ELSE 'BLOQUEADO: ' || p_motivo || ' | ' || observacoes END,
      atualizado_em = NOW()
  WHERE id = p_user_id;
  RETURN FOUND;
END; $$;

GRANT EXECUTE ON FUNCTION public.admin_bloquear_usuario TO anon, authenticated;

-- 8. FUNÇÃO ADMIN: estatísticas gerais
DROP FUNCTION IF EXISTS public.admin_stats();
CREATE OR REPLACE FUNCTION public.admin_stats()
RETURNS JSON LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
DECLARE result JSON;
BEGIN
  SELECT json_build_object(
    'total_usuarios', (SELECT COUNT(*) FROM public.profiles),
    'trial',          (SELECT COUNT(*) FROM public.profiles WHERE plano='trial'),
    'mensal',         (SELECT COUNT(*) FROM public.profiles WHERE plano='mensal'),
    'anual',          (SELECT COUNT(*) FROM public.profiles WHERE plano='anual'),
    'enterprise',     (SELECT COUNT(*) FROM public.profiles WHERE plano='enterprise'),
    'inativo',        (SELECT COUNT(*) FROM public.profiles WHERE plano='inativo'),
    'trial_expirado', (SELECT COUNT(*) FROM public.profiles WHERE plano='trial' AND trial_expires < NOW()),
    'novos_7dias',    (SELECT COUNT(*) FROM public.profiles WHERE criado_em >= NOW()-INTERVAL '7 days'),
    'novos_30dias',   (SELECT COUNT(*) FROM public.profiles WHERE criado_em >= NOW()-INTERVAL '30 days')
  ) INTO result;
  RETURN result;
END; $$;

GRANT EXECUTE ON FUNCTION public.admin_stats TO anon, authenticated;

-- 9. PERMISSÕES
GRANT SELECT, INSERT, UPDATE ON public.profiles TO authenticated;
GRANT SELECT, INSERT, UPDATE, DELETE ON public.profiles TO service_role;

-- 10. MIGRAR USUÁRIOS EXISTENTES (cria perfil para quem já se cadastrou)
INSERT INTO public.profiles (id, nome, email, plano, trial_expires, criado_em)
SELECT u.id,
  COALESCE(u.raw_user_meta_data->>'nome', split_part(u.email,'@',1)),
  u.email, 'trial',
  COALESCE(u.created_at + INTERVAL '24 hours', NOW() + INTERVAL '24 hours'),
  COALESCE(u.created_at, NOW())
FROM auth.users u
WHERE NOT EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = u.id);

-- VERIFICAÇÃO FINAL
SELECT 'profiles criados: ' || COUNT(*)::TEXT AS resultado FROM public.profiles;
3
Executar o SQL
Clique no botão "Run" (▶) ou pressione Ctrl+Enter no SQL Editor

Após colar o SQL, clique em Run ▶ no canto superior direito do SQL Editor.

O que você deve ver ao final:

resultado
─────────────────────────────
profiles criados: 3
O número indica quantos perfis foram criados ou já existiam. Se você tiver usuários cadastrados, eles aparecerão automaticamente no Painel Admin.
4
Verificar no Painel Admin
Acesse o Painel Admin → aba Usuários para confirmar

Volte ao Painel Admin e clique na aba 👥 Usuários. Você deverá ver:

  • Lista de todos os usuários cadastrados com nome, email e plano
  • Stats por plano: Trial, Mensal, Anual, Enterprise, Inativo
  • Botão "Editar" para alterar plano, nome, empresa e observações
  • Botão "Bloquear Acesso" para desativar um usuário
  • Contador de novos usuários nos últimos 7 e 30 dias
Ir para o Painel Admin
5
Como controlar planos e assinaturas
Fluxo completo para gerenciar assinantes
📅 Usuário assinou (Hotmart)
  1. Receba notificação de venda no Hotmart
  2. Pegue o email do comprador
  3. No Painel Admin → Usuários → busque o email
  4. Clique "Editar" → mude o plano para mensal ou anual
  5. Salve — o usuário já terá acesso!
❌ Usuário cancelou
  1. Receba notificação de cancelamento
  2. No Painel Admin → Usuários → busque o email
  3. Clique "Editar" → mude para inativo
  4. Ou clique "Bloquear Acesso" diretamente
  5. O usuário perderá o acesso imediatamente
🆓 Novo usuário (trial)
  1. Usuário se cadastra → trial automático de 24h
  2. Se quiser estender o trial, edite o plano para trial
  3. A coluna "Trial" mostra se ainda está ativo
  4. Após 24h sem assinar, o acesso é limitado automaticamente
🔑 Resetar senha
  1. No Painel Admin → Usuários → clique "Editar"
  2. No modal há um link direto para o Supabase Auth
  3. No Supabase: Auth → Users → encontre o email
  4. Clique nos 3 pontos → "Send password reset email"
  5. O usuário recebe o link de redefinição
Dica PRO: Configure webhooks no Hotmart para atualizar os planos automaticamente assim que o usuário pagar ou cancelar. Isso elimina a necessidade de atualização manual.
🎉

Configuração Concluída!

Agora você tem controle total sobre os usuários do ObraAnalytics. Gerencie planos, bloqueie acessos e acompanhe assinaturas direto no Painel Admin.

Abrir Painel Admin SQL Editor Supabase