/* |-------------------------------------------------------------------------- | Agência PSI |-------------------------------------------------------------------------- | Criado e desenvolvido por Leonardo Nohama | | Tecnologia aplicada à escuta. | Estrutura para o cuidado. | | Arquivo: src/services/DocumentShareLinks.service.js | Data: 2026 | Local: São Carlos/SP — Brasil |-------------------------------------------------------------------------- | © 2026 — Todos os direitos reservados |-------------------------------------------------------------------------- */ import { supabase } from '@/lib/supabase/client'; // ── Helpers ────────────────────────────────────────────────── async function getOwnerId() { const { data, error } = await supabase.auth.getUser(); if (error) throw error; const uid = data?.user?.id; if (!uid) throw new Error('Sessão inválida.'); return uid; } async function getActiveTenantId(uid) { const { data, error } = await supabase .from('tenant_members') .select('tenant_id') .eq('user_id', uid) .eq('status', 'active') .order('created_at', { ascending: false }) .limit(1) .single(); if (error) throw error; if (!data?.tenant_id) throw new Error('Tenant não encontrado.'); return data.tenant_id; } // ── Criar link temporario ─────────────────────────────────── /** * Gera link temporario para compartilhar documento com profissional externo. * * @param {string} documentoId * @param {object} opts - { expiracaoHoras: 48, usosMax: 5 } * @returns {object} registro com token para montar a URL */ export async function createShareLink(documentoId, opts = {}) { if (!documentoId) throw new Error('Documento não informado.'); const ownerId = await getOwnerId(); const tenantId = await getActiveTenantId(ownerId); const expiracaoHoras = opts.expiracaoHoras || 48; const expiraEm = new Date(); expiraEm.setHours(expiraEm.getHours() + expiracaoHoras); 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 }) .select('*') .single(); if (error) throw error; return data; } // ── Listar links de um documento ──────────────────────────── export async function listShareLinks(documentoId) { if (!documentoId) return []; const ownerId = await getOwnerId(); const { data, error } = await supabase .from('document_share_links') .select('*') .eq('documento_id', documentoId) .eq('criado_por', ownerId) .order('criado_em', { ascending: false }); if (error) throw error; return data || []; } // ── Validar token (acesso publico) ────────────────────────── /** * Valida token de compartilhamento e retorna dados do documento. * Incrementa o contador de usos. * * @param {string} token * @returns {object|null} - { link, document } ou null se invalido/expirado */ export async function validateShareToken(token) { if (!token) return null; // V#46: SELECT direto pela tabela com policy "public read by token" foi // removido. Agora chama RPC validate_share_token (SECURITY DEFINER) que // valida + incrementa usos + loga acesso atomicamente. Sem race condition. const { data, error } = await supabase.rpc('validate_share_token', { p_token: token }); if (error) return null; if (!data?.document_id) return null; return { link: { id: null, token, documento_id: data.document_id }, // estrutura mínima compat document: { id: data.document_id, nome_original: data.nome_original, mime_type: data.mime_type, bucket_path: data.bucket_path, storage_bucket: data.bucket } }; } // ── Desativar link ────────────────────────────────────────── export async function deactivateShareLink(linkId) { if (!linkId) throw new Error('ID inválido.'); const ownerId = await getOwnerId(); const { error } = await supabase .from('document_share_links') .update({ ativo: false }) .eq('id', linkId) .eq('criado_por', ownerId); if (error) throw error; return true; } // ── Montar URL publica ────────────────────────────────────── /** * Monta a URL de compartilhamento a partir do token. * A rota publica deve ser configurada no router. */ export function buildShareUrl(token) { const base = window.location.origin; return `${base}/shared/document/${token}`; }