Skip to main content

Ativos do usuário (upload versionado) — API

Gerencia arquivos de imagem do usuário (hoje PROFILE_PHOTO e SIGNATURE_IMAGE) por um fluxo de upload em duas etapas — sessão assinada seguida de confirmação com hash de conteúdo — com controle de versão otimista, idempotência e download por URL assinada. É o caminho mais recente do código, do pacote identity/user-asset, e convive com o caminho direto de multipart do pacote user (ver Identidade visual); nenhum dos dois foi identificado como descontinuado.

Funcionamento​

  1. Criar sessão de upload (POST .../upload-sessions): o cliente declara type, content_type e content_length; o backend valida essa forma declarada contra limites por tipo (tamanho e MIME permitidos) e devolve uma URL de upload assinada, válida por 5 minutos, mais os headers obrigatórios que o cliente deve reenviar ao fazer o PUT direto no armazenamento. Repetir a mesma Idempotency-Key com o mesmo payload devolve a mesma sessão.
  2. O cliente faz o upload diretamente para o armazenamento usando a URL assinada (fora da API).
  3. Completar (POST /users/:userId/assets): a API baixa o objeto enviado, confere que Content-Length, Content-Type e a "audiência" batem com a sessão, inspeciona os bytes reais da imagem (dimensões, transparência, MIME real) e confere o hash declarado (sha256:...) contra o hash calculado. Só então copia o objeto para seu destino definitivo e grava o registro do ativo — com controle de versão otimista (version esperado bate com o ativo atual, ou 0 se ainda não existe nenhum ativo daquele tipo para o usuário).
  4. Completar é idempotente por Idempotency-Key: reenviar a mesma chave com o mesmo payload de conclusão devolve o mesmo ativo já persistido, sem reprocessar.
  5. Listar e criar sessão de download: exigem a permissão de leitura; a URL de download é assinada e válida por 60 segundos.
  6. Excluir: exige If-Match com a versão atual do ativo (controle de concorrência otimista); versão desatualizada é conflito.

Endpoints​

MétodoRotaDescrição
POST/v1/users/:userId/assets/upload-sessionsCria uma sessão de upload assinada
POST/v1/users/:userId/assetsConfirma um upload e cria/substitui o ativo
GET/v1/users/:userId/assetsLista os ativos do usuário
GET/v1/users/:userId/assets/:assetId/download-sessionCria uma URL de download assinada
DELETE/v1/users/:userId/assets/:assetIdRemove um ativo (requer If-Match)

Versão: v1

Swagger: Identity — User assets · Rota (Dev): http://localhost:3000/v1/users/:userId/assets

Fluxo de upload em duas etapas:

Permissões​

RotaGuardsPermissão exigida
POST .../upload-sessions, POST /assets, DELETE .../:assetIdIdentityOAuthAccessTokenGuardidentity-user-asset:manage, resolvida em algum escopo que cubra o userId alvo
GET /assets, GET .../:assetId/download-sessionidemidentity-user-asset:read, mesma regra de escopo

Headers​

HeaderObrigatórioDescrição
AuthorizationSimBearer <access_token>
Idempotency-KeySim (POST .../upload-sessions, POST /assets)única por ator/usuário/payload
If-MatchSim (DELETE)versão atual do ativo, ex.: "3"

Path parameters​

NomeTipoObrigatórioDescrição
userIduuidSimUsuário dono dos ativos
assetIduuidSim (GET .../download-session, DELETE)Identificador do ativo

Body​

POST .../upload-sessions — IdentityUserAssetUploadSessionRequest:

json
{ "type": "PROFILE_PHOTO", "content_type": "image/jpeg", "content_length": 204800 }
CampoTipoObrigatórioValidação
typePROFILE_PHOTO | SIGNATURE_IMAGESimenum
content_typestringSimnão vazio
content_lengthintSim1 a 5.242.880 (5 MB) no corpo; limite efetivo por tipo é validado de novo no domínio

POST /users/:userId/assets — IdentityUserAssetCompletionRequest:

json
{
"type": "PROFILE_PHOTO",
"upload_session_id": "0192f3c6-...-fe83b",
"content_hash": "sha256:1c3a...b90f",
"version": 0
}
CampoTipoObrigatórioValidação
typeenumSimmesmo tipo declarado na sessão
upload_session_idstringSimsessão existente, não expirada, do mesmo usuário
content_hashstringSimformato sha256:<64 hex>
versionintSim0 para o primeiro ativo daquele tipo; versão atual para substituir

Limites por tipo de ativo (validados nos bytes reais, não só na forma declarada)​

