Files
agenciapsilmno/migrations/support_sessions.sql
2026-03-12 08:58:36 -03:00

236 lines
8.5 KiB
PL/PgSQL

-- ═══════════════════════════════════════════════════════════════════════════
-- Support Sessions — Sessões de suporte técnico SaaS
-- ═══════════════════════════════════════════════════════════════════════════
-- Permite que admins SaaS gerem tokens de acesso temporário para debug
-- de agendas de terapeutas, sem expor debug para usuários comuns.
--
-- SEGURANÇA:
-- - RLS: só saas_admin pode criar/listar sessões
-- - Token é opaco (gen_random_uuid) — não adivinhável
-- - expires_at com TTL máximo de 60 minutos
-- - validate_support_session() retorna apenas true/false + tenant_id
-- (não expõe dados do admin)
-- ═══════════════════════════════════════════════════════════════════════════
-- ── Tabela ──────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS "public"."support_sessions" (
"id" uuid DEFAULT gen_random_uuid() NOT NULL,
"tenant_id" uuid NOT NULL,
"admin_id" uuid NOT NULL,
"token" text NOT NULL DEFAULT (replace(gen_random_uuid()::text, '-', '') || replace(gen_random_uuid()::text, '-', '')),
"expires_at" timestamp with time zone NOT NULL
DEFAULT (now() + interval '60 minutes'),
"created_at" timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT "support_sessions_pkey" PRIMARY KEY ("id"),
CONSTRAINT "support_sessions_tenant_fk"
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE CASCADE,
CONSTRAINT "support_sessions_admin_fk"
FOREIGN KEY ("admin_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE,
CONSTRAINT "support_sessions_token_unique" UNIQUE ("token")
);
-- ── Índices ──────────────────────────────────────────────────────────────────
CREATE INDEX IF NOT EXISTS "support_sessions_token_idx"
ON "public"."support_sessions" ("token");
CREATE INDEX IF NOT EXISTS "support_sessions_tenant_idx"
ON "public"."support_sessions" ("tenant_id");
CREATE INDEX IF NOT EXISTS "support_sessions_expires_idx"
ON "public"."support_sessions" ("expires_at");
-- ── RLS ──────────────────────────────────────────────────────────────────────
ALTER TABLE "public"."support_sessions" ENABLE ROW LEVEL SECURITY;
-- Somente saas_admin pode ver suas próprias sessões de suporte
DROP POLICY IF EXISTS "support_sessions_saas_select" ON "public"."support_sessions";
CREATE POLICY "support_sessions_saas_select"
ON "public"."support_sessions"
FOR SELECT
USING (
auth.uid() = admin_id
AND EXISTS (
SELECT 1 FROM public.profiles
WHERE id = auth.uid()
AND role = 'saas_admin'
)
);
-- Somente saas_admin pode criar sessões de suporte
DROP POLICY IF EXISTS "support_sessions_saas_insert" ON "public"."support_sessions";
CREATE POLICY "support_sessions_saas_insert"
ON "public"."support_sessions"
FOR INSERT
WITH CHECK (
auth.uid() = admin_id
AND EXISTS (
SELECT 1 FROM public.profiles
WHERE id = auth.uid()
AND role = 'saas_admin'
)
);
-- Somente saas_admin pode deletar suas próprias sessões
DROP POLICY IF EXISTS "support_sessions_saas_delete" ON "public"."support_sessions";
CREATE POLICY "support_sessions_saas_delete"
ON "public"."support_sessions"
FOR DELETE
USING (
auth.uid() = admin_id
AND EXISTS (
SELECT 1 FROM public.profiles
WHERE id = auth.uid()
AND role = 'saas_admin'
)
);
-- ── RPC: create_support_session ───────────────────────────────────────────────
-- Cria uma sessão de suporte para um tenant.
-- Apenas saas_admin pode chamar. TTL: 60 minutos (configurável via p_ttl_minutes).
-- Retorna: token, expires_at
DROP FUNCTION IF EXISTS public.create_support_session(uuid, integer);
CREATE OR REPLACE FUNCTION public.create_support_session(
p_tenant_id uuid,
p_ttl_minutes integer DEFAULT 60
)
RETURNS json
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_admin_id uuid;
v_role text;
v_token text;
v_expires timestamp with time zone;
v_session support_sessions;
BEGIN
-- Verifica autenticação
v_admin_id := auth.uid();
IF v_admin_id IS NULL THEN
RAISE EXCEPTION 'Não autenticado.' USING ERRCODE = 'P0001';
END IF;
-- Verifica role saas_admin
SELECT role INTO v_role
FROM public.profiles
WHERE id = v_admin_id;
IF v_role <> 'saas_admin' THEN
RAISE EXCEPTION 'Acesso negado. Somente saas_admin pode criar sessões de suporte.'
USING ERRCODE = 'P0002';
END IF;
-- Valida TTL (1 a 120 minutos)
IF p_ttl_minutes < 1 OR p_ttl_minutes > 120 THEN
RAISE EXCEPTION 'TTL inválido. Use entre 1 e 120 minutos.'
USING ERRCODE = 'P0003';
END IF;
-- Valida tenant
IF NOT EXISTS (SELECT 1 FROM public.tenants WHERE id = p_tenant_id) THEN
RAISE EXCEPTION 'Tenant não encontrado.'
USING ERRCODE = 'P0004';
END IF;
-- Gera token único (64 chars hex, sem pgcrypto)
v_token := replace(gen_random_uuid()::text, '-', '') || replace(gen_random_uuid()::text, '-', '');
v_expires := now() + (p_ttl_minutes || ' minutes')::interval;
-- Insere sessão
INSERT INTO public.support_sessions (tenant_id, admin_id, token, expires_at)
VALUES (p_tenant_id, v_admin_id, v_token, v_expires)
RETURNING * INTO v_session;
RETURN json_build_object(
'token', v_session.token,
'expires_at', v_session.expires_at,
'session_id', v_session.id
);
END;
$$;
-- ── RPC: validate_support_session ────────────────────────────────────────────
-- Valida um token de suporte. Não requer autenticação (chamada pública).
-- Retorna: { valid: bool, tenant_id: uuid|null }
-- NUNCA retorna admin_id ou dados internos.
DROP FUNCTION IF EXISTS public.validate_support_session(text);
CREATE OR REPLACE FUNCTION public.validate_support_session(
p_token text
)
RETURNS json
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_session support_sessions;
BEGIN
IF p_token IS NULL OR length(trim(p_token)) < 32 THEN
RETURN json_build_object('valid', false, 'tenant_id', null);
END IF;
SELECT * INTO v_session
FROM public.support_sessions
WHERE token = p_token
AND expires_at > now()
LIMIT 1;
IF NOT FOUND THEN
RETURN json_build_object('valid', false, 'tenant_id', null);
END IF;
RETURN json_build_object(
'valid', true,
'tenant_id', v_session.tenant_id
);
END;
$$;
-- ── RPC: revoke_support_session ───────────────────────────────────────────────
-- Revoga um token manualmente. Apenas o admin que criou pode revogar.
DROP FUNCTION IF EXISTS public.revoke_support_session(text);
CREATE OR REPLACE FUNCTION public.revoke_support_session(
p_token text
)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_admin_id uuid;
v_role text;
BEGIN
v_admin_id := auth.uid();
IF v_admin_id IS NULL THEN
RAISE EXCEPTION 'Não autenticado.' USING ERRCODE = 'P0001';
END IF;
SELECT role INTO v_role FROM public.profiles WHERE id = v_admin_id;
IF v_role <> 'saas_admin' THEN
RAISE EXCEPTION 'Acesso negado.' USING ERRCODE = 'P0002';
END IF;
DELETE FROM public.support_sessions
WHERE token = p_token
AND admin_id = v_admin_id;
RETURN FOUND;
END;
$$;
-- ── Cleanup automático (opcional) ────────────────────────────────────────────
-- Sessões expiradas podem ser limpas periodicamente via pg_cron ou edge function.
-- DELETE FROM public.support_sessions WHERE expires_at < now();