# DevOps Guide - SaveInMed

## Status (pronto x faltando)

**Pronto**
- Conteúdo descrito neste documento.

**Faltando**
- Confirmar no código o estado real das funcionalidades e atualizar esta seção conforme necessário.

---


Guia de operações e infraestrutura do SaveInMed.

---

## Scaleway Container Registry

### 1. Login no Registry

```bash
# Login no registry Scaleway (usar secret do CI/CD)
echo "${SCW_SECRET_KEY}" | docker login rg.fr-par.scw.cloud -u nologin --password-stdin
```

> [!CAUTION]
> **NUNCA** commitar credenciais no código. Use variáveis de ambiente ou secrets do CI/CD.

### 2. Build da Imagem

```bash
# Build com platform específico (importante para deploy em servidores AMD64)
docker build --platform linux/amd64 --no-cache -t saveinmed-frontend:latest .

# Com BuildKit para cache otimizado
DOCKER_BUILDKIT=1 docker build \
  --platform linux/amd64 \
  --cache-from rg.fr-par.scw.cloud/${SCW_REGISTRY_NAMESPACE}/saveinmed-frontend:latest \
  -t saveinmed-frontend:latest .
```

### 3. Tag e Push

```bash
# Tag com versão e latest
docker tag saveinmed-frontend:latest \
  rg.fr-par.scw.cloud/${SCW_REGISTRY_NAMESPACE}/saveinmed-frontend:${VERSION}

docker tag saveinmed-frontend:latest \
  rg.fr-par.scw.cloud/${SCW_REGISTRY_NAMESPACE}/saveinmed-frontend:latest

# Push ambas as tags
docker push rg.fr-par.scw.cloud/${SCW_REGISTRY_NAMESPACE}/saveinmed-frontend:${VERSION}
docker push rg.fr-par.scw.cloud/${SCW_REGISTRY_NAMESPACE}/saveinmed-frontend:latest
```

### 4. Variáveis de Ambiente (CI/CD)

```bash
# Secrets do Forgejo/GitHub Actions
SCW_SECRET_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx   # IAM → API Keys
SCW_REGISTRY_NAMESPACE=funcscwinfrastructure...       # Container Registry → Namespace
```

---

## Melhores Práticas de Versionamento

### Semantic Versioning (SemVer)

```
MAJOR.MINOR.PATCH

Exemplo: 1.2.3
- MAJOR (1): Breaking changes
- MINOR (2): Novas features (backwards compatible)
- PATCH (3): Bug fixes
```

### Estratégias de Tag

| Tag | Uso | Quando |
|-----|-----|--------|
| `latest` | Desenvolvimento | Apenas em dev/staging |
| `v1.2.3` | Produção | Release oficial |
| `v1.2.3-rc.1` | Release Candidate | Pré-release para testes |
| `sha-abc1234` | CI/CD | Cada commit (rastreabilidade) |
| `main-20241229` | Branch + Data | Builds automáticos |

### Exemplo de Workflow CI/CD

```yaml
# .forgejo/workflows/deploy.yaml
env:
  REGISTRY: rg.fr-par.scw.cloud
  NAMESPACE: ${{ secrets.SCW_REGISTRY_NAMESPACE }}

jobs:
  build:
    steps:
      - name: Generate version
        run: |
          # Usar git tag se existir, senão usar SHA
          VERSION=$(git describe --tags --always 2>/dev/null || git rev-parse --short HEAD)
          echo "VERSION=$VERSION" >> $GITHUB_ENV

      - name: Login to Scaleway Registry
        run: |
          echo "${{ secrets.SCW_SECRET_KEY }}" | \
            docker login $REGISTRY -u nologin --password-stdin

      - name: Build and Push
        run: |
          docker build --platform linux/amd64 \
            -t $REGISTRY/$NAMESPACE/saveinmed-frontend:$VERSION \
            -t $REGISTRY/$NAMESPACE/saveinmed-frontend:latest .
          
          docker push $REGISTRY/$NAMESPACE/saveinmed-frontend:$VERSION
          docker push $REGISTRY/$NAMESPACE/saveinmed-frontend:latest
```

### ✅ Boas Práticas

