Skip to content

Compartilhamento

O sistema de compartilhamento permite gerar links públicos para relatórios pré-renderizados. O destinatário acessa o relatório sem precisar de conta ou token — basta o link (e opcionalmente um código de acesso).

┌──────────┐     POST /share     ┌──────────────┐     GET /s/:slug     ┌──────────┐
│  Admin    │───────────────────▶│  Server       │◀────────────────────│  Visitor  │
│  (auth)   │◀──── URL + código  │  (gera + arma │────── PDF/HTML ────▶│  (público)│
└──────────┘                     │   zena bytes) │                     └──────────┘
                                 └──────────────┘

Princípios:

  • O relatório é pré-gerado no momento da criação do share — o link público serve bytes estáticos
  • O endpoint público nunca executa o engine — zero risco de SSRF, latência de milissegundos
  • O cliente não envia dados para o share — o servidor resolve tudo internamente

Endpoints

Criar share — POST /api/v1/reports/:id/share

Permissão: canShare + canGenerate

Body:

json
{
  "format": "pdf",
  "expiresIn": 172800,
  "maxViews": 0,
  "accessCode": true,
  "recipientEmail": "cliente@example.com",
  "params": { "region": "Sul" }
}
CampoObrigatórioDefaultDescrição
formatNãopdfpdf, html, csv, excel
expiresInNão172800 (48h)TTL em segundos (min: 300, max: 2592000)
maxViewsNão00 = ilimitado, 1 = one-time, N = máximo
accessCodeNãofalsetrue = gerar código de 8 caracteres alfanuméricos (A–Z, 0–9); ou string "AB3X9M2P" para código personalizado
recipientEmailNãonullEnvia notificação por email (se SMTP configurado)
paramsNãonullParâmetros do relatório

dataPayload não aceito

O body não aceita dataPayload. O servidor usa dados próprios (metadata['defaultData'] ou resolvers configurados) para garantir integridade dos relatórios compartilhados.

Response 201:

json
{
  "id": "uuid",
  "slug": "aB3xkZ9mPqR2nW7vL4jYhC",
  "url": "https://server.com/s/aB3xkZ9mPqR2nW7vL4jYhC",
  "accessCode": "AB3X9M2P",
  "expiresAt": "2026-04-08T12:00:00.000Z",
  "maxViews": 0,
  "recipientEmail": "cliente@example.com",
  "emailDeliveryStatus": "sent",
  "authProvider": "none",
  "format": "pdf",
  "outputSize": 245760
}

O accessCode só é retornado nesta resposta — nunca em listagens.

O campo emailDeliveryStatus é "sent" quando o email foi entregue ou "failed" quando falhou. Nesse caso, um campo adicional emailWarning descreve o erro. O share é criado independentemente da entrega do email.

Erros:

StatusErroQuando
400validation_errorFormato inválido, expiresIn fora do range
403forbiddenSem canShare ou canGenerate
404not_foundRelatório não encontrado
413output_too_largeOutput excede limite
422generation_failedFalha na renderização
507insufficient_storageArmazenamento total excedido

Listar shares — GET /api/v1/reports/:id/shares

Permissão: canShare

Response 200:

json
{
  "shares": [
    {
      "id": "uuid",
      "slug": "aB3xkZ9mPqR2nW7vL4jYhC",
      "format": "pdf",
      "createdAt": "2026-04-06T12:00:00.000Z",
      "expiresAt": "2026-04-08T12:00:00.000Z",
      "maxViews": 1,
      "viewCount": 0,
      "hasAccessCode": true,
      "recipientEmail": "cli***@example.com",
      "authProvider": "none",
      "revoked": false,
      "lastViewedAt": null
    }
  ]
}

Mascaramento de email

O recipientEmail é mascarado nas listagens (primeiros 3 chars + *** + domínio).


Revogar share — DELETE /api/v1/reports/:id/shares/:shareId

Permissão: canShare

Response 200:

json
{ "revoked": true, "id": "uuid" }

Revogar todos — DELETE /api/v1/reports/:id/shares

Permissão: canShare

Response 200:

json
{ "revoked": true, "count": 5 }

Acesso público

Os endpoints públicos ficam fora do middleware de autenticação.

Acessar share — GET /s/:slug

Autenticação: nenhuma (endpoint público)

Query params:

ParamDescrição
codeCódigo de acesso (8 caracteres alfanuméricos (A–Z, 0–9))
downloadtrue = Content-Disposition: attachment (default: inline)

Fluxo:

GET /s/:slug

    ├─ Slug não encontrado? → 404 (página HTML de erro)
    ├─ Revogado? → 410 (página HTML "Link revogado")
    ├─ Expirado? → 410 (página HTML "Link expirado")
    ├─ maxViews atingido? → 410 (página HTML "Limite de visualizações")

    ├─ Código de acesso configurado?
    │   ├─ Sem código → página HTML com formulário
    │   ├─ Código correto → serve o relatório
    │   └─ Código incorreto → página HTML com erro + tentativas restantes

    └─ Tudo OK → serve os bytes pré-gerados

Headers de resposta:

