213 lines
6.4 KiB
PL/PgSQL
213 lines
6.4 KiB
PL/PgSQL
-- =========================================================
|
|
-- 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;
|
|
|