TipoMIME permitidoTamanho máx.Dimensão máx.Regra extra
PROFILE_PHOTOimage/png, image/jpeg, image/webp5 MB2048×2048—
SIGNATURE_IMAGEimage/png, image/webp2 MB4096×2048precisa ter canal de transparência

Response​

201 — sessão de upload ({ data: IdentityUserAssetUploadSessionResponse }):

json
{
"data": {
"id": "0192f3c6-...-fe83b",
"uploadUrl": "https://storage.exemplo.com/...",
"requiredHeaders": { "content-type": "image/jpeg", "x-amz-meta-audience": "identity-user-asset:8f2a...:0192f3c6..." },
"expiresAt": "2026-08-26T12:05:00.000Z"
}
}

201 — ativo confirmado ({ data: IdentityUserAssetResponse }):

json
{
"data": {
"id": "...", "userId": "8f2a...-uuid", "type": "PROFILE_PHOTO",
"contentHash": "sha256:1c3a...b90f", "mime": "image/jpeg",
"width": 512, "height": 512, "version": 1,
"createdAt": "2026-08-26T12:00:00.000Z", "updatedAt": "2026-08-26T12:00:00.000Z"
}
}

200 — download session ({ data: IdentityUserAssetDownloadSessionResponse }):

json
{ "data": { "url": "https://storage.exemplo.com/...", "expiresAt": "...", "audience": "user:8f2a...-uuid" } }

204 — DELETE: sem corpo.

Erros​

Classe de erroerrorCodeStatusQuando ocorre
ForbiddenActionIDENTITY_USER_ASSET_FORBIDDEN403ator sem identity-user-asset:manage/:read no escopo do usuário alvo
IdentityUserNotFoundErrorIDENTITY_USER_NOT_FOUND404usuário alvo inativo (checado na criação da sessão)
InvalidIdentityUserAssetErrorINVALID_ASSET_MIME422forma declarada ou bytes reais fora dos limites da tabela acima, ou MIME declarado ≠ MIME real
ValidationErrorIDEMPOTENCY_KEY_REQUIRED400header ausente ou vazio
IdentityUserAssetIdempotencyKeyReusedErrorIDEMPOTENCY_KEY_REUSED409mesma chave usada com payload diferente
IdentityUserAssetUploadSessionNotFoundErrorIDENTITY_USER_ASSET_UPLOAD_SESSION_NOT_FOUND404sessão inexistente, expirada, ou de outro usuário/tipo
IdentityUserAssetHashMismatchErrorIDENTITY_USER_ASSET_HASH_MISMATCH422hash declarado ≠ hash calculado do conteúdo enviado
IdentityUserAssetVersionConflictErrorIDENTITY_USER_ASSET_VERSION_CONFLICT409version informado não bate com a versão atual do ativo
IdentityUserAssetNotFoundErrorIDENTITY_USER_ASSET_NOT_FOUND404assetId inexistente para o usuário
ValidationErrorVERSION_REQUIRED400If-Match ausente ou não numérico no DELETE
—IDENTITY_INVALID_OAUTH_ACCESS_TOKEN401token ausente, inválido, expirado ou revogado

Regras de negócio​

IDRegraComportamento esperado
RN-01Upload é sempre em duas etapasnunca há envio de arquivo direto para a API; a API só recebe metadados e hash
RN-02Bytes reais são a fonte de verdadeforma declarada (content_type/content_length) é só uma pré-checagem; a validação definitiva usa a imagem baixada do storage
RN-03Versão é otimista por tipo de ativocada (userId, type) tem sua própria sequência de versão, começando em 0
RN-04Completar é idempotente, criar sessão tambémmesma Idempotency-Key + mesmo payload nunca duplica trabalho nem gera dois ativos
RN-05Assinatura exige transparência; foto de perfil nãoSIGNATURE_IMAGE sem canal alfa é rejeitada
RN-06Objetos intermediários nunca ficam acumuladosstaging e versão substituída são removidos do armazenamento após a conclusão

Compliance​

Órgão / normaExigênciaComo a rota atende
LGPDintegridade e minimizaçãohash de conteúdo obrigatório; download só por URL assinada de curta duração
ANVISA (indireto)integridade da assinatura associada a documentovalidação de bytes reais e versionamento otimista evitam substituição silenciosa/corrompida

Variáveis de ambiente​

Nenhuma específica a esta rota (usa a configuração geral de armazenamento do módulo identity).

Requisitos não funcionais​

RequisitoDefinição
IdempotênciaSim, em ambos os POST, por Idempotency-Key
ConcorrênciaOtimista, por version/If-Match
AuditoriaSim — identity.user-asset.saved.v1, identity.user-asset.deleted.v1

Relacionado​