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
+1 -2
View File
@@ -32,7 +32,6 @@ import Popover from 'primevue/popover';
import { useToast } from 'primevue/usetoast';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
const emit = defineEmits(['quick-create', 'go-complete', 'show', 'hide']);
const toast = useToast();
@@ -53,7 +52,7 @@ async function loadToken() {
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);
if (data?.[0]?.token) {
inviteToken.value = data[0].token;
tokenLoaded = true;
+4 -3
View File
@@ -59,14 +59,15 @@ async function _refresh() {
// 2. Cadastros recebidos (status = 'new') — RLS filtra pelo owner
{
const { count } = await tenantDb().from('patient_intake_requests').select('id', { count: 'exact', head: true }).eq('status', 'new');
const { count } = await supabase.from('patient_intake_requests').select('id', { count: 'exact', head: true }).eq('status', 'new');
cadastrosRecebidos.value = count || 0;
}
// 3. Agendamentos recebidos (status = 'pendente')
{
let q = tenantDb().from('agendador_solicitacoes').select('id', { count: 'exact', head: true }).eq('status', 'pendente');
if (!(isClinic && tenantId)) q = q.eq('owner_id', ownerId);
let q = supabase.from('agendador_solicitacoes').select('id', { count: 'exact', head: true }).eq('status', 'pendente');
if (isClinic && tenantId) q = q.eq('tenant_id', tenantId);
else q = q.eq('owner_id', ownerId);
const { count } = await q;
agendamentosRecebidos.value = count || 0;
}
@@ -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;
}
@@ -79,7 +79,7 @@ async function onFileSelected(event, field) {
// Persiste imediatamente no banco sem fechar o accordion
const uid = ownerId.value;
const tenantId = await getActiveTenantId(uid);
await tenantDb().from('agendador_configuracoes').upsert({ owner_id: uid, ...buildPayload('identidade'), updated_at: new Date().toISOString() }, { onConflict: 'owner_id' });
await supabase.from('agendador_configuracoes').upsert({ owner_id: uid, tenant_id: tenantId, ...buildPayload('identidade'), updated_at: new Date().toISOString() }, { onConflict: 'owner_id' });
toast.add({ severity: 'success', summary: 'Imagem salva', life: 2000 });
}
} catch (e) {
@@ -361,7 +361,7 @@ async function load() {
const uid = await getOwnerId();
ownerId.value = uid;
const [{ data, error }] = await Promise.all([tenantDb().from('agendador_configuracoes').select('*').eq('owner_id', uid).maybeSingle(), loadPaymentSettings(uid)]);
const [{ data, error }] = await Promise.all([supabase.from('agendador_configuracoes').select('*').eq('owner_id', uid).maybeSingle(), loadPaymentSettings(uid)]);
if (error) throw error;
@@ -400,7 +400,7 @@ async function toggleAtivo() {
try {
const tenantId = await getActiveTenantId(uid);
await tenantDb().from('agendador_configuracoes').upsert({ owner_id: uid, ativo: novoAtivo, updated_at: new Date().toISOString() }, { onConflict: 'owner_id' });
await supabase.from('agendador_configuracoes').upsert({ owner_id: uid, tenant_id: tenantId, ativo: novoAtivo, updated_at: new Date().toISOString() }, { onConflict: 'owner_id' });
toast.add({
severity: novoAtivo ? 'success' : 'info',
@@ -422,7 +422,7 @@ async function saveCard(cardKey) {
const tenantId = await getActiveTenantId(uid);
const payload = buildPayload(cardKey);
await tenantDb().from('agendador_configuracoes').upsert({ owner_id: uid, ...payload, updated_at: new Date().toISOString() }, { onConflict: 'owner_id' });
await supabase.from('agendador_configuracoes').upsert({ owner_id: uid, tenant_id: tenantId, ...payload, updated_at: new Date().toISOString() }, { onConflict: 'owner_id' });
toast.add({ severity: 'success', summary: 'Salvo', life: 2500 });
expandedCard.value = new Set();
+10 -7
View File
@@ -343,7 +343,7 @@ async function load() {
ownerId.value = uid;
const [{ data, error }] = await Promise.all([
tenantDb().from('agendador_configuracoes').select('*').eq('owner_id', uid).maybeSingle(),
supabase.from('agendador_configuracoes').select('*').eq('owner_id', uid).maybeSingle(),
loadPaymentSettings(uid)
]);
if (error) throw error;
@@ -382,9 +382,10 @@ async function toggleAtivo() {
cfg.value.ativo = novoAtivo;
try {
const tenantId = await getActiveTenantId(uid);
const { error } = await tenantDb().from('agendador_configuracoes')
const { error } = await supabase
.from('agendador_configuracoes')
.upsert(
{ owner_id: uid, ativo: novoAtivo, updated_at: new Date().toISOString() },
{ owner_id: uid, tenant_id: tenantId, ativo: novoAtivo, updated_at: new Date().toISOString() },
{ onConflict: 'owner_id' }
);
if (error) throw error;
@@ -473,9 +474,10 @@ async function saveCard(cardKey) {
const uid = ownerId.value;
const tenantId = await getActiveTenantId(uid);
const payload = buildPayload(cardKey);
const { error } = await tenantDb().from('agendador_configuracoes')
const { error } = await supabase
.from('agendador_configuracoes')
.upsert(
{ owner_id: uid, ...payload, updated_at: new Date().toISOString() },
{ owner_id: uid, tenant_id: tenantId, ...payload, updated_at: new Date().toISOString() },
{ onConflict: 'owner_id' }
);
if (error) throw error;
@@ -516,9 +518,10 @@ async function onFileSelected(event, field) {
if (field === 'fundo') cfg.value.imagem_fundo_url = url;
const uid = ownerId.value;
const tenantId = await getActiveTenantId(uid);
await tenantDb().from('agendador_configuracoes')
await supabase
.from('agendador_configuracoes')
.upsert(
{ owner_id: uid, ...buildPayload('identidade'), updated_at: new Date().toISOString() },
{ owner_id: uid, tenant_id: tenantId, ...buildPayload('identidade'), updated_at: new Date().toISOString() },
{ onConflict: 'owner_id' }
);
toast.add({ severity: 'success', summary: 'Imagem salva', life: 2000 });
@@ -222,12 +222,14 @@ async function fetchSolicitacoes() {
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, recusado_motivo, autorizado_em, 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, recusado_motivo, autorizado_em, 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);
const { data, error } = await q;
if (error) throw error;
@@ -293,7 +295,8 @@ async function autorizar() {
if (!item) return;
dlg.value.saving = true;
try {
const { error } = await tenantDb().from('agendador_solicitacoes')
const { error } = await supabase
.from('agendador_solicitacoes')
.update({ status: 'autorizado', autorizado_em: new Date().toISOString() })
.eq('id', item.id);
if (error) throw error;
@@ -323,7 +326,8 @@ async function recusar() {
dlg.value.saving = true;
try {
const motivo = String(dlg.value.recusa_note || '').trim() || null;
const { error } = await tenantDb().from('agendador_solicitacoes')
const { error } = await supabase
.from('agendador_solicitacoes')
.update({ status: 'recusado', recusado_motivo: motivo })
.eq('id', item.id);
if (error) throw error;
@@ -442,7 +446,8 @@ async function onEventSaved(arg) {
const dbPayload = {};
for (const k of dbFields) if (normalized[k] !== undefined) dbPayload[k] = normalized[k];
await createEvento(dbPayload);
const { error } = await tenantDb().from('agendador_solicitacoes')
const { error } = await supabase
.from('agendador_solicitacoes')
.update({ status: 'convertido' })
.eq('id', target.id);
if (error) throw error;
@@ -16,7 +16,6 @@ import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
import { useToast } from 'primevue/usetoast';
import { useConfirm } from 'primevue/useconfirm';
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 — extração pro repository.
import { createPatient, markIntakeConverted } from '@/features/patients/services/patientsRepository';
@@ -258,7 +257,8 @@ const pagedItems = computed(() => {
async function fetchIntakes() {
loading.value = true;
try {
const { data, error } = await tenantDb().from('patient_intake_requests').select('*')
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);
@@ -346,7 +346,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')
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;
+7 -5
View File
@@ -15,8 +15,6 @@
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
// ── Helpers ──────────────────────────────────────────────────
async function getOwnerId() {
@@ -60,9 +58,11 @@ export async function createShareLink(documentoId, opts = {}) {
const expiraEm = new Date();
expiraEm.setHours(expiraEm.getHours() + expiracaoHoras);
const { data, error } = await tenantDb().from('document_share_links')
const { data, error } = await supabase
.from('document_share_links')
.insert({
documento_id: documentoId,
tenant_id: tenantId,
expira_em: expiraEm.toISOString(),
usos_max: opts.usosMax || 5,
criado_por: ownerId
@@ -81,7 +81,8 @@ export async function listShareLinks(documentoId) {
const ownerId = await getOwnerId();
const { data, error } = await tenantDb().from('document_share_links')
const { data, error } = await supabase
.from('document_share_links')
.select('*')
.eq('documento_id', documentoId)
.eq('criado_por', ownerId)
@@ -129,7 +130,8 @@ export async function deactivateShareLink(linkId) {
const ownerId = await getOwnerId();
const { error } = await tenantDb().from('document_share_links')
const { error } = await supabase
.from('document_share_links')
.update({ ativo: false })
.eq('id', linkId)
.eq('criado_por', ownerId);
+6 -4
View File
@@ -432,15 +432,17 @@ async function load() {
.eq('tenant_id', tid)
.in('role', ['therapist', 'tenant_admin'])
.eq('status', 'active'),
tenantDb().from('agendador_solicitacoes')
supabase
.from('agendador_solicitacoes')
.select('id, paciente_nome, paciente_sobrenome, tipo, modalidade, data_solicitada, hora_solicitada')
.eq('tenant_id', tid)
.eq('status', 'pendente')
.order('created_at', { ascending: false })
.limit(10),
tenantDb().from('patient_intake_requests')
supabase
.from('patient_intake_requests')
.select('id, nome_completo, status, created_at')
.eq('tenant_id', tid)
.eq('status', 'new')
.order('created_at', { ascending: false })
.limit(10)
@@ -562,7 +562,7 @@ const solicitacoesPendentes = computed(() => solicitacoes.value.length);
async function aceitarSol(id) {
try {
await tenantDb().from('agendador_solicitacoes').update({ status: 'autorizado' }).eq('id', id);
await supabase.from('agendador_solicitacoes').update({ status: 'autorizado' }).eq('id', id);
_solicitacoesBruto.value = _solicitacoesBruto.value.filter((s) => s.id !== id);
} catch (e) {
console.error('[TherapistDashboard] aceitarSol:', e);
@@ -570,7 +570,7 @@ async function aceitarSol(id) {
}
async function recusarSol(id) {
try {
await tenantDb().from('agendador_solicitacoes').update({ status: 'recusado' }).eq('id', id);
await supabase.from('agendador_solicitacoes').update({ status: 'recusado' }).eq('id', id);
_solicitacoesBruto.value = _solicitacoesBruto.value.filter((s) => s.id !== id);
} catch (e) {
console.error('[TherapistDashboard] recusarSol:', e);
@@ -900,13 +900,14 @@ async function load() {
.order('start_date', { ascending: false });
return q;
})(),
tenantDb().from('agendador_solicitacoes')
supabase
.from('agendador_solicitacoes')
.select('id, paciente_nome, paciente_sobrenome, tipo, modalidade, data_solicitada, hora_solicitada')
.eq('owner_id', ownerId.value)
.eq('status', 'pendente')
.order('created_at', { ascending: false })
.limit(10),
tenantDb().from('patient_intake_requests').select('id, nome_completo, status, created_at').eq('owner_id', ownerId.value).eq('status', 'new').order('created_at', { ascending: false }).limit(10)
supabase.from('patient_intake_requests').select('id, nome_completo, status, created_at').eq('owner_id', ownerId.value).eq('status', 'new').order('created_at', { ascending: false }).limit(10)
]);
eventosDoMes.value = eventosRes.data || [];
_solicitacoesBruto.value = solRes.data || [];