compliance CFP #7: RPCs de assinatura + service ext + composable

Backend foundation pra assinatura eletronica (ROADMAP #1.2 #7).

Migration 20260521000006 cria 3 RPCs:
  - sign_document_by_signature_id (paciente logado, SECURITY INVOKER)
  - sign_document_by_token        (terceiro via share link, SECURITY DEFINER)
  - get_signable_document_by_token (preview pre-assinatura)

IP + user-agent capturados SERVER-SIDE via inet_client_addr() e
current_setting('request.headers'). Hash SHA-256 vem do cliente
pra integridade. Token via share link incrementa usos no UPDATE.

DocumentSignatures.service estendido com 3 wrappers RPC: signByPortal,
signByToken, getSignableDocumentByToken. useDocumentSignatures composable
novo (Tipo A blueprint) expoe state reativo + acoes: fetchForDocument,
requestSignatures, sign, refuse, signWithToken, loadByToken.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-21 04:39:05 -03:00
parent 8601ac0d70
commit 934c620295
3 changed files with 453 additions and 0 deletions
@@ -170,3 +170,56 @@ export async function refuseSignature(signatureId) {
if (error) throw error;
return data;
}
// ── Assinar via portal (paciente logado) ─────────────────────
//
// IP e user-agent são capturados SERVER-SIDE pela RPC via
// inet_client_addr() e current_setting('request.headers'). O cliente
// só passa o hash SHA-256 do PDF (gerado via hashDocument()) pra
// garantir integridade do documento no momento da assinatura.
//
export async function signByPortal(signatureId, hashDocumento = null) {
if (!signatureId) throw new Error('ID da assinatura inválido.');
const { data, error } = await supabase.rpc('sign_document_by_signature_id', {
p_signature_id: signatureId,
p_hash_documento: hashDocumento || null
});
if (error) throw error;
return data;
}
// ── Assinar via token público (share link) ──────────────────
//
// Para signatários não-logados acessando via /shared/document/:token.
// p_signature_id é opcional — quando o documento tem múltiplos
// signatários, identifica qual deles está assinando. Quando há apenas
// um pendente, deixa null e o backend resolve.
//
export async function signByToken(token, signatureId = null, hashDocumento = null) {
if (!token) throw new Error('Token inválido.');
const { data, error } = await supabase.rpc('sign_document_by_token', {
p_token: token,
p_signature_id: signatureId || null,
p_hash_documento: hashDocumento || null
});
if (error) throw error;
return data;
}
// ── Pré-visualizar documento por token (sem assinar) ────────
//
// Usado pela página pública pra carregar info do documento +
// signatários pendentes ANTES do click em "Assinar". Retorna
// { valid, document, signatures, expira_em, usos_restantes }.
//
export async function getSignableDocumentByToken(token) {
if (!token) throw new Error('Token inválido.');
const { data, error } = await supabase.rpc('get_signable_document_by_token', {
p_token: token
});
if (error) throw error;
return data;
}