- **Sempre usar tags específicas** em produção (nunca `latest`)
- **Imagens imutáveis**: uma vez publicada, nunca sobrescrever
- **Multi-stage builds** para imagens menores
- **Scan de vulnerabilidades** antes do deploy
- **Cleanup automático** de imagens antigas no registry

---

## Deploy no Servidor (apolo)

### Pré-requisitos

- Servidor: `apolo.rede5.com.br`
- SO: AlmaLinux com Podman
- Rede: `web_proxy` (para comunicação com Traefik)

### Estrutura de Diretórios

```
/mnt/data/saveinmed/
├── backend/.env          # Variáveis de ambiente backend (Go)
├── backoffice/.env       # Variáveis de ambiente backoffice (NestJS)
└── marketplace/.env      # Variáveis de ambiente marketplace (Vite/React)

/etc/containers/systemd/
├── saveinmed-backend.container
├── saveinmed-backoffice.container
└── saveinmed-marketplace.container
```

> [!NOTE]
> Como as imagens vêm do Scaleway Registry, **não é necessário** clonar o repo no servidor.
> O código está empacotado na imagem Docker.

---

### Passo 1: Criar Diretórios e Arquivos .env

```bash
# Conectar no servidor
ssh root@apolo

# Criar estrutura de diretórios
mkdir -p /mnt/data/saveinmed/{backend,backoffice,marketplace}
```

#### Backend .env (`/mnt/data/saveinmed/backend/.env`)

```bash
# Geral
ENV=production
PORT=8080
TZ=America/Sao_Paulo

# Database
DATABASE_URL=postgresql://USER:PASS@postgres-main:5432/saveinmed?sslmode=disable

# Redis
REDIS_URL=redis://:PASSWORD@redis-saveinmed:6379/0

# JWT
JWT_SECRET=GERAR_SECRET_32_CHARS
JWT_EXPIRATION=7d

# CORS
CORS_ORIGINS=https://saveinmed.com.br,https://api.saveinmed.com.br

# Cookies
COOKIE_SECRET=GERAR_SECRET
COOKIE_DOMAIN=saveinmed.com.br

# Stripe
STRIPE_SECRET_KEY=sk_live_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx

# Scaleway Storage
SCW_ACCESS_KEY=SCWxxx
SCW_SECRET_KEY=xxx
SCW_REGION=fr-par
SCW_BUCKET_NAME=saveinmed-uploads
SCW_ENDPOINT=https://s3.fr-par.scw.cloud
```

#### Marketplace .env (`/mnt/data/saveinmed/marketplace/.env`)

```bash
# Vite/React SPA - variáveis são injetadas no build
# Para produção, configure no Dockerfile ou CI/CD
VITE_API_URL=https://api.saveinmed.com.br
```

> [!NOTE]
> O marketplace é uma SPA (Single Page Application) servida pelo nginx.
> As variáveis `VITE_*` são injetadas em tempo de build, não de runtime.

---

### Passo 2: Criar Arquivos Quadlet

> [!IMPORTANT]
> Quadlet é o sistema nativo do Podman para gerenciar containers como serviços systemd.

#### Backend (`/etc/containers/systemd/saveinmed-backend.container`)

```ini
[Unit]
Description=SaveInMed Backend
After=network-online.target postgres-main.service

[Container]
Image=rg.fr-par.scw.cloud/${SCW_REGISTRY_NAMESPACE}/saveinmed-backend:latest
ContainerName=saveinmed-backend

# Variáveis de ambiente
EnvironmentFile=/mnt/data/saveinmed/backend/.env

# Rede interna (Traefik)
Network=web_proxy
PublishPort=8080:8080

# Labels Traefik
Label=traefik.enable=true
Label=traefik.http.routers.saveinmed-backend.rule=Host(`api.saveinmed.com.br`)
Label=traefik.http.routers.saveinmed-backend.entrypoints=websecure
Label=traefik.http.routers.saveinmed-backend.tls.certresolver=myresolver

[Install]
WantedBy=multi-user.target
```

#### Marketplace (`/etc/containers/systemd/saveinmed-marketplace.container`)

