-- ============================================================================= -- Migration: 20260420000001_patient_intake_invite_info_rpc -- A#31 — RPC read-only de lookup público do terapeuta/clínica a partir do -- token do patient_invite. Consumida pela edge function get-intake-invite-info -- para alimentar o "hero header" da página /cadastro/paciente. -- -- Segurança: -- • SECURITY DEFINER (ignora RLS de profiles/company_profiles) -- • Valida token: existe, ativo, não-expirado, dentro do max_uses -- • Retorna APENAS campos explicitamente seguros (não-sensíveis) -- • Execute revogado de PUBLIC/anon; grantado só para service_role (edge) -- e authenticated (usos internos futuros) -- -- Payload devolvido: -- { ok: true, info: { therapist: {...}, clinic: {...}|null } } -- { error: 'invalid-token' } — token inválido/expirado/esgotado -- { error: 'missing-token' } — input vazio -- ============================================================================= CREATE OR REPLACE FUNCTION public.get_patient_intake_invite_info(p_token text) RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp AS $$ DECLARE v_token_clean text; v_invite RECORD; v_result jsonb; BEGIN v_token_clean := nullif(trim(coalesce(p_token, '')), ''); IF v_token_clean IS NULL THEN RETURN jsonb_build_object('error', 'missing-token'); END IF; SELECT pi.owner_id, pi.tenant_id, pi.active, pi.expires_at, pi.max_uses, pi.uses INTO v_invite FROM public.patient_invites pi WHERE pi.token = v_token_clean LIMIT 1; IF v_invite.owner_id IS NULL THEN RETURN jsonb_build_object('error', 'invalid-token'); END IF; IF v_invite.active IS DISTINCT FROM true THEN RETURN jsonb_build_object('error', 'invalid-token'); END IF; IF v_invite.expires_at IS NOT NULL AND v_invite.expires_at < now() THEN RETURN jsonb_build_object('error', 'invalid-token'); END IF; IF v_invite.max_uses IS NOT NULL AND v_invite.uses >= v_invite.max_uses THEN RETURN jsonb_build_object('error', 'invalid-token'); END IF; SELECT jsonb_build_object( 'therapist', jsonb_build_object( 'display_name', coalesce( nullif(trim(p.full_name), ''), nullif(trim(p.nickname), ''), 'Profissional' ), 'avatar_url', nullif(trim(coalesce(p.avatar_url, '')), ''), 'work_description', nullif(trim(coalesce(p.work_description, '')), ''), 'bio', nullif(trim(coalesce(p.bio, '')), ''), 'phone', nullif(trim(coalesce(p.phone, '')), ''), 'site_url', nullif(trim(coalesce(p.site_url, '')), ''), 'instagram', nullif(trim(coalesce(p.social_instagram, '')), '') ), 'clinic', CASE WHEN cp.tenant_id IS NOT NULL THEN jsonb_build_object( 'name', nullif(trim(coalesce(cp.nome_fantasia, '')), ''), 'logo_url', nullif(trim(coalesce(cp.logo_url, '')), ''), 'email', nullif(trim(coalesce(cp.email, '')), ''), 'phone', nullif(trim(coalesce(cp.telefone, '')), ''), 'site', nullif(trim(coalesce(cp.site, '')), ''), 'city', nullif(trim(coalesce(cp.cidade, '')), ''), 'state', nullif(trim(coalesce(cp.estado, '')), ''), 'neighborhood', nullif(trim(coalesce(cp.bairro, '')), ''), 'street', nullif(trim(coalesce(cp.logradouro, '')), ''), 'number', nullif(trim(coalesce(cp.numero, '')), ''), 'social', coalesce(cp.redes_sociais, '[]'::jsonb) ) ELSE NULL END ) INTO v_result FROM public.profiles p LEFT JOIN public.company_profiles cp ON cp.tenant_id = v_invite.tenant_id WHERE p.id = v_invite.owner_id LIMIT 1; IF v_result IS NULL THEN RETURN jsonb_build_object('error', 'invalid-token'); END IF; RETURN jsonb_build_object('ok', true, 'info', v_result); END; $$; REVOKE EXECUTE ON FUNCTION public.get_patient_intake_invite_info(text) FROM PUBLIC, anon; GRANT EXECUTE ON FUNCTION public.get_patient_intake_invite_info(text) TO authenticated, service_role; COMMENT ON FUNCTION public.get_patient_intake_invite_info(text) IS 'A#31 — Lookup público read-only (via edge function) dos dados de apresentação do terapeuta/clínica dono do link de cadastro externo. Só retorna campos não-sensíveis.';