-- ========================================================= -- Agência PSI Quasar — Cadastro Externo de Paciente (Supabase/Postgres) -- Objetivo: -- - Ter um link público com TOKEN que o terapeuta envia ao paciente -- - Paciente preenche um formulário público -- - Salva em "intake requests" (pré-cadastro) -- - Terapeuta revisa e converte em paciente dentro do sistema -- -- Tabelas: -- - patient_invites -- - patient_intake_requests -- -- Funções: -- - create_patient_intake_request (RPC pública - anon) -- -- Segurança: -- - RLS habilitada -- - Público (anon) não lê nada, só executa RPC -- - Terapeuta (authenticated) lê/atualiza somente seus registros -- ========================================================= -- 0) Tabelas create table if not exists public.patient_invites ( id uuid primary key default gen_random_uuid(), owner_id uuid not null, token text not null unique, active boolean not null default true, expires_at timestamptz null, max_uses int null, uses int not null default 0, created_at timestamptz not null default now() ); create index if not exists patient_invites_owner_id_idx on public.patient_invites(owner_id); create index if not exists patient_invites_token_idx on public.patient_invites(token); create table if not exists public.patient_intake_requests ( id uuid primary key default gen_random_uuid(), owner_id uuid not null, token text not null, name text not null, email text null, phone text null, notes text null, consent boolean not null default false, status text not null default 'new', -- new | converted | rejected created_at timestamptz not null default now() ); create index if not exists patient_intake_owner_id_idx on public.patient_intake_requests(owner_id); create index if not exists patient_intake_token_idx on public.patient_intake_requests(token); create index if not exists patient_intake_status_idx on public.patient_intake_requests(status); -- 1) RLS alter table public.patient_invites enable row level security; alter table public.patient_intake_requests enable row level security; -- 2) Fechar acesso direto para anon (público) revoke all on table public.patient_invites from anon; revoke all on table public.patient_intake_requests from anon; -- 3) Policies: terapeuta (authenticated) - somente próprios registros -- patient_invites drop policy if exists invites_select_own on public.patient_invites; create policy invites_select_own on public.patient_invites for select to authenticated using (owner_id = auth.uid()); drop policy if exists invites_insert_own on public.patient_invites; create policy invites_insert_own on public.patient_invites for insert to authenticated with check (owner_id = auth.uid()); drop policy if exists invites_update_own on public.patient_invites; create policy invites_update_own on public.patient_invites for update to authenticated using (owner_id = auth.uid()) with check (owner_id = auth.uid()); -- patient_intake_requests drop policy if exists intake_select_own on public.patient_intake_requests; create policy intake_select_own on public.patient_intake_requests for select to authenticated using (owner_id = auth.uid()); drop policy if exists intake_update_own on public.patient_intake_requests; create policy intake_update_own on public.patient_intake_requests for update to authenticated using (owner_id = auth.uid()) with check (owner_id = auth.uid()); -- 4) RPC pública para criar intake (página pública) -- Importantíssimo: security definer + search_path fixo create or replace function public.create_patient_intake_request( p_token text, p_name text, p_email text default null, p_phone text default null, p_notes text default null, p_consent boolean default false ) returns uuid language plpgsql security definer set search_path = public as $$ declare v_owner uuid; v_active boolean; v_expires timestamptz; v_max_uses int; v_uses int; v_id uuid; begin select owner_id, active, expires_at, max_uses, uses into v_owner, v_active, v_expires, v_max_uses, v_uses from public.patient_invites where token = p_token limit 1; if v_owner is null then raise exception 'Token inválido'; end if; if v_active is not true then raise exception 'Link desativado'; end if; if v_expires is not null and now() > v_expires then raise exception 'Link expirado'; end if; if v_max_uses is not null and v_uses >= v_max_uses then raise exception 'Limite de uso atingido'; end if; if p_name is null or length(trim(p_name)) = 0 then raise exception 'Nome é obrigatório'; end if; insert into public.patient_intake_requests (owner_id, token, name, email, phone, notes, consent, status) values (v_owner, p_token, trim(p_name), nullif(lower(trim(p_email)), ''), nullif(trim(p_phone), ''), nullif(trim(p_notes), ''), coalesce(p_consent, false), 'new') returning id into v_id; update public.patient_invites set uses = uses + 1 where token = p_token; return v_id; end; $$; grant execute on function public.create_patient_intake_request(text, text, text, text, text, boolean) to anon; grant execute on function public.create_patient_intake_request(text, text, text, text, text, boolean) to authenticated; -- 5) (Opcional) helper para rotacionar token no painel (somente authenticated) -- Você pode usar no front via supabase.rpc('rotate_patient_invite_token') create or replace function public.rotate_patient_invite_token( p_new_token text ) returns uuid language plpgsql security definer set search_path = public as $$ declare v_uid uuid; v_id uuid; begin -- pega o usuário logado v_uid := auth.uid(); if v_uid is null then raise exception 'Usuário não autenticado'; end if; -- desativa tokens antigos ativos do usuário update public.patient_invites set active = false where owner_id = v_uid and active = true; -- cria novo token insert into public.patient_invites (owner_id, token, active) values (v_uid, p_new_token, true) returning id into v_id; return v_id; end; $$; grant execute on function public.rotate_patient_invite_token(text) to authenticated; grant select, insert, update, delete on table public.patient_invites to authenticated; grant select, insert, update, delete on table public.patient_intake_requests to authenticated; -- anon não precisa acessar tabelas diretamente revoke all on table public.patient_invites from anon; revoke all on table public.patient_intake_requests from anon;