```ini
[Unit]
Description=SaveInMed Marketplace (Vite/React SPA)
After=network-online.target saveinmed-backend.service

[Container]
Image=rg.fr-par.scw.cloud/${SCW_REGISTRY_NAMESPACE}/saveinmed-marketplace:latest
ContainerName=saveinmed-marketplace
Pull=never

# serve roda na porta 3000
Network=web_proxy
PublishPort=3010:3000

# Labels Traefik
Label=traefik.enable=true
Label=traefik.http.routers.saveinmed-marketplace.rule=Host(`saveinmed.com.br`)
Label=traefik.http.routers.saveinmed-marketplace.entrypoints=websecure
Label=traefik.http.routers.saveinmed-marketplace.tls.certresolver=myresolver
Label=traefik.http.services.saveinmed-marketplace.loadbalancer.server.port=3000

[Install]
WantedBy=multi-user.target
```

#### Backoffice (`/etc/containers/systemd/saveinmed-backoffice.container`)

```ini
[Unit]
Description=SaveInMed Backoffice
After=network-online.target saveinmed-backend.service

[Container]
Image=rg.fr-par.scw.cloud/${SCW_REGISTRY_NAMESPACE}/saveinmed-backoffice:latest
ContainerName=saveinmed-backoffice
Pull=never

EnvironmentFile=/mnt/data/saveinmed/backoffice/.env

Network=web_proxy
PublishPort=3011:3000

Label=traefik.enable=true
Label=traefik.http.routers.saveinmed-backoffice.rule=Host(`backoffice.saveinmed.com.br`)
Label=traefik.http.routers.saveinmed-backoffice.entrypoints=websecure
Label=traefik.http.routers.saveinmed-backoffice.tls.certresolver=myresolver
Label=traefik.http.services.saveinmed-backoffice.loadbalancer.server.port=3000

[Install]
WantedBy=multi-user.target
```

---

### Passo 3: Ativar e Iniciar Serviços

```bash
# Recarregar configurações do systemd
systemctl daemon-reload

# Iniciar serviços
systemctl start saveinmed-backend
systemctl start saveinmed-backoffice
systemctl start saveinmed-marketplace

# Habilitar para iniciar no boot
systemctl enable saveinmed-backend
systemctl enable saveinmed-backoffice
systemctl enable saveinmed-marketplace

# Verificar status
systemctl status saveinmed-backend
systemctl status saveinmed-marketplace
```

---

### Passo 4: Atualizar Imagem (sem rebuild)

Como você não vai fazer build da imagem localmente, o processo de atualização é:

```bash
# 1. Fazer pull da nova imagem
podman pull rg.fr-par.scw.cloud/${SCW_REGISTRY_NAMESPACE}/saveinmed-backend:latest

# 2. Reiniciar o serviço (vai usar a nova imagem)
systemctl restart saveinmed-backend
```

Ou para todos:

```bash
# Script de atualização
for svc in backend backoffice marketplace; do
  podman pull rg.fr-par.scw.cloud/${SCW_REGISTRY_NAMESPACE}/saveinmed-${svc}:latest
  systemctl restart saveinmed-${svc}
done
```

---

### Configuração do Traefik

O Traefik já está configurado em `/etc/containers/systemd/traefik.container`:

```ini
[Container]
Image=docker.io/library/traefik:v3.0
ContainerName=traefik
Network=web_proxy

PublishPort=80:80
PublishPort=443:443

Volume=/run/podman/podman.sock:/run/podman/podman.sock:ro
Volume=/opt/traefik/letsencrypt:/letsecrypt

Exec=--providers.docker=true \
     --providers.docker.endpoint=unix:///run/podman/podman.sock \
     --providers.docker.exposedByDefault=false \
     --entryPoints.web.address=:80 \
     --entryPoints.websecure.address=:443 \
     --certificatesResolvers.myresolver.acme.tlschallenge=true \
     --certificatesResolvers.myresolver.acme.email=seu-email@dominio.com \
     --certificatesResolvers.myresolver.acme.storage=/letsecrypt/acme.json
```

> [!TIP]
> Os certificados SSL são gerados automaticamente pelo Let's Encrypt via TLS challenge.

---

### Comandos Úteis

```bash
# Ver containers rodando
podman ps

# Ver logs de um serviço
journalctl -u saveinmed-backend -f

# Reiniciar todos os serviços SaveInMed
systemctl restart saveinmed-*

# Verificar healthcheck
curl -s https://api.saveinmed.com.br/health | jq

# Login no registry antes de pull
echo "${SCW_SECRET_KEY}" | podman login rg.fr-par.scw.cloud -u nologin --password-stdin
```

