Files
MeowSpool/backend/agent.md
T
Felipe d5af338ee1 feat: implement email verification and password reset features
- Add EmailTokenService for handling email verification and password reset tokens.
- Create EmailService for sending verification and reset emails via SMTP.
- Update AuthService to handle email verification status during login.
- Modify user registration to redirect to a check email screen instead of issuing a token.
- Implement resend verification email functionality.
- Add deep link handling for email verification and password reset in the mobile app.
- Update mobile app routes and components to support new email verification flow.
- Enhance error handling for unverified emails during login attempts.
- Update configuration to include SMTP settings for email service.
2026-03-14 23:38:15 -03:00

30 KiB
Raw Blame History

MeowSpool Backend — Agent Guide

Visão Geral

Backend da aplicação MeowSpool escrito em Rust, utilizando Axum como framework HTTP e SQLx com PostgreSQL como banco de dados. A arquitetura segue o padrão Hexagonal (Ports & Adapters), garantindo que o núcleo do domínio seja completamente isolado de detalhes de infraestrutura.


Stack

Componente Biblioteca Versão mínima
HTTP Framework axum 0.7
Async Runtime tokio (full) 1.x
ORM / Query sqlx (postgres, uuid, time, macros) 0.8
Autenticação JWT jsonwebtoken 9.x
Hash de senha argon2 0.5
OAuth Google oauth2 4.x
UUID uuid (v4, serde) 1.x
Serialização serde, serde_json 1.x
Erros thiserror 1.x
Env vars dotenvy 0.15
Logging tracing, tracing-subscriber 0.1
Validação validator 0.18
HTTP Client reqwest (json, rustls-tls) 0.12
Email (SMTP) lettre (smtp-transport, tokio1-rustls-tls, builder) 0.11
Geração QR Code qrcode 0.14
Geração de imagem image 0.25
Encode Base64 base64 0.22
Geração PDF printpdf 0.7

Estrutura de Pastas

backend/
├── Cargo.toml
├── Cargo.lock
├── .env.example
├── agent.md                           <- este arquivo
├── migrations/                        <- SQL puro, gerenciado pelo SQLx CLI
│   ├── 20240101000001_create_users.sql
│   ├── 20240101000002_create_spool_presets.sql
│   ├── 20240101000003_create_filaments.sql
│   └── 20240101000004_create_token_tables.sql  <- password_reset_tokens + email_verification_tokens
└── src/
    ├── main.rs                        <- entry point: inicializa config, DB, router e servidor
    ├── config.rs                      <- struct Config lida de variáveis de ambiente
    ├── error.rs                       <- AppError unificado com IntoResponse
    ├── router.rs                      <- composição de todas as rotas Axum
    │
    ├── domain/                        <- NÚCLEO: entidades puras, sem dependências externas
    │   ├── mod.rs
    │   ├── user.rs                    <- struct User, enum AuthProvider
    │   ├── filament.rs                <- struct Filament, enum Material
    │   └── spool_preset.rs            <- struct SpoolPreset
    │
    ├── ports/                         <- INTERFACES: traits que o domínio exige
    │   ├── mod.rs
    │   ├── user_repository.rs         <- trait UserRepository
    │   ├── filament_repository.rs     <- trait FilamentRepository
    │   └── spool_preset_repository.rs <- trait SpoolPresetRepository
    │
    ├── application/                   <- CASOS DE USO: orquestram domínio + ports
    │   ├── mod.rs
    │   ├── auth_service.rs            <- login, register, OAuth, refresh, logout
    │   ├── email_token_service.rs     <- forgot_password, verify_email, reset_password (tokens DB + email)
    │   ├── filament_service.rs        <- CRUD, cálculo de peso líquido, QR, SVG
    │   └── spool_preset_service.rs    <- CRUD presets (system read-only, user CRUD)
    │
    ├── infrastructure/                <- Serviços externos (SMTP, etc.)
    │   ├── mod.rs
    │   └── email_service.rs           <- EmailService: SMTP via lettre (STARTTLS)
    │
    └── adapters/
        ├── inbound/                   <- HTTP: recebe requisições, delega ao application
        │   ├── mod.rs
        │   ├── auth_handler.rs
        │   ├── filament_handler.rs
        │   ├── spool_preset_handler.rs
        │   ├── user_handler.rs
        │   └── middleware/
        │       └── auth.rs            <- extrator JWT que popula CurrentUser no estado
        └── outbound/                  <- INFRAESTRUTURA: implementa as traits de ports
            ├── mod.rs
            ├── postgres_user_repo.rs
            ├── postgres_filament_repo.rs
            └── postgres_spool_preset_repo.rs

