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
- Criar sessão de upload (
POST .../upload-sessions): o cliente declaratype,content_typeecontent_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 oPUTdireto no armazenamento. Repetir a mesmaIdempotency-Keycom o mesmo payload devolve a mesma sessão. - O cliente faz o upload diretamente para o armazenamento usando a URL assinada (fora da API).
- Completar (
POST /users/:userId/assets): a API baixa o objeto enviado, confere queContent-Length,Content-Typee 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 (versionesperado bate com o ativo atual, ou0se ainda não existe nenhum ativo daquele tipo para o usuário). - Completar é idempotente por
Idempotency-Key: reenviar a mesma chave com o mesmo payload de conclusão devolve o mesmo ativo já persistido, sem reprocessar. - Listar e criar sessão de download: exigem a permissão de leitura; a URL de download é assinada e válida por 60 segundos.
- Excluir: exige
If-Matchcom a versão atual do ativo (controle de concorrência otimista); versão desatualizada é conflito.
Endpoints
| Método | Rota | Descrição |
|---|---|---|
| POST | /v1/users/:userId/assets/upload-sessions | Cria uma sessão de upload assinada |
| POST | /v1/users/:userId/assets | Confirma um upload e cria/substitui o ativo |
| GET | /v1/users/:userId/assets | Lista os ativos do usuário |
| GET | /v1/users/:userId/assets/:assetId/download-session | Cria uma URL de download assinada |
| DELETE | /v1/users/:userId/assets/:assetId | Remove 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
| Rota | Guards | Permissão exigida |
|---|---|---|
POST .../upload-sessions, POST /assets, DELETE .../:assetId | IdentityOAuthAccessTokenGuard | identity-user-asset:manage, resolvida em algum escopo que cubra o userId alvo |
GET /assets, GET .../:assetId/download-session | idem | identity-user-asset:read, mesma regra de escopo |
Headers
| Header | Obrigatório | Descrição |
|---|---|---|
Authorization | Sim | Bearer <access_token> |
Idempotency-Key | Sim (POST .../upload-sessions, POST /assets) | única por ator/usuário/payload |
If-Match | Sim (DELETE) | versão atual do ativo, ex.: "3" |
Path parameters
| Nome | Tipo | Obrigatório | Descrição |
|---|---|---|---|
userId | uuid | Sim | Usuário dono dos ativos |
assetId | uuid | Sim (GET .../download-session, DELETE) | Identificador do ativo |
Body
POST .../upload-sessions — IdentityUserAssetUploadSessionRequest:
json{ "type": "PROFILE_PHOTO", "content_type": "image/jpeg", "content_length": 204800 }
| Campo | Tipo | Obrigatório | Validação |
|---|---|---|---|
type | PROFILE_PHOTO | SIGNATURE_IMAGE | Sim | enum |
content_type | string | Sim | não vazio |
content_length | int | Sim | 1 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}
| Campo | Tipo | Obrigatório | Validação |
|---|---|---|---|
type | enum | Sim | mesmo tipo declarado na sessão |
upload_session_id | string | Sim | sessão existente, não expirada, do mesmo usuário |
content_hash | string | Sim | formato sha256:<64 hex> |
version | int | Sim | 0 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)
| Tipo | MIME permitido | Tamanho máx. | Dimensão máx. | Regra extra |
|---|---|---|---|---|
PROFILE_PHOTO | image/png, image/jpeg, image/webp | 5 MB | 2048×2048 | — |
SIGNATURE_IMAGE | image/png, image/webp | 2 MB | 4096×2048 | precisa 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 erro | errorCode | Status | Quando ocorre |
|---|---|---|---|
ForbiddenAction | IDENTITY_USER_ASSET_FORBIDDEN | 403 | ator sem identity-user-asset:manage/:read no escopo do usuário alvo |
IdentityUserNotFoundError | IDENTITY_USER_NOT_FOUND | 404 | usuário alvo inativo (checado na criação da sessão) |
InvalidIdentityUserAssetError | INVALID_ASSET_MIME | 422 | forma declarada ou bytes reais fora dos limites da tabela acima, ou MIME declarado ≠ MIME real |
| ValidationError | IDEMPOTENCY_KEY_REQUIRED | 400 | header ausente ou vazio |
IdentityUserAssetIdempotencyKeyReusedError | IDEMPOTENCY_KEY_REUSED | 409 | mesma chave usada com payload diferente |
IdentityUserAssetUploadSessionNotFoundError | IDENTITY_USER_ASSET_UPLOAD_SESSION_NOT_FOUND | 404 | sessão inexistente, expirada, ou de outro usuário/tipo |
IdentityUserAssetHashMismatchError | IDENTITY_USER_ASSET_HASH_MISMATCH | 422 | hash declarado ≠ hash calculado do conteúdo enviado |
IdentityUserAssetVersionConflictError | IDENTITY_USER_ASSET_VERSION_CONFLICT | 409 | version informado não bate com a versão atual do ativo |
IdentityUserAssetNotFoundError | IDENTITY_USER_ASSET_NOT_FOUND | 404 | assetId inexistente para o usuário |
| ValidationError | VERSION_REQUIRED | 400 | If-Match ausente ou não numérico no DELETE |
| — | IDENTITY_INVALID_OAUTH_ACCESS_TOKEN | 401 | token ausente, inválido, expirado ou revogado |
Regras de negócio
| ID | Regra | Comportamento esperado |
|---|---|---|
| RN-01 | Upload é sempre em duas etapas | nunca há envio de arquivo direto para a API; a API só recebe metadados e hash |
| RN-02 | Bytes reais são a fonte de verdade | forma declarada (content_type/content_length) é só uma pré-checagem; a validação definitiva usa a imagem baixada do storage |
| RN-03 | Versão é otimista por tipo de ativo | cada (userId, type) tem sua própria sequência de versão, começando em 0 |
| RN-04 | Completar é idempotente, criar sessão também | mesma Idempotency-Key + mesmo payload nunca duplica trabalho nem gera dois ativos |
| RN-05 | Assinatura exige transparência; foto de perfil não | SIGNATURE_IMAGE sem canal alfa é rejeitada |
| RN-06 | Objetos intermediários nunca ficam acumulados | staging e versão substituída são removidos do armazenamento após a conclusão |
Compliance
| Órgão / norma | Exigência | Como a rota atende |
|---|---|---|
| LGPD | integridade e minimização | hash de conteúdo obrigatório; download só por URL assinada de curta duração |
| ANVISA (indireto) | integridade da assinatura associada a documento | validaçã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
| Requisito | Definição |
|---|---|
| Idempotência | Sim, em ambos os POST, por Idempotency-Key |
| Concorrência | Otimista, por version/If-Match |
| Auditoria | Sim — identity.user-asset.saved.v1, identity.user-asset.deleted.v1 |
Relacionado
- 🖥️ Tela:
A confirmar — responsável: time de frontend; data: 24/09/2026. - 📂 Módulo: Usuário
- 🖼️ Identidade visual (foto e assinatura) — caminho direto que cobre os mesmos dois tipos de imagem