Pular para o conteúdo principal

Upload de Imagens (Presign Flow)

As imagens não passam pelo servidor da API. O upload vai direto do seu computador para o S3 da AWS via presigned URL.

:::info Quem usa este fluxo?

  • Admin — upload de imagens base para templates (mockups)
  • Seller — upload de artworks (artes) :::

Como funciona

O que é uma Presigned URL?

Uma URL temporária gerada pelo servidor que dá permissão para upload direto no S3, sem credenciais da AWS:

  1. Servidor gera URL com assinatura criptográfica
  2. URL permite exatamente uma operação (PUT de um arquivo específico)
  3. Expira em 15 minutos

Upload de Template

Etapa 1: Presign

POST /products/templates/{templateId}/images/presign
{
"kind": "base",
"contentType": "image/png",
"sizeBytes": 1048576
}
CampoDescrição
kindTipo da imagem (ver tabela abaixo)
contentTypeMIME type do arquivo
sizeBytesTamanho em bytes

Kinds disponíveis:

KindObrigatórioDescrição
baseSimImagem principal do mockup
maskNãoMáscara da área de impressão
outlineNãoContorno do produto
shadowNãoSombra (blend multiply)
highlightNãoBrilho/reflexo (blend screen)
previewNãoThumbnail

Formatos aceitos:

ExtensãoContent-TypeRecomendado para
.pngimage/pngImagens com transparência (RGBA)
.jpg/.jpegimage/jpegFotos sem transparência
.webpimage/webpFotos otimizadas

Tamanho máximo: 25MB por arquivo.

Response
{
"uploadUrl": "https://s3.amazonaws.com/bucket/templates/caneca-350ml-black-v1/base.png?X-Amz-Signature=...",
"key": "templates/caneca-350ml-black-v1/base.png",
"kind": "base",
"headers": { "Content-Type": "image/png" }
}
aviso

Guarde o key — você vai precisar na Etapa 3.

Etapa 2: Upload direto no S3

Envie a imagem diretamente para o S3 usando a uploadUrl:

curl -X PUT \
"COLE_A_UPLOAD_URL_AQUI" \
-H "Content-Type: image/png" \
--data-binary @~/Downloads/caneca-preta.png

:::danger Content-Type deve ser exatamente o mesmo do presign Se no presign mandou image/jpeg, aqui tem que ser image/jpeg. Se divergir, o S3 retorna 403 Forbidden. :::

Resposta esperada: HTTP 200 OK com body vazio.

Erros comuns:

ErroCausaSolução
403 ForbiddenURL expirou ou Content-Type divergeFaça presign novamente
405 Method Not AllowedUsou POST em vez de PUTMude para PUT
400 Bad RequestContent-Type duplicado no headerDeixe apenas um

Etapa 3: Complete

POST /products/templates/{templateId}/images/complete
{
"kind": "base",
"key": "templates/caneca-350ml-black-v1/base.png"
}
Response
{
"kind": "base",
"key": "templates/caneca-350ml-black-v1/base.png",
"cdnUrl": "https://cdn.labanana.art/templates/caneca-350ml-black-v1/base.png",
"widthPx": 1200,
"heightPx": 1600
}

:::tip Auto-ativação Quando você faz complete de uma imagem base, o template é ativado automaticamente (isActive: true). :::

Upload em lote (batch)

Para enviar várias imagens de uma vez:

POST /products/templates/{templateId}/images/presign-batch
{
"images": [
{ "kind": "base", "contentType": "image/png", "sizeBytes": 1000000 },
{ "kind": "shadow", "contentType": "image/png", "sizeBytes": 500000 }
]
}

Confirmar todas de uma vez:

POST /products/templates/{templateId}/images/complete-batch
{
"kinds": ["base", "shadow"]
}

Upload de Artwork (Seller)

O fluxo é similar, com uma diferença: o registro no banco só é criado no complete.

:::info Diferença importante vs Template

  • Template: registro criado antes do upload (via POST .../templates)
  • Artwork: registro criado no complete (não existe no banco antes) :::

Etapa 1: Presign

POST /uploads/artworks/presign

Como seller:

{
"contentType": "image/png",
"sizeBytes": 2097152
}

Como admin (criando para um seller):

{
"contentType": "image/png",
"sizeBytes": 2097152,
"sellerProfileId": "uuid-do-seller"
}
Response
{
"uploadUrl": "https://s3.amazonaws.com/...",
"key": "private/sellers/{sellerProfileId}/artworks/{artworkId}/original/v1.png",
"artworkId": "uuid-gerado-automaticamente",
"headers": { "Content-Type": "image/png" }
}
aviso