---

## Como Subir Imagem no Scaleway

### Passo a Passo Rápido

1. **Obter credenciais** no console Scaleway → IAM → API Keys
2. **Configurar variáveis de ambiente** (ver abaixo)
3. **Usar o endpoint de upload** da API

### Endpoint de Upload

```bash
# Upload de imagem de produto
curl -X POST https://api.saveinmed.com.br/v1/products/{productId}/images \
  -H "Authorization: Bearer {token}" \
  -F "image=@/path/to/image.jpg"

# Upload de documento KYC
curl -X POST https://api.saveinmed.com.br/v1/companies/documents \
  -H "Authorization: Bearer {token}" \
  -F "document=@/path/to/license.pdf" \
  -F "type=license"
```

### Response

```json
{
  "url": "https://s3.fr-par.scw.cloud/saveinmed-uploads/products/abc123/main.webp"
}
```

---

## Scaleway Object Storage

O Scaleway Object Storage é um serviço de armazenamento de objetos compatível com a API S3 da AWS. Usamos este serviço para:
- Imagens de produtos
- Documentos KYC (licenças, CNPJs)
- Fotos de perfil
- Comprovantes de entrega

---

## Configuração

### 1. Variáveis de Ambiente

```bash
# .env
SCW_ACCESS_KEY=SCWxxxxxxxxxxxxxxxxx
SCW_SECRET_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
SCW_REGION=fr-par                    # ou nl-ams, pl-waw
SCW_BUCKET_NAME=saveinmed-uploads
SCW_ENDPOINT=https://s3.fr-par.scw.cloud
```

### 2. Estrutura de Buckets

```
saveinmed-uploads/
├── products/           # Imagens de produtos
│   └── {product_id}/
│       ├── main.webp
│       └── thumb.webp
├── documents/          # Documentos KYC
│   └── {company_id}/
│       ├── cnpj.pdf
│       └── license.pdf
├── profiles/           # Fotos de perfil
│   └── {user_id}.webp
└── shipments/          # Comprovantes de entrega
    └── {shipment_id}/
        └── proof.webp
```

---

## Implementação Backend (Go)

### 1. Dependências

```bash
go get github.com/aws/aws-sdk-go-v2/config
go get github.com/aws/aws-sdk-go-v2/service/s3
go get github.com/aws/aws-sdk-go-v2/credentials
```

### 2. Cliente S3 (internal/storage/s3.go)

```go
package storage

import (
    "context"
    "fmt"
    "io"
    "os"
    "time"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/credentials"
    "github.com/aws/aws-sdk-go-v2/service/s3"
)

type S3Client struct {
    client     *s3.Client
    bucketName string
    endpoint   string
}

func NewS3Client() (*S3Client, error) {
    region := os.Getenv("SCW_REGION")
    endpoint := os.Getenv("SCW_ENDPOINT")
    accessKey := os.Getenv("SCW_ACCESS_KEY")
    secretKey := os.Getenv("SCW_SECRET_KEY")
    bucketName := os.Getenv("SCW_BUCKET_NAME")

    resolver := aws.EndpointResolverWithOptionsFunc(
        func(service, region string, options ...interface{}) (aws.Endpoint, error) {
            return aws.Endpoint{URL: endpoint}, nil
        },
    )

    cfg, err := config.LoadDefaultConfig(context.TODO(),
        config.WithRegion(region),
        config.WithEndpointResolverWithOptions(resolver),
        config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKey, secretKey, "")),
    )
    if err != nil {
        return nil, fmt.Errorf("failed to load config: %w", err)
    }

    client := s3.NewFromConfig(cfg, func(o *s3.Options) {
        o.UsePathStyle = true // Required for Scaleway
    })

    return &S3Client{
        client:     client,
        bucketName: bucketName,
        endpoint:   endpoint,
    }, nil
}

// Upload envia um arquivo para o bucket
func (c *S3Client) Upload(ctx context.Context, key string, reader io.Reader, contentType string) (string, error) {
    _, err := c.client.PutObject(ctx, &s3.PutObjectInput{
        Bucket:      aws.String(c.bucketName),
        Key:         aws.String(key),
        Body:        reader,
        ContentType: aws.String(contentType),
        ACL:         "public-read", // ou "private" para documentos sensíveis
    })
    if err != nil {
        return "", fmt.Errorf("failed to upload: %w", err)
    }

    url := fmt.Sprintf("%s/%s/%s", c.endpoint, c.bucketName, key)
    return url, nil
}

// GetPresignedURL gera URL temporária para download
func (c *S3Client) GetPresignedURL(ctx context.Context, key string, expiration time.Duration) (string, error) {
    presigner := s3.NewPresignClient(c.client)
    
    req, err := presigner.PresignGetObject(ctx, &s3.GetObjectInput{
        Bucket: aws.String(c.bucketName),
        Key:    aws.String(key),
    }, s3.WithPresignExpires(expiration))
    if err != nil {
        return "", err
    }
    
    return req.URL, nil
}

// Delete remove um arquivo do bucket
func (c *S3Client) Delete(ctx context.Context, key string) error {
    _, err := c.client.DeleteObject(ctx, &s3.DeleteObjectInput{
        Bucket: aws.String(c.bucketName),
        Key:    aws.String(key),
    })
    return err
}
```

