-- ═══════════════════════════════════════════════════════════════════════════ -- 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();