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:
{
"format": "pdf",
"expiresIn": 172800,
"maxViews": 0,
"accessCode": true,
"recipientEmail": "cliente@example.com",
"params": { "region": "Sul" }
}
| Campo | Obrigatório | Default | Descrição |
|---|---|---|---|
format |
Não | pdf |
pdf, html, csv, excel |
expiresIn |
Não | 172800 (48h) |
TTL em segundos (min: 300, max: 2592000) |
maxViews |
Não | 0 |
0 = ilimitado, 1 = one-time, N = máximo |
accessCode |
Não | false |
true
= gerar código de 8 caracteres alfanuméricos (A–Z, 0–9); ou string
"AB3X9M2P"
para código personalizado
|
recipientEmail |
Não | null |
Envia notificação por email (se SMTP configurado) |
params | Não | null | Parâmetros do relatório |
Response 201:
{
"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:
| Status | Erro | Quando |
|---|---|---|
400 |
validation_error |
Formato inválido, expiresIn fora do range |
403 |
forbidden |
Sem canShare ou canGenerate |
404 | not_found | Relatório não encontrado |
413 | output_too_large | Output excede limite |
422 | generation_failed | Falha na renderização |
507 |
insufficient_storage |
Armazenamento total excedido |
Listar shares — GET /api/v1/reports/:id/shares#
Permissão: canShare
Response 200:
{
"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
}
]
}
Revogar share — DELETE /api/v1/reports/:id/shares/:shareId#
Permissão: canShare
Response 200:
{ "revoked": true, "id": "uuid" }
Revogar todos — DELETE /api/v1/reports/:id/shares#
Permissão: canShare
Response 200:
{ "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:
| Param | Descrição |
|---|---|
code | Código de acesso (8 caracteres alfanuméricos (A–Z, 0–9)) |
download |
true = 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:
| Status | Quando |
|---|---|
403 | IP banido (fail2ban) |
404 | Slug não encontrado |
410 | Expirado, 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:
{
"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ção | Limite |
|---|---|
| Acesso a share | 10/min por IP |
| Tentativas de código | 5/min por IP |
| Info de share | 30/min por IP |
| Fail2ban | 15 req/min em /s/ → IP banido por 1h |
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:
- O servidor gera o share link normalmente
- Envia um email de notificação com o link (e código, se aplicável)
- O envio é não-bloqueante — falha no email não impede a criação do share
Auditoria#
Eventos logados no audit log:
| Evento | Quando |
|---|---|
share_created | Share criado |
share_accessed | Share acessado com sucesso |
share_code_failed | Código incorreto |
share_code_lockout | Share excluído após 5 tentativas erradas |
share_ip_banned | IP banido por fail2ban |
share_revoked | Share revogado manualmente |
share_expired_access | Tentativa de acesso a share expirado |
Exemplo completo#
Criar e compartilhar#
# 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#
# 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#
# 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.