### 3. Handler de Upload (internal/http/handler/upload_handler.go)

```go
package handler

import (
    "fmt"
    "net/http"
    "path/filepath"
    "strings"

    "github.com/google/uuid"
)

const maxUploadSize = 10 << 20 // 10MB

var allowedMimeTypes = map[string]bool{
    "image/jpeg": true,
    "image/png":  true,
    "image/webp": true,
    "application/pdf": true,
}

func (h *Handler) UploadProductImage(w http.ResponseWriter, r *http.Request) {
    // Limitar tamanho
    r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)

    if err := r.ParseMultipartForm(maxUploadSize); err != nil {
        http.Error(w, "Arquivo muito grande (max 10MB)", http.StatusBadRequest)
        return
    }

    file, header, err := r.FormFile("image")
    if err != nil {
        http.Error(w, "Arquivo não encontrado", http.StatusBadRequest)
        return
    }
    defer file.Close()

    // Validar MIME type
    contentType := header.Header.Get("Content-Type")
    if !allowedMimeTypes[contentType] {
        http.Error(w, "Tipo de arquivo não permitido", http.StatusBadRequest)
        return
    }

    // Gerar key única
    productID := r.PathValue("productId")
    ext := filepath.Ext(header.Filename)
    key := fmt.Sprintf("products/%s/%s%s", productID, uuid.New().String(), ext)

    // Upload
    url, err := h.storage.Upload(r.Context(), key, file, contentType)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{"url": url})
}
```

---

## Implementação Frontend (TypeScript)

### 1. Serviço de Upload (src/services/storageService.ts)

```typescript
import { apiClient } from './apiClient'

interface UploadResponse {
  url: string
}

export const storageService = {
  // Upload de imagem de produto
  uploadProductImage: async (productId: string, file: File): Promise<string> => {
    const formData = new FormData()
    formData.append('image', file)
    
    const response = await apiClient.post<UploadResponse>(
      `/v1/products/${productId}/images`,
      formData
    )
    return response.url
  },

  // Upload de documento KYC
  uploadDocument: async (file: File, type: 'cnpj' | 'license' | 'other'): Promise<string> => {
    const formData = new FormData()
    formData.append('document', file)
    formData.append('type', type)
    
    const response = await apiClient.post<UploadResponse>(
      '/v1/companies/documents',
      formData
    )
    return response.url
  },

  // Upload com preview e progresso
  uploadWithProgress: async (
    endpoint: string,
    file: File,
    onProgress: (percent: number) => void
  ): Promise<string> => {
    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest()
      const formData = new FormData()
      formData.append('file', file)

      xhr.upload.addEventListener('progress', (e) => {
        if (e.lengthComputable) {
          onProgress(Math.round((e.loaded / e.total) * 100))
        }
      })

      xhr.addEventListener('load', () => {
        if (xhr.status === 200) {
          const response = JSON.parse(xhr.responseText)
          resolve(response.url)
        } else {
          reject(new Error('Upload failed'))
        }
      })

      xhr.addEventListener('error', () => reject(new Error('Upload failed')))
      
      xhr.open('POST', `${import.meta.env.VITE_API_URL}${endpoint}`)
      xhr.setRequestHeader('Authorization', `Bearer ${localStorage.getItem('token')}`)
      xhr.send(formData)
    })
  }
}
```