Arquitetura Hexagonal — Regras de Dependência

adapters/inbound (HTTP)
        |
        v
  application (services)
        |
        v
    domain (entidades)
        |
        ^
    ports (traits)
        |
        ^
adapters/outbound (PostgreSQL)

Regra fundamental: O domain e os ports nunca importam nada de adapters ou application. Dependências permitidas no domínio: uuid, serde, time. Qualquer violação é um bug arquitetural.


Rotas da API

Todas as rotas são prefixadas com /api/v1.

Auth — /api/v1/auth

Método Rota Handler Acesso
POST /register register_handler Público
POST /login login_handler Público
POST /oauth/google google_oauth_handler Público
POST /refresh refresh_token_handler Público (requer refresh token)
POST /logout logout_handler Autenticado
POST /resend-verification resend_verification_handler Público (sempre 200)
POST /forgot-password forgot_password_handler Público
POST /verify-email verify_email_handler Público
POST /reset-password reset_password_handler Público (requer token de reset)
Método Rota Handler Descrição
GET /verify-email verify_email_redirect_handler Redireciona 302 → {APP_SCHEME}://verify-email?token=xxx
GET /reset-password reset_password_redirect_handler Redireciona 302 → {APP_SCHEME}://reset-password?token=xxx

Esses endpoints são os destinos dos links nos e-mails. Clientes de e-mail (Gmail etc.) aceitam URLs https:// normalmente; o backend redireciona para o deep link do app. O OS reconhece o scheme e abre o MeowSpool.

Users — /api/v1/users

Método Rota Handler Acesso
GET /me get_me_handler Autenticado
PUT /me update_me_handler Autenticado

Filaments — /api/v1/filaments

Método Rota Handler Acesso
GET / list_filaments_handler Autenticado
POST / create_filament_handler Autenticado
GET /:id get_filament_handler Autenticado
PUT /:id update_filament_handler Autenticado
DELETE /:id delete_filament_handler Autenticado
GET /:id/qrcode get_qrcode_handler Autenticado
GET /:id/label.svg export_label_handler Autenticado
GET /:id/label.pdf export_label_pdf_handler Autenticado

Query params de listagem (GET /filaments):

  • material — filtra por tipo (PLA, ABS, PETG, TPU, ASA, PA, PC...)
  • brand — filtra por marca
  • search — busca em marca, modelo e notas
  • stock_levellow (<=15%), medium (<=35%), ok (>35%)
  • sortnet_weight_asc, net_weight_desc, created_at_desc (padrão)
  • page / per_page — paginação (padrão: página 1, 20 itens)

Spool Presets — /api/v1/spool-presets

Método Rota Handler Acesso
GET / list_presets_handler Autenticado
POST / create_preset_handler Autenticado
PUT /:id update_preset_handler Autenticado (apenas presets do user)
DELETE /:id delete_preset_handler Autenticado (apenas presets do user)

Dashboard — /api/v1/dashboard

Método Rota Handler Acesso
GET / dashboard_handler Autenticado

Resposta do dashboard:

{
  "total_stock_kg": 14.2,
  "low_stock_count": 3,
  "by_material": [
    { "material": "PLA", "count": 4, "total_kg": 5.8 }
  ],
  "low_stock_filaments": [
    { "id": "...", "model": "PETG", "net_weight_g": 120, "percentage": 12 }
  ],
  "recent_filaments": [...]
}

