# 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 | | Geração QR Code | `qrcode` | 0.14 | --- ## 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 └── 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 │ ├── filament_service.rs <- CRUD, cálculo de peso líquido, QR, SVG │ └── spool_preset_service.rs <- CRUD presets (system read-only, user CRUD) │ └── 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 | `/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)| ### 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 | **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_level` — `low` (<=15%), `medium` (<=35%), `ok` (>35%) - `sort` — `net_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:** ```json { "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`: ```rust #[derive(Debug, thiserror::Error)] pub enum AppError { #[error("not found")] NotFound, #[error("unauthorized")] Unauthorized, #[error("forbidden")] Forbidden, #[error("validation error: {0}")] Validation(String), #[error("conflict: {0}")] Conflict(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: ```json { "error": "not found", "code": "NOT_FOUND" } ``` --- ## Convenções de Código ### Nomenclatura - **Structs de domínio:** `PascalCase` — `User`, `Filament`, `SpoolPreset` - **Traits (ports):** `PascalCase` com sufixo `Repository` — `UserRepository` - **Implementações concretas:** prefixo do banco — `PostgresUserRepository` - **Serviços:** sufixo `Service` — `AuthService`, `FilamentService` - **Handlers:** sufixo `_handler` — `login_handler`, `create_filament_handler` - **DTOs de entrada:** sufixo `Request` — `CreateFilamentRequest` - **DTOs de saída:** sufixo `Response` — `FilamentResponse` ### 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`. ```rust // adapters/inbound/filament_handler.rs pub async fn create_filament_handler( State(state): State, Extension(current_user): Extension, Json(req): Json, ) -> Result { 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): ```rust // application/filament_service.rs pub struct FilamentService { repo: Arc, preset_repo: Arc, } impl FilamentService { pub async fn create(&self, user_id: Uuid, req: CreateFilamentRequest) -> Result { // 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) ```rust // 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, AppError>; async fn list(&self, user_id: Uuid, filter: FilamentFilter) -> Result, AppError>; async fn create(&self, filament: &Filament) -> Result; async fn update(&self, filament: &Filament) -> Result; async fn delete(&self, id: Uuid, user_id: Uuid) -> Result<(), AppError>; } ``` --- ## Migrations Use o **SQLx CLI** para gerenciar migrations: ```bash # instalar cargo install sqlx-cli --no-default-features --features postgres # criar nova migration sqlx migrate add # 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. Veja o arquivo `.env.example` para a lista completa. --- ## 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 ```bash # 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) - Retorna SVG com: cor visual, modelo, material, marca, peso líquido e QR Code embutido - `Content-Type: image/svg+xml` - `Content-Disposition: attachment; filename="meowspool-label-{id}.svg"` --- ## 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`. - 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`.