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 |
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:
{
"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
}
]
}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:
{ "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é-geradosHeaders 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: DENYErros:
| 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 |
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:
- 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.