Edição Protegida
A EditPolicy permite restringir quem pode abrir o designer e salvar alterações em relatórios. Quando ativa, o Studio solicita um código OTP por email antes de liberar a edição e exige um segundo código OTP para confirmar o salvamento.
┌──────────┐ POST /edit/verify ┌──────────────┐ email ┌──────────┐
│ Studio │────────────────────────▶│ Server │──────────▶│ Email │
│ (edit) │◀── { status: sent } │ (gera OTP) │ │ (OTP) │
│ │ └──────────────┘ └──────────┘
│ │ POST /edit/confirm
│ │ { email, reportId, code, intent: "edit" }
│ │────────────────────────▶┌──────────────┐
│ │◀── { token, expiresAt } │ Server │
│ │ │ (verifica + │
│ │ (usa token para editar)│ emite token)│
│ │ └──────────────┘
│ (save) │ POST /edit/verify (intent: "save")
│ │────────────────────────▶ (mesmo fluxo)
│ │◀── saveToken
│ │
│ │ PUT /api/v1/reports/:id
│ │ X-Edit-Token: <saveToken>
│ │────────────────────────▶┌──────────────┐
│ │◀── { report atualizado }│ Server │
└──────────┘ └──────────────┘Dois intents distintos:
| Intent | Quando | TTL do token |
|---|---|---|
edit | Ao abrir o designer | 1 hora (configurável) |
save | Ao confirmar salvamento | 5 minutos (configurável) |
Os dois tokens são assinados com HMAC-SHA256, ligados ao reportId e ao email verificado.
Endpoints
Política — GET /api/v1/edit/policy
Retorna a configuração de EditPolicy do servidor. O cliente usa isso para decidir se deve exibir o fluxo OTP.
Response 200:
{
"enabled": true,
"editTokenExpirationSeconds": 3600,
"saveTokenExpirationSeconds": 300,
"otpExpirationSeconds": 300,
"identityHints": ["@acme.com"]
}Solicitar OTP — POST /api/v1/edit/verify
Gera e envia um código OTP para o email informado.
Body:
{
"email": "user@acme.com",
"reportId": "uuid-do-relatorio",
"intent": "edit"
}| Campo | Obrigatório | Valores |
|---|---|---|
email | Sim | email válido |
reportId | Sim | ID do relatório |
intent | Não | "edit" (default) ou "save" |
Response 200:
{
"status": "sent",
"expiresIn": 300
}Erros:
| Status | Erro | Quando |
|---|---|---|
400 | invalid_email | Email inválido |
400 | validation_error | reportId ausente |
400 | invalid_intent | intent diferente de "edit" ou "save" |
400 | policy_disabled | EditPolicy não habilitada no servidor |
403 | identity_not_allowed | Email/domínio não permitido por EDIT_ALLOWED_IDENTITIES |
404 | not_found | Relatório não encontrado |
429 | rate_limited | Código já enviado recentemente |
503 | not_configured | OTP service não configurado (sem email provider) |
503 | service_unavailable | Falha no envio do email ou OTP store cheio |
Confirmar OTP — POST /api/v1/edit/confirm
Verifica o código OTP e retorna um token assinado.
Body:
{
"email": "user@acme.com",
"reportId": "uuid-do-relatorio",
"code": "123456",
"intent": "edit"
}Response 200:
{
"token": "<base64url(payload)>.<hmac>",
"expiresAt": "2026-04-11T18:00:00.000Z"
}Erros:
| Status | Erro | Quando |
|---|---|---|
400 | invalid_code | Código incorreto |
400 | otp_not_found | Nenhum código pendente para este email/relatório |
400 | policy_disabled | EditPolicy não habilitada |
403 | identity_not_allowed | Email/domínio não permitido |
404 | not_found | Relatório não encontrado |
410 | expired | Código expirado |
423 | max_attempts | Tentativas esgotadas — solicitar novo código |
503 | not_configured | OTP service não configurado |
Salvar com token — PUT /api/v1/reports/:id
Quando a EditPolicy está ativa, o endpoint de atualização exige o header X-Edit-Token com um token de intent save.
Headers:
X-Edit-Token: <saveToken>Erros específicos da EditPolicy:
| Status | Erro | Quando |
|---|---|---|
401 | save_token_required | Header X-Edit-Token ausente |
403 | save_token_invalid | Token inválido, expirado, intent errado ou reportId diferente |
Configuração
Variáveis de ambiente
| Variável | Default | Descrição |
|---|---|---|
EDIT_POLICY_ENABLED | false | Habilita a EditPolicy |
EDIT_ALLOWED_IDENTITIES | — (todos) | Emails ou domínios autorizados, separados por vírgula |
EDIT_TOKEN_SECRET | — (efêmero) | Segredo HMAC para assinar tokens. Sem essa variável, tokens expiram ao reiniciar o servidor |
EDIT_TOKEN_EXPIRATION | 3600 | TTL do token de edição em segundos |
SAVE_TOKEN_EXPIRATION | 300 | TTL do token de salvamento em segundos |
EDIT_OTP_EXPIRATION | 300 | Expiração do código OTP em segundos |
EDIT_OTP_MAX_ATTEMPTS | 3 | Tentativas máximas antes de invalidar |
EDIT_OTP_LENGTH | 6 | Comprimento do código OTP numérico |
Também é necessário configurar um email provider. Veja Configuração → Email.
Exemplo de configuração
export EDIT_POLICY_ENABLED=true
export EDIT_ALLOWED_IDENTITIES="acme.com,admin@parceiro.com"
export EDIT_TOKEN_SECRET="segredo-hmac-longo-e-aleatorio"
export RESEND_API_TOKEN="re_..."
export RESEND_FROM_EMAIL="noreply@acme.com"Fluxo completo
1. Cliente detecta a política
GET /api/v1/edit/policy
# → { "enabled": true, "editTokenExpirationSeconds": 3600, ... }Se enabled: true, o Studio exibe o fluxo OTP ao abrir o designer.
2. Solicitar OTP de edição
curl -X POST https://server.com/api/v1/edit/verify \
-H "Content-Type: application/json" \
-d '{ "email": "user@acme.com", "reportId": "abc123", "intent": "edit" }'
# Response: { "status": "sent", "expiresIn": 300 }3. Confirmar e obter token de edição
curl -X POST https://server.com/api/v1/edit/confirm \
-H "Content-Type: application/json" \
-d '{ "email": "user@acme.com", "reportId": "abc123", "code": "482910", "intent": "edit" }'
# Response: { "token": "eyJ...<payload>.<sig>", "expiresAt": "..." }O Studio armazena o token e libera a interface de edição pelo tempo configurado em EDIT_TOKEN_EXPIRATION.
4. Solicitar OTP de salvamento
Ao salvar, o Studio repete o fluxo com intent: "save":
curl -X POST https://server.com/api/v1/edit/verify \
-d '{ ..., "intent": "save" }'
curl -X POST https://server.com/api/v1/edit/confirm \
-d '{ ..., "intent": "save" }'
# → saveToken com TTL de SAVE_TOKEN_EXPIRATION segundos5. Salvar o relatório
curl -X PUT https://server.com/api/v1/reports/abc123 \
-H "X-Edit-Token: <saveToken>" \
-H "Content-Type: application/json" \
-d '{ "name": "Nome", "definitionJson": "{...}" }'Formato do token
O token é composto por dois segmentos separados por .:
base64url(payload).hmac_sha256O payload é um JSON com:
{
"reportId": "uuid-do-relatorio",
"email": "user@acme.com",
"intent": "edit",
"exp": 1713456000000
}exp é um timestamp em milissegundos (não segundos). O servidor rejeita tokens expirados, com signature inválida, intent diferente de "save" (no PUT) ou reportId diferente do recurso acessado.
Segurança
| Proteção | Detalhe |
|---|---|
| OTP hashing | SHA-256 — código nunca armazenado em plaintext |
| Comparação de tokens | Timing-safe (evita timing attacks) |
| Token bound ao relatório | reportId no payload — token de um relatório não serve para outro |
| Token bound ao intent | Token edit não aceito em PUT /reports/:id |
| Expiração curta para save | Default 5 minutos — reduz janela de uso indevido |
| Segredo persistente | Configurar EDIT_TOKEN_SECRET para que tokens sobrevivam a reinícios |
| Auditoria | Eventos edit_verify_requested, edit_identity_rejected, edit_otp_sent, edit_otp_failed (ver Segurança → Auditoria) |