F1b: 6 tabelas anon-facing ficam em public (decisao roteamento anon)

Fluxos anon identificam tenant por token/slug e nao resolvem o schema fisico.
Decisao (opcao C): manter em public com RLS por token. Volta a global:
patient_intake_requests, patient_invites, patient_invite_attempts,
document_share_links, agendador_configuracoes, agendador_solicitacoes.

- migration 20260613000001_f1b: remove as 6 do _tenant_template (template v2,
  78 tabelas). Smoke: clone gera 78, zero tabelas anon no schema, drop limpo
- frontend: 38 cadeias em 14 arquivos revertidas tenantDb().from() ->
  supabase.from() com tenant_id/owner_id restaurado (via comparacao com main)
- edge: convert-abandoned-intakes restaurada do main (SELECT global)
- save-intake-progress: ja usava public, sem mudanca
- doc F0 atualizado: 78 tenant + 59 global

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-06-13 09:09:46 -03:00
parent 9b21642e15
commit f17e9ee786
17 changed files with 164 additions and 98 deletions
@@ -595,7 +595,7 @@ export function useAgendaEventLifecycle({
const d = new Date(composer.form.value.dia);
const isoDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
const { data } = await tenantDb().from('agendador_solicitacoes')
const { data } = await supabase.from('agendador_solicitacoes')
.select('id, paciente_nome, paciente_sobrenome, paciente_email')
.eq('owner_id', props.ownerId)
.eq('status', 'pendente')
@@ -981,7 +981,7 @@ async function loadAgendadorSlug() {
const uid = ownerId.value;
if (!uid) return;
try {
const { data } = await tenantDb().from('agendador_configuracoes').select('link_slug').eq('owner_id', uid).eq('ativo', true).maybeSingle();
const { data } = await supabase.from('agendador_configuracoes').select('link_slug').eq('owner_id', uid).eq('ativo', true).maybeSingle();
agendadorSlug.value = data?.link_slug || '';
} catch {
agendadorSlug.value = '';
@@ -993,7 +993,7 @@ async function loadCadastroToken() {
const { data: authData } = await supabase.auth.getUser();
const uid = authData?.user?.id;
if (!uid) return;
const { data } = await tenantDb().from('patient_invites').select('token').eq('owner_id', uid).eq('active', true).order('created_at', { ascending: false }).limit(1);
const { data } = await supabase.from('patient_invites').select('token').eq('owner_id', uid).eq('active', true).order('created_at', { ascending: false }).limit(1);
cadastroToken.value = data?.[0]?.token || '';
} catch {
cadastroToken.value = '';
@@ -64,12 +64,14 @@ async function load() {
if (!ownerId.value) return;
loading.value = true;
try {
let q = tenantDb().from('agendador_solicitacoes')
.select('id, owner_id, paciente_nome, paciente_sobrenome, paciente_email, paciente_celular, paciente_cpf, tipo, modalidade, data_solicitada, hora_solicitada, reservado_ate, motivo, como_conheceu, status, created_at')
let q = supabase
.from('agendador_solicitacoes')
.select('id, owner_id, tenant_id, paciente_nome, paciente_sobrenome, paciente_email, paciente_celular, paciente_cpf, tipo, modalidade, data_solicitada, hora_solicitada, reservado_ate, motivo, como_conheceu, status, created_at')
.order('data_solicitada', { ascending: false })
.order('hora_solicitada', { ascending: true });
if (!isClinic.value) q = q.eq('owner_id', ownerId.value);
if (isClinic.value) q = q.eq('tenant_id', tenantId.value);
else q = q.eq('owner_id', ownerId.value);
if (filtroStatus.value) q = q.eq('status', filtroStatus.value);
@@ -78,8 +80,9 @@ async function load() {
solicitacoes.value = data || [];
if (filtroStatus.value !== 'pendente') {
let qp = tenantDb().from('agendador_solicitacoes').select('id', { count: 'exact', head: true }).eq('status', 'pendente');
if (!isClinic.value) qp = qp.eq('owner_id', ownerId.value);
let qp = supabase.from('agendador_solicitacoes').select('id', { count: 'exact', head: true }).eq('status', 'pendente');
if (isClinic.value) qp = qp.eq('tenant_id', tenantId.value);
else qp = qp.eq('owner_id', ownerId.value);
const { count } = await qp;
totalPendentes.value = count || 0;
} else {
@@ -88,8 +91,9 @@ async function load() {
// Conta autorizados (sempre, independente do filtro ativo)
if (filtroStatus.value !== 'autorizado') {
let qa = tenantDb().from('agendador_solicitacoes').select('id', { count: 'exact', head: true }).eq('status', 'autorizado');
if (!isClinic.value) qa = qa.eq('owner_id', ownerId.value);
let qa = supabase.from('agendador_solicitacoes').select('id', { count: 'exact', head: true }).eq('status', 'autorizado');
if (isClinic.value) qa = qa.eq('tenant_id', tenantId.value);
else qa = qa.eq('owner_id', ownerId.value);
const { count: ca } = await qa;
totalAutorizados.value = ca || 0;
} else {
@@ -155,7 +159,7 @@ const aprovando = ref(null);
async function aprovar(s) {
aprovando.value = s.id;
try {
const { error } = await tenantDb().from('agendador_solicitacoes').update({ status: 'autorizado', autorizado_em: new Date().toISOString() }).eq('id', s.id);
const { error } = await supabase.from('agendador_solicitacoes').update({ status: 'autorizado', autorizado_em: new Date().toISOString() }).eq('id', s.id);
if (error) throw error;
toast.add({ severity: 'success', summary: 'Autorizado', detail: `Solicitação de ${nomeCompleto(s)} autorizada.`, life: 3000 });
await load();
@@ -184,7 +188,8 @@ async function confirmarRecusa() {
if (!s) return;
recusandoId.value = s.id;
try {
const { error } = await tenantDb().from('agendador_solicitacoes')
const { error } = await supabase
.from('agendador_solicitacoes')
.update({ status: 'recusado', recusado_motivo: recusaMotivo.value || null })
.eq('id', s.id);
if (error) throw error;
@@ -293,7 +298,7 @@ async function onEventSaved(arg) {
if (normalized[k] !== undefined) dbPayload[k] = normalized[k];
}
await createEvento(dbPayload);
const { error } = await tenantDb().from('agendador_solicitacoes').update({ status: 'convertido' }).eq('id', target.id);
const { error } = await supabase.from('agendador_solicitacoes').update({ status: 'convertido' }).eq('id', target.id);
if (error) throw error;
toast.add({ severity: 'success', summary: 'Convertido!', detail: `Sessão criada para ${nomeCompleto(target)}.`, life: 4000 });
await load();
@@ -16,7 +16,6 @@
-->
<script setup>
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
// Fase 2 (Graphify hotspot): convertToPatient duplicado em 2 pages — INSERT/UPDATE
// extraídos pro repository pra remover duplicação.
@@ -276,7 +275,7 @@ const intakeSections = computed(() => {
async function fetchIntakes() {
loading.value = true;
try {
const { data, error } = await tenantDb().from('patient_intake_requests').select('*').order('created_at', { ascending: false });
const { data, error } = await supabase.from('patient_intake_requests').select('*').order('created_at', { ascending: false });
if (error) throw error;
const weight = (s) => (s === 'new' ? 0 : s === 'converted' ? 1 : s === 'rejected' ? 2 : 9);
rows.value = (data || []).slice().sort((a, b) => {
@@ -323,7 +322,7 @@ async function markRejected() {
dlg.value.saving = true;
try {
const reason = String(dlg.value.reject_note || '').trim() || null;
const { error } = await tenantDb().from('patient_intake_requests').update({ status: 'rejected', rejected_reason: reason, updated_at: new Date().toISOString() }).eq('id', item.id);
const { error } = await supabase.from('patient_intake_requests').update({ status: 'rejected', rejected_reason: reason, updated_at: new Date().toISOString() }).eq('id', item.id);
if (error) throw error;
toast.add({ severity: 'success', summary: 'Rejeitado', detail: 'Solicitação rejeitada.', life: 2500 });
await fetchIntakes();
@@ -483,7 +483,8 @@ export async function markIntakeConverted(intakeId, patientId, { tenantId } = {}
// tenant_id no patient_intake_requests pode ser nullable (intake público sem tenant)
// — só filtramos se passado explícito.
let q = tenantDb().from('patient_intake_requests')
let q = supabase
.from('patient_intake_requests')
.update({
status: 'converted',
converted_patient_id: patientId,
@@ -491,6 +492,11 @@ export async function markIntakeConverted(intakeId, patientId, { tenantId } = {}
})
.eq('id', intakeId);
if (tenantId) {
const tid = resolveTenantId(tenantId);
q = q.eq('tenant_id', tid);
}
const { error } = await q;
if (error) throw error;
}