### 2. Componente de Upload (src/components/ImageUpload.tsx)

```tsx
import { useState, useRef } from 'react'
import { storageService } from '../services/storageService'

interface ImageUploadProps {
  productId: string
  onUploadComplete: (url: string) => void
}

export function ImageUpload({ productId, onUploadComplete }: ImageUploadProps) {
  const [uploading, setUploading] = useState(false)
  const [progress, setProgress] = useState(0)
  const [preview, setPreview] = useState<string | null>(null)
  const inputRef = useRef<HTMLInputElement>(null)

  const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0]
    if (!file) return

    // Validações
    if (!['image/jpeg', 'image/png', 'image/webp'].includes(file.type)) {
      alert('Apenas JPEG, PNG ou WebP são permitidos')
      return
    }

    if (file.size > 10 * 1024 * 1024) {
      alert('Máximo 10MB')
      return
    }

    // Preview local
    setPreview(URL.createObjectURL(file))
    setUploading(true)

    try {
      const url = await storageService.uploadWithProgress(
        `/v1/products/${productId}/images`,
        file,
        setProgress
      )
      onUploadComplete(url)
    } catch (err) {
      console.error('Upload failed:', err)
      alert('Falha no upload')
    } finally {
      setUploading(false)
      setProgress(0)
    }
  }

  return (
    <div className="upload-container">
      <input
        ref={inputRef}
        type="file"
        accept="image/jpeg,image/png,image/webp"
        onChange={handleFileSelect}
        hidden
      />
      
      <button 
        onClick={() => inputRef.current?.click()}
        disabled={uploading}
      >
        {uploading ? `Enviando ${progress}%` : '📤 Enviar Imagem'}
      </button>

      {preview && (
        <img src={preview} alt="Preview" className="upload-preview" />
      )}
    </div>
  )
}
```

---

## Melhores Práticas

### 1. Segurança

- ✅ **Validar MIME type** no backend (não confiar no frontend)
- ✅ **Limitar tamanho** máximo de upload (10MB recomendado)
- ✅ **Usar URLs presignadas** para documentos sensíveis
- ✅ **Verificar autenticação** antes de permitir upload
- ✅ **Sanitizar nomes** de arquivos (usar UUID)

### 2. Performance

- ✅ **Compressão de imagens** antes do upload (client-side)
- ✅ **Gerar thumbnails** automaticamente
- ✅ **Usar WebP** como formato preferencial
- ✅ **CDN** em produção para distribuição global

### 3. Organização

```bash
# Estrutura de keys recomendada
{bucket}/{tipo}/{entity_id}/{arquivo}

# Exemplos:
products/abc123/main.webp
documents/company456/license.pdf
profiles/user789.webp
```

### 4. Custo

- ✅ **Lifecycle rules** para limpar arquivos antigos
- ✅ **Storage class** adequado (Standard vs Glacier-like)
- ✅ **Monitorar uso** para evitar surpresas

---

## Configuração do Bucket (Console Scaleway)

1. Acesse **Object Storage** no console Scaleway
2. Clique em **Create a bucket**
3. Nome: `saveinmed-uploads`
4. Região: `fr-par` (ou mais próxima)
5. Visibility: **Private** (acesso via API)
6. Configure **CORS**:

```json
[
  {
    "AllowedOrigins": ["https://saveinmed.com.br", "http://localhost:5173"],
    "AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
    "AllowedHeaders": ["*"],
    "MaxAgeSeconds": 3600
  }
]
```

---

## Troubleshooting

### Erro: "Access Denied"
- Verifique as credenciais `SCW_ACCESS_KEY` e `SCW_SECRET_KEY`
- Confirme que as permissões do bucket estão corretas

### Erro: "CORS Policy"
- Configure CORS no bucket (ver acima)
- Adicione o domínio do frontend na lista de origens permitidas

### Upload lento
- Use upload multipart para arquivos > 5MB
- Considere compressão client-side para imagens

---

## Referências

- [Scaleway Object Storage Documentation](https://www.scaleway.com/en/docs/object-storage-feature/)
- [AWS SDK for Go v2](https://aws.github.io/aws-sdk-go-v2/docs/)
- [S3-Compatible API Reference](https://www.scaleway.com/en/docs/object-storage-s3-api/)
