236 lines
8.5 KiB
PL/PgSQL
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();
|