- Add `FilamentService` for managing filament inventory, including creation, retrieval, updating, and deletion of filaments. - Introduce `SpoolPresetService` for handling spool presets, allowing users to create, update, and delete their custom presets. - Create domain models for `Filament` and `SpoolPreset` with necessary fields and methods. - Define repository interfaces for filament and spool preset persistence. - Implement application configuration management from environment variables. - Set up error handling with a centralized `AppError` type. - Build the Axum router with public and protected routes for user authentication and resource management.
384 lines
14 KiB
Markdown
384 lines
14 KiB
Markdown
# 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<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):
|
|
|
|
```rust
|
|
// 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)
|
|
|
|
```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<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:
|
|
|
|
```bash
|
|
# 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. 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://filaments/:id`
|
|
- 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<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`.
|