Padrão de Erros

Use thiserror em todos os módulos e um AppError central em src/error.rs:

#[derive(Debug, thiserror::Error)]
pub enum AppError {
    #[error("not found")]
    NotFound,

    #[error("unauthorized")]
    Unauthorized,

    #[error("forbidden")]
    Forbidden,

    #[error("validation error: {0}")]
    Validation(String),

    #[error("bad request: {0}")]
    BadRequest(String),   // token inválido, expirado, já utilizado

    #[error("conflict: {0}")]
    Conflict(String),

    #[error("unprocessable entity: {0}")]
    UnprocessableEntity(String),

    #[error("internal error")]
    Internal(#[from] anyhow::Error),
}

AppError implementa IntoResponse do Axum, mapeando cada variante para o status HTTP correto e body JSON consistente:

{ "error": "not found", "code": "NOT_FOUND" }

Convenções de Código

Nomenclatura

  • Structs de domínio: PascalCaseUser, Filament, SpoolPreset
  • Traits (ports): PascalCase com sufixo RepositoryUserRepository
  • Implementações concretas: prefixo do banco — PostgresUserRepository
  • Serviços: sufixo ServiceAuthService, FilamentService
  • Handlers: sufixo _handlerlogin_handler, create_filament_handler
  • DTOs de entrada: sufixo RequestCreateFilamentRequest
  • DTOs de saída: sufixo ResponseFilamentResponse

Estrutura de um Handler

Todo handler deve ser uma função async pequena. A lógica de negócio nunca fica no handler — ela fica no application/*_service.rs.

// adapters/inbound/filament_handler.rs
pub async fn create_filament_handler(
    State(state): State<AppState>,
    Extension(current_user): Extension<CurrentUser>,
    Json(req): Json<CreateFilamentRequest>,
) -> Result<impl IntoResponse, AppError> {
    req.validate()?;
    let filament = state.filament_service.create(current_user.id, req).await?;
    Ok((StatusCode::CREATED, Json(FilamentResponse::from(filament))))
}

Estrutura de um Service

O service recebe repositórios via injeção de dependência (trait objects em Arc):

// application/filament_service.rs
pub struct FilamentService {
    repo: Arc<dyn FilamentRepository>,
    preset_repo: Arc<dyn SpoolPresetRepository>,
}

impl FilamentService {
    pub async fn create(&self, user_id: Uuid, req: CreateFilamentRequest) -> Result<Filament, AppError> {
        // 1. buscar preset para calcular net_weight
        // 2. calcular net_weight_g = total_weight_g - spool_weight_g
        // 3. construir entidade Filament
        // 4. persistir via repo
        // 5. retornar entidade
    }
}

Estrutura de um Repository (Port)

// ports/filament_repository.rs
#[async_trait::async_trait]
pub trait FilamentRepository: Send + Sync {
    async fn find_by_id(&self, id: Uuid, user_id: Uuid) -> Result<Option<Filament>, AppError>;
    async fn list(&self, user_id: Uuid, filter: FilamentFilter) -> Result<Vec<Filament>, AppError>;
    async fn create(&self, filament: &Filament) -> Result<Filament, AppError>;
    async fn update(&self, filament: &Filament) -> Result<Filament, AppError>;
    async fn delete(&self, id: Uuid, user_id: Uuid) -> Result<(), AppError>;
}

Migrations

Use o SQLx CLI para gerenciar migrations:

# instalar
cargo install sqlx-cli --no-default-features --features postgres

# criar nova migration
sqlx migrate add <nome_descritivo>

# aplicar
sqlx migrate run

# reverter
sqlx migrate revert

Arquivos ficam em backend/migrations/. Nomeie com timestamp e descrição clara.


Variáveis de Ambiente

Copie .env.example para .env antes de rodar. Variáveis principais:

Variável Obrigatória Padrão Descrição
DATABASE_URL Connection string PostgreSQL
JWT_SECRET Segredo de assinatura JWT
JWT_EXPIRY_SECS 3600 TTL do access token (segundos)
JWT_REFRESH_EXPIRY_SECS 2592000 TTL do refresh token (segundos)
GOOGLE_CLIENT_ID Client ID OAuth Google
GOOGLE_CLIENT_SECRET Client Secret OAuth Google
HOST 0.0.0.0 Endereço de bind do servidor
PORT 8080 Porta do servidor
APP_ENV development development ou production
SMTP_HOST smtp.gmail.com Servidor SMTP
SMTP_PORT 587 Porta SMTP (STARTTLS)
SMTP_USER Usuário SMTP (e-mail)
SMTP_PASS Senha / App Password SMTP
EMAIL_FROM noreply@meowspool.app Endereço remetente dos e-mails
APP_BASE_URL http://localhost:8080 URL base usada nos links de e-mail. Em produção: https://meowspool.felipecncloud.com/api/v1
APP_SCHEME meowspool Scheme do deep link do app mobile. Usado nos redirects de e-mail

Como Adicionar um Novo Endpoint (Passo a Passo)

  1. Domain — adicione ou modifique a entidade em src/domain/.
  2. Port — adicione o método necessário no trait em src/ports/.
  3. Outbound Adapter — implemente o método no repositório PostgreSQL em src/adapters/outbound/.
  4. Application Service — adicione o caso de uso em src/application/, chamando o port.
  5. Request/Response DTOs — defina structs com serde e validator no handler.
  6. Inbound Handler — crie o handler em src/adapters/inbound/, delegando para o service.
  7. Router — registre a rota em src/router.rs.
  8. Migration — se necessário, crie um arquivo SQL em migrations/.

Como Rodar Localmente

# na raiz do backend/
cp .env.example .env
# edite .env com suas credenciais locais

# subir PostgreSQL via Docker
docker run -d \
  --name meowspool-db \
  -e POSTGRES_USER=meowspool \
  -e POSTGRES_PASSWORD=meowspool \
  -e POSTGRES_DB=meowspool \
  -p 5432:5432 \
  postgres:16-alpine

# rodar migrations
sqlx migrate run

# rodar em modo watch (requer cargo-watch)
cargo watch -x run

Sincronização Offline (Estratégia)

O backend implementa last-write-wins com timestamp:

  • Toda entidade possui updated_at (timestamp UTC).
  • O mobile envia o updated_at local no body do PUT.
  • Se o updated_at do servidor for mais recente, retorna 409 Conflict com a versão do servidor.
  • Se o updated_at do cliente for mais recente (ou igual), o servidor aceita a atualização.

Geração de QR Code e Etiqueta SVG

QR Code (GET /filaments/:id/qrcode)

  • Gera QR Code com deep link: meowspool://filament/:id (singular)
  • Retorna PNG (image/png) por padrão, ou SVG com ?format=svg
  • Biblioteca: crate qrcode

Etiqueta SVG (GET /filaments/:id/label.svg)

  • Query params: width_mm (padrão: 50), height_mm (padrão: 30), fields (opcional), dpi (opcional, padrão 96)
  • Layout adaptativo: modo mini para width_mm < 30 ou height_mm < 20
  • dpi afeta px_per_mm e portanto as dimensões em pixels do SVG gerado
  • Content-Type: image/svg+xml
  • Content-Disposition: attachment; filename="meowspool-label-{id}.svg"

Etiqueta PDF (GET /filaments/:id/label.pdf)

  • Query params: width_mm (padrão: 50), height_mm (padrão: 30), fields (opcional), dpi (opcional, reservado)
  • Layout adaptativo: modo mini para width_mm < 30 ou height_mm < 20 — fontes e posições proporcionais, garante que nenhum elemento saia da página
  • Gerado com printpdf: fundo escuro #1E1B18, barra de cor do filamento, texto com Helvetica builtin, QR Code PNG luma embutido com interpolate: false (bordas nítidas em impressoras térmicas)
  • Content-Type: application/pdf
  • Content-Disposition: attachment; filename="meowspool-label-{id}.pdf"

Parâmetro fields (SVG e PDF)

Controla quais elementos aparecem na etiqueta. Valor: string com itens separados por vírgula.

Valor Elemento
color Barra de cor lateral
name Nome do modelo
material_brand Texto "Marca · Material"
net_weight Peso líquido disponível
print_temp Temperatura de impressão
qrcode QR Code com deep link

Se fields for omitido, todos os elementos são incluídos.

Implementado em LabelFields (filament_service.rs) — LabelFields::from_str(Option<&str>).

Parâmetro dpi (SVG e PDF)

Controla o DPI alvo da impressora. Afeta o cálculo de pixels do SVG e garante QR nítido em impressoras térmicas.

Impressora dpi recomendado
Tela / padrão 96 (omitir param)
Niimbot D11/D110 203
Brother QL / Dymo 300

Layout adaptativo ("mini" vs padrão)

Etiquetas com width_mm < 30 ou height_mm < 20 ativam o modo mini, que:

  • Usa fontes menores (proporcionais à altura física)
  • Posiciona QR Code com margem de 1mm (vs 2mm no padrão)
  • Estreita a barra de cor (~11% da largura vs ~5% + 6px no padrão)
  • Encurta o rótulo de peso: "320g" em vez de "320g disponível"
  • Separador de marca/material: espaço simples em vez de · (economiza espaço)
  • No SVG, o separador de meta usa espaço em vez de ·
  • Garante que nenhum texto extrapole a borda inferior da etiqueta

Exemplo para Niimbot 22×14mm:

GET /api/v1/filaments/:id/label.pdf?width_mm=22&height_mm=14&fields=color,name,qrcode&dpi=203
GET /api/v1/filaments/:id/label.svg?width_mm=22&height_mm=14&fields=color,name,qrcode&dpi=203

Segurança

  • Senhas hasheadas com Argon2id (crate argon2).
  • JWT assinado com HS256 — nunca exponha o JWT_SECRET.
  • Toda rota autenticada passa pelo middleware auth.rs que valida o token e popula Extension<CurrentUser>.
  • Queries usam bind parameters do SQLx — nunca interpolação de string em SQL.
  • user_id é sempre extraído do token JWT, nunca aceito como parâmetro de URL ou body.
  • Presets do sistema (is_system = true) são protegidos no nível de serviço: edição ou deleção retorna 403 Forbidden.

Mudanças Recentes (14/03/2026)

Reenvio de e-mail de verificação

Arquivos modificados: src/application/email_token_service.rs, src/adapters/inbound/auth_handler.rs, src/router.rs

Novo endpoint: POST /api/v1/auth/resend-verification — body: { "email": "..." }

  • Sempre retorna 200 OK (não vaza se e-mail existe ou já foi verificado)
  • Ignora silenciosamente se: e-mail não cadastrado, conta já verificada
  • Reutiliza send_verification_email internamente (gera novo token com TTL 24h)

Novo método (email_token_service.rs):

pub async fn resend_verification_email(&self, email: &str) -> Result<(), AppError> {
    let Some(user) = self.user_repo.find_by_email(email).await? else { return Ok(()); };
    if user.email_verified { return Ok(()); }
    self.send_verification_email(user.id, email).await
}

Registro não emite tokens — login bloqueado sem verificação de e-mail

Arquivos modificados: src/application/auth_service.rs, src/adapters/inbound/auth_handler.rs, src/error.rs

Problema: após o registro, o app autenticava o usuário imediatamente (emitia tokens) sem exigir verificação de e-mail. Login também aceitava contas não verificadas.

Mudanças:

  • AppError::EmailNotVerified adicionado → HTTP 403, code "EMAIL_NOT_VERIFIED"
  • AuthService::register passa a retornar apenas User (sem TokenPair) — o handler responde 201 sem body de auth
  • AuthService::login verifica user.email_verified antes de emitir tokens:
    if !user.email_verified {
        return Err(AppError::EmailNotVerified);
    }
    

Fluxo resultante:

  1. Registro → 201 Created (sem tokens) → backend envia e-mail de verificação em background
  2. Login com e-mail não verificado → 403 { "code": "EMAIL_NOT_VERIFIED" }
  3. Após verificar e-mail → login funciona normalmente

Arquivos modificados: src/adapters/inbound/auth_handler.rs, src/router.rs, src/config.rs, .env

Problema: Clientes de e-mail (Gmail, etc.) bloqueiam links com scheme customizado (meowspool://). O link no e-mail não abria o app.

Solução: O backend agora gera links https:// nos e-mails. Ao clicar, o backend redireciona (302) para o deep link do app.

Fluxo completo:

E-mail → https://meowspool.felipecncloud.com/api/v1/verify-email?token=xxx
                              ↓ GET (browser abre normalmente)
              Backend responde 302 Location: meowspool://verify-email?token=xxx
                              ↓ OS reconhece o scheme
                     App MeowSpool abre → app/verify-email.tsx
                              ↓
                 POST /auth/verify-email { token } → verifica no banco

Novos handlers (auth_handler.rs):

  • verify_email_redirect_handlerGET /api/v1/verify-email?token=xxx
  • reset_password_redirect_handlerGET /api/v1/reset-password?token=xxx

Novo campo Config (config.rs):

  • app_scheme: String — lido de APP_SCHEME (padrão: meowspool)

.env atualizado:

  • APP_BASE_URL=https://meowspool.felipecncloud.com/api/v1 (antes: meowspool:/)
  • APP_SCHEME=meowspool (novo)

Fluxo completo de e-mail: verificação e reset de senha

Arquivos criados/modificados: migrations/20240101000004_create_token_tables.sql, src/infrastructure/email_service.rs, src/infrastructure/mod.rs, src/application/email_token_service.rs, src/adapters/inbound/auth_handler.rs, src/config.rs, src/error.rs, src/main.rs, src/router.rs, Cargo.toml

Migrations

Criadas as tabelas password_reset_tokens e email_verification_tokens com:

  • UUID como PK (gen_random_uuid)
  • token TEXT UNIQUE — indexado para lookup rápido
  • used_at TIMESTAMPTZNULL = não usado; preenchido na validação para invalidar após uso
  • expires_at TIMESTAMPTZ — TTL: 1h para reset de senha, 24h para verificação de e-mail

EmailService (src/infrastructure/email_service.rs)

Envia e-mails via SMTP com STARTTLS usando lettre. Métodos:

  • send_password_reset(to, reset_link) — e-mail de redefinição de senha
  • send_email_verification(to, verify_link) — e-mail de confirmação de conta

EmailTokenService (src/application/email_token_service.rs)

Orquestra tokens no banco + envio de e-mail. Métodos:

  • forgot_password(email) — cria token em password_reset_tokens, envia e-mail. Sempre retorna Ok (não vaza se o e-mail existe)
  • verify_email(token) — valida token em email_verification_tokens, chama user_repo.verify_email(), marca token como usado
  • reset_password(token, new_password) — valida token em password_reset_tokens, re-hash da senha com Argon2id, atualiza usuário, marca token como usado
  • send_verification_email(user_id, email) — cria token em email_verification_tokens e envia e-mail. Chamado após o registro

Handlers implementados

Anteriormente retornavam 200 OK sem lógica. Agora delegam ao EmailTokenService:

  • forgot_password_handler — POST /auth/forgot-password
  • verify_email_handler — POST /auth/verify-email
  • reset_password_handler — POST /auth/reset-password

Registro envia e-mail de verificação

register_handler chama email_token_service.send_verification_email() via tokio::spawn (background) após criar o usuário — o cadastro responde imediatamente sem esperar o SMTP.

AppError::BadRequest adicionado

Nova variante para erros previsíveis do usuário (token inválido, expirado, já usado) → HTTP 400.

Configuração APP_BASE_URL

  • Em produção: APP_BASE_URL=https://meowspool.felipecncloud.com/api/v1
    • Gera links nos e-mails como https://meowspool.felipecncloud.com/api/v1/verify-email?token=xxx
    • O backend redireciona esse GET para o deep link via APP_SCHEME

Suporte a Impressoras Pequenas — Niimbot e layout adaptativo

Arquivos alterados: filament_handler.rs, filament_service.rs

Novo query param dpi

Adicionado campo dpi: Option<u32> ao LabelQuery (handler). Aceito em /label.svg e /label.pdf. Valores recomendados:

Impressora dpi
Tela / padrão omitir (96)
Niimbot D11/D110 203
Brother / Dymo 300

No SVG, dpi define px_per_mm = dpi / 25.4 — o tamanho em pixels do arquivo corresponde à resolução física da impressora. No PDF, o dpi é ignorado (PDF já trabalha em mm), mas reservado para uso futuro.

Layout adaptativo — modo "mini"

Ativado quando width_mm < 30 ou height_mm < 20 (ex: Niimbot 22×14mm). Diferenças em relação ao modo padrão:

Aspecto Padrão Mini
Barra de cor 3.5mm fixo 11% da largura
QR size 75% da altura 80% da altura
Margem QR 2mm 1mm
Y do texto h - 8/14/20 (fixo, quebrava para h<20) h * 0.72/0.50/0.28 (proporcional)
Fontes 79pt fixo proporcionais à altura física
Rótulo peso "320g disponível" "320g"
Separador meta · (espaço)
interpolate no QR (PDF) true false (bordas nítidas em térmica)

Correção crítica: no modo anterior, Mm(h - 20.0) gerava coordenada negativa para h < 20mm (Niimbot), colocando o texto fora da página.


Exportação de Etiqueta PDF (GET /filaments/:id/label.pdf)

  • Dependência: printpdf = "0.7" adicionada ao Cargo.toml
  • Service: FilamentService::generate_label_pdf em filament_service.rs
    • Layout: fundo #1E1B18, barra de cor (filament.color_hex), texto Helvetica builtin, QR Code PNG luma embutido
    • Página com dimensões exatas em mm via printpdf::Mm
    • Usa Polygon + PaintMode::Fill para retângulos (API do printpdf 0.7)
    • QR Code embutido via ImageXObject com pixels luma brutos — sem depender do feature image do printpdf
  • Handler: export_label_pdf_handler em filament_handler.rs
  • Rota: GET /api/v1/filaments/:id/label.pdf

Controle de Campos na Etiqueta (fields query param)

  • Struct: LabelFields em filament_service.rs — parseia string CSV de campos ativos
  • Aplicado em: generate_label_svg e generate_label_pdf
  • LabelQuery: adicionado campo fields: Option<String> em filament_handler.rs
  • Permite que o cliente selecione quais elementos aparecem na etiqueta (cor, nome, material, peso, temp, QR)

Correção: PUT Spool Preset retornava FORBIDDEN

Local: src/adapters/outbound/postgres_spool_preset_repo.rs — método update()

Problema: O SQL UPDATE não validava o user_id na cláusula WHERE. Qualquer usuário poderia tentar editar presets de outros usuários ou presets do sistema.

// ❌ ANTES (inseguro)
UPDATE spool_presets
SET name = $2, spool_weight_g = $3
WHERE id = $1 AND is_system = false
RETURNING ...

Solução: Adicionado AND user_id = $4 para validar propriedade antes de atualizar:

// ✅ DEPOIS (seguro)
UPDATE spool_presets
SET name = $2, spool_weight_g = $3
WHERE id = $1 AND is_system = false AND user_id = $4
RETURNING ...

Agora o repositório valida que o preset pertence ao usuário autenticado (extraído do JWT). Tentativas de editar presets de outro usuário ou do sistema recebem 404 Not Found (sem vazar que o preset existe).