Content-Type: application/pdf
Content-Disposition: inline; filename="report-name.pdf"
Cache-Control: no-store, private
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

Erros:

StatusQuando
403IP banido (fail2ban)
404Slug não encontrado
410Expirado, revogado, ou max views atingido

Info do share — GET /s/:slug/info

Retorna metadados sem baixar o relatório. Útil para landing pages customizadas.

Response 200:

json
{
  "slug": "aB3xkZ9mPqR2nW7vL4jYhC",
  "reportName": "Vendas Mensal",
  "format": "pdf",
  "hasAccessCode": true,
  "authProvider": "none",
  "expiresAt": "2026-04-08T12:00:00.000Z",
  "viewCount": 3,
  "maxViews": 0
}

Segurança

Slug

  • 16 bytes random (Random.secure()) → base64url → 22 chars, 128 bits de entropia
  • Brute force: 2^128 possibilidades → inviável

Código de acesso

  • 8 caracteres alfanuméricos (A–Z, 0–9), gerado via Random.secure()
  • Armazenado como hash (SHA-256 com salt) — nunca em plaintext
  • Comparação timing-safe
  • 5 tentativas máximas — após 5 erros, o share inteiro é excluído (metadata + bytes)

Rate limiting público

AçãoLimite
Acesso a share10/min por IP
Tentativas de código5/min por IP
Info de share30/min por IP
Fail2ban15 req/min em /s/ → IP banido por 1h

Fail2ban

Diferente do rate limit (que retorna 429), o fail2ban bloqueia o IP em todos os endpoints /s/ por 1 hora. O ban se aplica a qualquer combinação de slugs.

Sem SSRF no acesso público

O endpoint /s/:slug serve apenas bytes pré-gerados — nunca resolve datasources. Isso elimina completamente o vetor SSRF no acesso público.

Resolução de dados (server-only)

O POST /share não aceita dados do cliente. Para relatórios com datasources inline, os dados são injetados em definition.metadata['defaultData'] no seeding do relatório (server-side). Para datasources externos, o servidor usa seus próprios resolvers.


Landing page

Quando um share exige código de acesso, o servidor retorna uma página HTML self-contained com:

  • Formulário para inserir o código de 8 caracteres alfanuméricos (A–Z, 0–9)
  • Nome do relatório e formato
  • Mensagens de erro com tentativas restantes
  • Design responsivo, sem dependências externas
  • Proteção XSS nos campos dinâmicos

Páginas de erro (expirado, revogado, banido) também são servidas como HTML.


Email

Se um EmailProvider estiver configurado e recipientEmail for informado na criação:

  1. O servidor gera o share link normalmente
  2. Envia um email de notificação com o link (e código, se aplicável)
  3. O envio é não-bloqueante — falha no email não impede a criação do share

Auditoria

Eventos logados no audit log:

EventoQuando
share_createdShare criado
share_accessedShare acessado com sucesso
share_code_failedCódigo incorreto
share_code_lockoutShare excluído após 5 tentativas erradas
share_ip_bannedIP banido por fail2ban
share_revokedShare revogado manualmente
share_expired_accessTentativa de acesso a share expirado

Exemplo completo

Criar e compartilhar

bash
# Criar share com código de acesso (48h de validade)
curl -X POST https://server.com/api/v1/reports/abc123/share \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "format": "pdf",
    "expiresIn": 172800,
    "accessCode": true,
    "recipientEmail": "cliente@example.com"
  }'

# Response:
# {
#   "url": "https://server.com/s/aB3xkZ9mPqR2nW7vL4jYhC",
#   "accessCode": "AB3X9M2P",
#   "emailDeliveryStatus": "sent",
#   ...
# }

Acessar o share

bash
# Sem código — retorna landing page HTML
curl https://server.com/s/aB3xkZ9mPqR2nW7vL4jYhC

# Com código — retorna o PDF
curl -o report.pdf \
  "https://server.com/s/aB3xkZ9mPqR2nW7vL4jYhC?code=AB3X9M2P"

# Download forçado
curl -o report.pdf \
  "https://server.com/s/aB3xkZ9mPqR2nW7vL4jYhC?code=AB3X9M2P&download=true"

Listar e revogar

bash
# Listar shares ativos
curl https://server.com/api/v1/reports/abc123/shares \
  -H "Authorization: Bearer $TOKEN"

# Revogar um share específico
curl -X DELETE https://server.com/api/v1/reports/abc123/shares/share-uuid \
  -H "Authorization: Bearer $TOKEN"

# Revogar todos os shares de um relatório
curl -X DELETE https://server.com/api/v1/reports/abc123/shares \
  -H "Authorization: Bearer $TOKEN"

Studio

O Sulfite Studio oferece uma interface visual para compartilhamento:

  • ShareReportDialog — escolha formato, validade, código de acesso e email
  • Botão de share no toolbar do Designer, Consumer e Viewer
  • Gerenciamento — liste e revogue shares ativos

Veja Studio para mais detalhes.

Sulfite do 🇧🇷 para o mundo © 2026 Rafael S. Pinheiro