Guarde o key e o artworkId — você precisa dos dois na Etapa 3.

Etapa 2: Upload direto no S3

Mesmo fluxo do template — PUT na uploadUrl com o mesmo Content-Type do presign.

Etapa 3: Complete

POST /uploads/artworks/{artworkId}/complete

Como seller:

{
"key": "private/sellers/{sellerProfileId}/artworks/{artworkId}/original/v1.png",
"title": "Minha Arte Abstrata"
}

Como admin (deve repetir o mesmo sellerProfileId do presign):

{
"key": "private/sellers/{sellerProfileId}/artworks/{artworkId}/original/v1.png",
"title": "Arte do Artista X",
"sellerProfileId": "uuid-do-seller"
}

O servidor:

  1. Verifica se o arquivo existe no S3
  2. Extrai dimensões, formato e hash
  3. Cria o registro Artwork no banco (vinculado ao seller)
  4. Gera preview WebP (max 1200px) no bucket público
Response
{
"artworkId": "uuid",
"artworkVersion": 1,
"widthPx": 3000,
"heightPx": 4000,
"sizeBytes": 2097152,
"mimeType": "image/png"
}

:::tip Transparência PNG O arquivo vai direto para o S3 sem processamento. Se estiver aparecendo com fundo branco em algum visualizador, pode ser:

  • O console do S3 não renderiza transparência (mostra branco)
  • O PNG original pode já ter sido salvo com fundo sólido (verifique no Figma/Photoshop — se o fundo xadrez aparece, há transparência)

O preview WebP gerado mantém transparência. :::

Estrutura no S3

private/sellers/{sellerProfileId}/artworks/{artworkId}/
├── original/
│ └── v1.png ← original (bucket privado)
├── meta/
│ └── v1.json ← metadados

public/sellers/{sellerProfileId}/artworks/{artworkId}/
├── preview/
│ └── v1.webp ← preview (bucket público)
└── renders/
└── {templateId}/
└── v1/
└── render.webp ← render final

:::info Versionamento O v1 no path é a versão da arte. Se o seller atualizar, a nova versão vai para v2.png e novos renders para v2/. :::


Upload de Imagens de Perfil

Avatar (seller + customer) e banner (só seller). Mesmo pattern presign → PUT S3 → complete. Bucket público com CDN — a URL retornada é permanente.

Limites

CampoValor
MIMEimage/png, image/jpeg, image/webp
Tamanho5 MB

Endpoints

MétodoRotaDescrição
POST/profiles/seller/me/upload-urlPresign seller (imageType: "avatar" ou "banner")
POST/profiles/seller/me/upload-completeCompleta e atualiza avatar_key / banner_key
DELETE/profiles/seller/me/image/{imageType}Remove do S3 e zera a key
POST/profiles/customer/me/upload-urlPresign customer (só avatar)
POST/profiles/customer/me/upload-completeCompleta
DELETE/profiles/customer/me/image/avatarRemove avatar

Fluxo

Etapa 1: Presign

POST /profiles/seller/me/upload-url
{
"imageType": "avatar",
"contentType": "image/png",
"sizeBytes": 1048576
}
Response
{
"uploadUrl": "https://s3.amazonaws.com/...",
"key": "public/sellers/{sellerProfileId}/avatar/v1.png",
"headers": { "Content-Type": "image/png" }
}

Etapa 2: PUT no S3

Mesmo padrão dos outros uploads — PUT uploadUrl com o arquivo binário e o Content-Type exatamente igual ao do presign.

Etapa 3: Complete

POST /profiles/seller/me/upload-complete
{
"key": "public/sellers/{sellerProfileId}/avatar/v1.png",
"imageType": "avatar"
}
Response
{
"url": "https://cdn.labanana.art/public/sellers/.../avatar/v1.png",
"key": "public/sellers/{sellerProfileId}/avatar/v1.png"
}

A url é permanente (CDN) — guarde e use diretamente. Não precisa refresh.

:::warning Validações

  • A key no complete deve ser igual à retornada no presign — o backend valida que o prefix da key bate com o profile autenticado (não dá pra fazer upload "no lugar de outro")
  • Se você chamar complete antes do PUT S3 completar, o backend retorna 404 (o HEAD no S3 falha). Só chame complete depois do PUT retornar 200 :::

Remover imagem

DELETE /profiles/seller/me/image/avatar

Deleta do S3 e zera avatar_key no perfil. O endpoint equivalente de customer só aceita avatar.